Technical SEO

JavaScript SEO: How Rendering Problems Affect Crawling and Indexing

The site we audited was a Vite + React single-page application for a structured credit boutique. Six landing pages lived on path routes: /partners, /sell-your-quota, /simulator and three others. Each

Nayara Martins
Nayara Martins Desenvolvedora de Sistemas Web
20 de setembro de 2026
9 min de leitura
JavaScript SEO: How Rendering Problems Affect Crawling and Indexing, artigo de Nayara Martins, desenvolvedora de sistemas sob medida

JavaScript SEO: How Rendering Problems Affect Crawling and Indexing

In short:
  • A React single-page application can serve the same HTML for every route and still get indexed, because Google renders JavaScript. The problem is not indexing itself, it is that rendering happens in a separate, queued pass after crawling.
  • Anything that consumes HTML without executing JavaScript, such as link previews in WhatsApp, Meta and LinkedIn, will show the generic title and description of the shell page, not the page the user shared.
  • The fix is to emit one static HTML file per route at build time with the correct title, description and canonical, and keep React for interaction. The same site had 4,314 statically generated city pages that never had this problem.

The symptom: every route returns the same HTML

The site we audited was a Vite + React single-page application for a structured credit boutique. Six landing pages lived on path routes: /partners, /sell-your-quota, /simulator and three others. Each one was a real page with its own content, its own intent and its own audience.

The first sign was not in Search Console. It was in a message. Someone shared one of the landing pages in a WhatsApp group and the preview card showed the company name and the generic home description, not the text of the page. The same happened on LinkedIn and in Meta previews. The link worked, the page looked correct in a browser, but the preview was wrong.

The second sign was slower and harder to notice: those six routes took longer to appear in the index than the rest of the site. Nothing was blocked, nothing returned a 404, nothing was marked noindex. They simply entered the index later than pages of comparable importance.

Both symptoms have the same root cause. The server returns one HTML document for every path, and the correct metadata only exists after JavaScript runs.

How to diagnose it

Start with the raw HTTP response, not with the browser. The browser runs JavaScript, so it hides the problem. curl does not.

curl -s https://example.com/partners | grep -i "<title>"
curl -s https://example.com/sell-your-quota | grep -i "<title>"
curl -s https://example.com/simulator | grep -i "<title>"
curl -s https://example.com/ | grep -i "<title>"

On the site we audited, all four commands returned the same line:

<title>Company | Structured Credit Boutique</title>

Check the description and the canonical the same way:

curl -s https://example.com/partners | grep -i 'name="description"'
curl -s https://example.com/partners | grep -i 'rel="canonical"'

If the canonical is absent from the raw HTML, that is the confirmation. A canonical injected by JavaScript is a canonical that non-rendering consumers never see.

The second tool is Search Console. Use URL Inspection on the route and open the "View crawled page" or "View rendered HTML" panel. Compare the rendered <title> with the one from curl. If they differ, the page depends on rendering. That is the whole test, and it takes about a minute per URL.

The third check is the rewrite rule. On Vercel, a catch-all rewrite to the SPA entry point looks like this in vercel.json:

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

That rule is correct for a client-side router and wrong for anything that needs per-route HTML. It is the mechanism that makes every path return the home document.

What the data showed

Measured on 19/09/2026 on a single client site. A Vite + React single-page application with 6 landing pages on path routes. curl -s against each route returned the same HTML as the home page: the same <title>Company | Structured Credit Boutique</title> and the same home meta description, because the platform rewrites /(.*) to /index.html.

Each component set document.title and appended a <link rel="canonical"> inside a React useEffect. So the correct title existed only after JavaScript executed. Before execution, the document had the wrong title and no canonical at all.

Google renders JavaScript, so the six pages did get indexed. The measurable effects were narrower than "the pages are not indexed", and that distinction matters because it changes the fix. Two effects were observed:

  • Link previews on WhatsApp, Meta and LinkedIn showed the generic home title, because those crawlers do not run JavaScript.
  • Indexing was slower for those routes, because rendering is queued separately from crawling. The page has to be crawled, then queued for rendering, then the rendered output is used for indexing decisions.

The same site had 4,314 static HTML pages, city pages generated as real files at build time. Those pages never had the problem. Same domain, same deployment, same crawler. The only difference was whether the HTML arrived complete from the server or was assembled in the browser.

Comparison by page type

Page type HTML served Title in raw HTML Canonical in raw HTML Link previews Indexing
Home page Static shell document Correct Present Correct Normal
/partners Same shell document Home title Absent Generic home title Slower, queued for rendering
/sell-your-quota Same shell document Home title Absent Generic home title Slower, queued for rendering
/simulator Same shell document Home title Absent Generic home title Slower, queued for rendering
City page, one of 4,314 Real file on disk Correct Present Correct Normal

The fix: emit real HTML per route at build time

The goal is simple to state. Every route that needs to be understood without JavaScript should have its own HTML file on disk, containing the correct title, description and canonical, plus enough content to be meaningful. React stays for interaction.

For a Vite build, the standard path is server-side rendering or static site generation of the route list. With Vite, vite-plugin-ssr or the newer vite-plugin-ssr successor handles this, and Next.js, Nuxt and SvelteKit have it built in. The important part is not the tool, it is that the build produces one file per route.

If a full SSR migration is too large a change, a build-time prerender step is enough. After vite build, render each route to HTML and write it to dist/<route>/index.html:

// scripts/prerender.mjs
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { renderToString } from 'react-dom/server';

const routes = [
  { path: '/partners', title: 'Partners | Company', description: '...' },
  { path: '/sell-your-quota', title: 'Sell Your Quota | Company', description: '...' },
  { path: '/simulator', title: 'Simulator | Company', description: '...' }
];

const shell = await readFile('dist/index.html', 'utf8');

for (const route of routes) {
  const html = shell
    .replace(/<title>.*?<\/title>/, `<title>${route.title}</title>`)
    .replace(
      /<meta name="description" content=".*?">/,
      `<meta name="description" content="${route.description}">`
    )
    .replace(
      '</head>',
      `<link rel="canonical" href="https://example.com${route.path}"></head>`
    );

  await mkdir(`dist${route.path}`, { recursive: true });
  await writeFile(`dist${route.path}/index.html`, html);
}

Then change the rewrite so it does not swallow real files. On Vercel, files that exist on disk are served before the rewrite applies, so writing dist/partners/index.html is enough for that path to stop falling through to the SPA shell. The catch-all stays for genuinely client-side routes.

Two details that are easy to get wrong:

  • Remove the document.title and canonical injection from the useEffect once the static HTML carries them. Duplicate canonicals are worse than a missing one, because the crawler has to pick.
  • Keep the same title string in the build config and in the component, or the client-side navigation will overwrite the correct title with a different one after hydration.

How to verify the fix

Verification is a before and after comparison on the same URLs, using the same command.

Before: curl -s https://example.com/partners | grep -i "<title>" returns the home title. After: it returns the partners title. That single check is the core of it.

Then check the canonical is present in the raw response, not only after rendering:

curl -s https://example.com/partners | grep -i 'rel="canonical"'

Then re-run URL Inspection in Search Console on each of the six routes and compare the rendered HTML with the curl output. Before the fix the two differed. After the fix they should match, because the server already sends what the renderer would have produced.

For previews, use the platform debuggers. Meta has a sharing debugger, LinkedIn has a Post Inspector. Both fetch the URL without executing JavaScript, so they are a direct test of the fix. Paste the route, refresh the cache, and confirm the preview shows the page title rather than the home title. This is the fastest visible confirmation and it does not depend on Search Console's reporting lag.

What to measure over time: the gap between crawl date and first appearance in the index for those six routes, compared with the statically generated city pages on the same domain. If that gap closes, the fix worked. Do not expect a ranking change from this alone. The change is about what non-rendering consumers see and about removing a queue step from indexing, nothing more.

Frequently asked questions

If Google renders JavaScript, why does any of this matter?

Because rendering is a second step, not part of crawling. The crawler fetches the HTML, and the page is queued for rendering separately. Indexing decisions that depend on the rendered output wait for that queue. Google does render, but other consumers of your HTML do not: link preview crawlers on WhatsApp, Meta and LinkedIn, most social scrapers, and a number of third-party tools. A page whose title only exists after JavaScript is a page that half the web sees incorrectly.

Does a canonical injected by JavaScript work?

It works for Google, which renders the page and then reads the canonical from the rendered DOM. It does not work for anything that reads the raw HTML. The practical risk is not that Google ignores it, it is that the raw HTML has no canonical at all, so any non-rendering consumer that decides to index or aggregate the URL has no signal about which version is preferred. Emitting it in the served HTML removes that ambiguity at no cost.

Can I keep the single-page application and just fix the metadata?

Yes, if you emit static HTML per route at build time. The React app stays exactly as it is for interaction and client-side navigation. What changes is that the build produces one file per route with the correct title, description and canonical already in the markup, instead of one shell file that every path rewrites to. On the site we audited, the 4,314 statically generated pages already worked this way, which is why they were unaffected.

How do I know which of my routes have this problem?

Run curl -s on each route and grep for the title, then compare with the rendered HTML in Search Console URL Inspection. If the two differ, the route depends on rendering. You can do this across a whole sitemap quickly by looping over the URL list and printing the title for each one, then looking for repeated values. Repeated titles across distinct URLs is the signature of a catch-all rewrite to a single-page application shell. For a deeper walkthrough of the diagnostic process, see the technical SEO reference.

Frequently asked questions

Because rendering is a second step, not part of crawling. The crawler fetches the HTML, and the page is queued for rendering separately. Indexing decisions that depend on the rendered output wait for that queue. Google does render, but other consumers of your HTML do not: link preview crawlers on WhatsApp, Meta and LinkedIn, most social scrapers, and a number of third-party tools. A page whose title only exists after JavaScript is a page that half the web sees incorrectly.

It works for Google, which renders the page and then reads the canonical from the rendered DOM. It does not work for anything that reads the raw HTML. The practical risk is not that Google ignores it, it is that the raw HTML has no canonical at all, so any non-rendering consumer that decides to index or aggregate the URL has no signal about which version is preferred. Emitting it in the served HTML removes that ambiguity at no cost.

Yes, if you emit static HTML per route at build time. The React app stays exactly as it is for interaction and client-side navigation. What changes is that the build produces one file per route with the correct title, description and canonical already in the markup, instead of one shell file that every path rewrites to. On the site we audited, the 4,314 statically generated pages already worked this way, which is why they were unaffected.

Run curl -s on each route and grep for the title, then compare with the rendered HTML in Search Console URL Inspection. If the two differ, the route depends on rendering. You can do this across a whole sitemap quickly by looping over the URL list and printing the title for each one, then looking for repeated values. Repeated titles across distinct URLs is the signature of a catch-all rewrite to a single-page application shell. For a deeper walkthrough of the diagnostic process, see the technical SEO reference.

Need this built for how your company actually works?

No invented deadline, no empty promise. We talk and you hear what is possible.

Start the conversation