Overview
Ship the meaningful content, links, and canonical in the initial HTML response. Googlebot renders JavaScript in a headless Chromium, but only after a queue step, and other crawlers render less or nothing. A Vercel and MERJ study (December 2024) found that the OpenAI, Anthropic, Meta, ByteDance, and Perplexity crawlers did not execute JavaScript, while Gemini (through Googlebot) and Applebot did, so client-only content can be invisible to most AI search.
Pick the rendering mode that matches the page’s job
- Static (SSG) for content that changes on deploy: Astro, Next.js
output: "export", Quartz, Hugo. - Server-side rendering (SSR) for personalized or frequently updated content: Next.js App Router, Remix, Astro
output: "server", Nuxt. - Client-side rendering (CSR) only for authenticated, interactive surfaces that do not need to rank.
- Dynamic rendering (pre-rendered HTML for bots, client HTML for users): Google calls it a workaround, not a long-term solution. Avoid it in new builds. It is acceptable only while bots and users get the same content; Bing treats serving materially different content to crawlers as cloaking.
Know what Google does between crawl and index
Googlebot crawls the HTML, queues the page for rendering, then indexes the rendered output. Google says a page may wait a few seconds in the queue “but it can take longer,” and gives no upper bound.
- Before rendering, Google has only the initial HTML. An empty shell has nothing to index until rendering succeeds.
- Pages with a non-200 status may skip rendering entirely.
- If the raw HTML contains
noindex, Google may skip rendering, so JavaScript that removes the tag cannot be relied on. Do not shipnoindexin the raw HTML of a page you want indexed. - Do not use JavaScript to change the canonical to a different URL than the one in the original HTML. Set it in the HTML.
- Blocked, failing, or timed-out resources produce incomplete rendered HTML. Never block
.jsor.cssinrobots.txt. - Googlebot reads only the first 2 MB of an HTML file and of each CSS or JavaScript file it fetches (64 MB for PDFs). Keep large inline JSON state below the content and links.
og:image,og:title, andog:descriptionmust be in the initial HTML for social and AI previewers that do not run JavaScript.
Fix the SPA patterns that hide content from crawlers
- Hash routes (
/#/article/1) are not crawlable. Use the History API and have the server return correct HTML for every path. - Google discovers links only in
<a>elements with anhref.<a onclick>and button-driven navigation are not links; intercept the click in JavaScript on a real<a href>. - Content that loads only on a click, tab, or “load more” is missed, because Google does not click or type. Put it in the HTML and hide it with CSS if needed.
- Do not lazy-load the LCP image. Use
fetchpriority="high"on it and lazy-load only below the fold. - Soft 404s in SPAs: when a route has no content, redirect to a URL that returns a real 404 or add
noindex.
Verify what Googlebot receives
Run both checks on every page template before launch.
- Search Console URL Inspection, “Test live URL”: shows the rendered HTML and a screenshot. A gap between it and your browser’s DOM is the bug, and it surfaces blocked resources and render timeouts.
view-source:versus DevTools: view-source is the initial HTML; DevTools is the post-JavaScript DOM. A paragraph present only in DevTools is client-rendered.
curl -s https://example.com/page | grep -c "<main>"
# Zero means the main content is not in the initial HTML.Cap hydration cost
Hydration delay stacks up input latency and can hold back LCP. Split bundles per route, use islands or server components (Astro islands, React Server Components in Next.js), and make the LCP element render from static HTML and CSS alone. The fix list is in optimize-core-web-vitals.