Dynamic Title Set by JavaScript Not Indexed by Google

Your SPA updates `document.title` after the first render, but Googlebot indexes the original placeholder. The SERP shows "Loading..." or your home title on every URL.

You ship a React / Vue / SPA application. Each route sets the page title via document.title = "..." or a <Helmet> component after the route mounts. In the browser the title bar updates correctly. On Google, every URL in the SERP shows the same generic title — “MyApp” or “Loading…” or whatever the server returned in the initial HTML. The crawler never executed the title-setting code, or did but indexed the pre-update HTML snapshot. The fix is to ship the correct title in the server-rendered HTML or to use a hybrid rendering strategy.

Common causes

Ordered by hit rate on real SPAs.

1. Server returns the same <title> shell for every route

The initial HTML on every URL contains <title>MyApp</title>. JS replaces it after mount. Google indexes the shell HTML and never gets the per-route title.

How to spot it: curl https://example.com/any/route and every URL returns the same <title>.

2. Initial title is “Loading…” or empty

The shell is <title>Loading...</title> to signal hydration is pending. Google takes that literally and ships “Loading…” into the SERP.

How to spot it: Search site:yourdomain.com in Google. Many results have the same “Loading…” title.

3. Title set by client-only state hook

In React, useEffect(() => { document.title = data.title }, [data]) only runs in the browser. Server rendering bypasses the effect.

How to spot it: Title is correct in browser DevTools after hydration; missing in view-source and curl.

4. SSR pipeline returns a partial render before the title hook fires

A streaming SSR setup sends the <head> to the wire before the data fetch completes. Title is missing from the flushed HTML.

How to spot it: HTML response shows <title></title> or no title at all in the head; browser shows correct title post-fetch.

5. Title set after a redirect

URL /old-path redirects via JS (not HTTP 301) to /new-path. Google crawls /old-path, gets the pre-redirect shell title, indexes that.

How to spot it: SERP shows the pre-redirect URL with a generic title; HTTP 200 instead of 301 on the redirect path.

6. Crawler renders, but title hook depends on cookies or auth

The title fetch requires a logged-in API call. Googlebot is anonymous; the API returns 401; the title state never updates.

How to spot it: Network panel of crawler-simulator (or curl with no cookies) shows 401 on the title-data endpoint.

Before you start

  • Confirm the symptom with curl -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://example.com/some-route and inspect <title>.
  • Decide your rendering strategy: full SSR, SSG, or hybrid? Each unblocks a different fix path.
  • Check whether your framework already supports server-side metadata (Next.js metadata, Nuxt useHead, SvelteKit <svelte:head>).
  • Note how many distinct route templates need fixing. Often one shared layout drives all.

Information to collect

  • Server-rendered HTML for 5-10 representative URLs.
  • Browser DOM after hydration for the same URLs (to confirm the client-side title is correct).
  • Whether the framework supports SSR / SSG and whether you have it enabled.
  • The data source for titles (frontmatter, API call, route param).
  • Any redirects between the SERP-indexed URL and the canonical URL.

Step-by-step fix

Ordered by impact and cost.

Step 1: Verify what Googlebot actually sees

curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1)" \
  https://example.com/products/widget \
  | grep -oE '<title>[^<]*</title>'

Or use Search Console → URL Inspection → View crawled page to see the rendered HTML Google uses for indexing.

Step 2: Move title generation server-side

In Next.js App Router:

// app/products/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const product = await getProduct(params.slug);
  return {
    title: `${product.name} | Acme Store`,
    description: product.summary,
  };
}

In Nuxt 3:

useHead({ title: () => `${product.value.name} | Acme Store` });

In SvelteKit:

<svelte:head>
  <title>{product.name} | Acme Store</title>
</svelte:head>

These run during SSR and emit the correct title in the response HTML.

Step 3: Prerender static routes when SSR is overkill

For content that does not change per request, pre-generate static HTML:

// next.config / nuxt.config / astro.config
export default { output: 'static' };

Static prerender embeds the title in HTML at build time. No runtime cost, fully crawler-safe.

Step 4: For pure SPA frameworks, add a build-time prerender step

Tools like prerender-spa-plugin (Vue / Webpack) or react-snap crawl your dev server and write static HTML for each route. The resulting HTML contains the correct title.

npm install --save-dev react-snap
# add postbuild hook
"scripts": {
  "build": "vite build && react-snap"
}

Step 5: Replace JS redirects with HTTP 301s

If /old-path should redirect, use server-level redirects (Netlify _redirects, Cloudflare rules, Express middleware):

/old-path  /new-path  301

The crawler follows the 301 directly and indexes /new-path with its correct title.

Step 6: Make title-data accessible without auth

If the title text depends on data behind auth, ship a public version for SSR:

const title = isAuthed
  ? `${user.name}'s Dashboard | Acme`
  : `Sign In to Acme`;

Or shift the auth-gated text to the body, keeping the title public.

Step 7: Request reindexing on fixed URLs

In Search Console → URL Inspection → Request indexing for the highest-traffic affected URLs. SERP titles typically update within 1-2 weeks after a successful recrawl.

Verify

  • curl of any route returns the route-specific title in the initial HTML.
  • Search Console → URL Inspection shows the correct title in the rendered HTML snapshot.
  • A site:yourdomain.com search shows distinct, route-specific titles for each result.
  • Click-through rate recovers as relevant titles appear in SERP.
  • Your framework’s metadata APIs run during SSR and the output is consistent across deploys.

Long-term prevention

  • Default to server-side metadata APIs (generateMetadata, useHead, <svelte:head>) for every route from day one.
  • Forbid document.title = "..." patterns in code review for production routes.
  • Add a CI assertion that curl of each route returns a unique, non-placeholder <title>.
  • Use Lighthouse or Search Console’s URL Inspection in CI to catch missing titles on new routes.
  • Document the title-source pattern in your component library so new contributors follow it automatically.

Common pitfalls

  • Assuming “Googlebot renders JavaScript so it should be fine.” It does, but the rendering tier runs on a delay and not for every URL. Server-rendered metadata is far more reliable.
  • Setting a default title at the framework level (“MyApp”) and forgetting it overrides per-route titles in some routing scenarios.
  • Relying on <Helmet> in React Router SPA without SSR. <Helmet> only updates document.title in the browser.
  • Using nextRouter.events.on('routeChangeComplete', updateTitle) — this also runs only in the browser.
  • Caching the title-data endpoint with auth so crawlers get a 401 but real users get 200. Make title fetches public-friendly.

FAQ

Q: Does Googlebot run JavaScript at all?

Yes — for most URLs eventually. But the initial indexing pass uses raw HTML; JS-only titles surface on the second pass, which can take days or weeks. Server-rendered titles index immediately.

Q: My framework is SPA-only. Do I need to switch to Next.js or Nuxt?

Not necessarily. Use a prerender step (react-snap, vite-plugin-ssr, prerender-spa-plugin) to write static HTML at build time. That fixes the title indexing problem without a framework migration.

Q: How long after fixing the title will SERPs update?

Typically 1-2 weeks. High-traffic URLs recrawl fastest; long-tail can take a month or more. Request indexing on critical pages to accelerate.

Q: Can I keep the “Loading…” shell title for users while serving a real title to Googlebot?

No — cloaking different content to crawlers vs. users violates Google’s guidelines. Ship the real title to everyone. If “Loading…” is a UX requirement, replace it the moment hydration starts, but ensure the SSR-rendered HTML already has the real value.

Tags: #SEO #Troubleshooting #spa #title-tag #rendering