In short
Google crawls a JavaScript page, puts it in a render queue, renders it in headless Chromium and indexes the rendered HTML. Anything that is missing from that rendered HTML, such as content behind a click, links without <a href> or data from blocked scripts, cannot be indexed. Server-side rendering or static generation removes most of the risk, and the URL Inspection tool shows you exactly what Google got.
How Google processes JavaScript
Google's documentation starts with the pipeline: “Google processes JavaScript web apps in three main phases:” crawling, rendering and indexing. A plain HTML page goes through the same steps. The difference is how much of the page exists before rendering.
Crawling. Googlebot takes a URL from the crawl queue and first checks robots.txt. “If it marks the URL as disallowed, then Googlebot skips making an HTTP request to this URL and skips the URL.” If the URL is allowed, Googlebot fetches the HTML. “Googlebot then parses the response for other URLs in the href attribute of HTML links and adds the URLs to the crawl queue.”
Rendering. “All pages with a 200 HTTP status code are sent to the rendering queue, no matter whether JavaScript is present on the page.” Rendering is not instant: “The page may stay on this queue for a few seconds, but it can take longer than that.” Then “a headless Chromium renders the page and executes the JavaScript.” Links that appear only after rendering are found at this stage and also go to the crawl queue.
Indexing. “Google also uses the rendered HTML to index the page.” This is the key sentence for JavaScript SEO. Google indexes what the page looks like after your scripts have run, not what you see in your own browser after you click around.
Two practical consequences follow. First, the raw HTML still matters: robots.txt, the status code, a noindex and the links in it are acted on before any JavaScript runs. Second, anything that needs the render step to exist waits for the render queue, so it reaches the index later than content that is already in the HTML.
What Google officially says
“Google also uses the rendered HTML to index the page.”
Google Search Central — Understand the JavaScript SEO basics“If the content isn't visible in the rendered HTML, Google won't be able to index it.”
Google Search Central — Understand the JavaScript SEO basics“Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.”
Google Search Central — Dynamic rendering as a workaroundOther statements from the same pages that this lesson relies on:
- “Googlebot queues pages for both crawling and rendering.”
- “Google Search won't render JavaScript from blocked files or on blocked pages.”
- “If you do want the page indexed, don't use a noindex tag in the original page code.”
- “Expect Googlebot to decline user permission requests.”
- “We recommend using the History API to load different content based on the URL in a SPA.”
- “Client-side analytics may not provide a full or accurate representation of Googlebot and WRS activity on your site.”
WRS is the Web Rendering Service, the part of Google that renders pages.
CSR, SSR, SSG and hydration compared
The rendering strategy decides what is in the very first HTML response, and that decides how much depends on Google's render step. Google says that “server-side or pre-rendering is still a great idea”, and its dynamic rendering page recommends server-side rendering, static rendering or hydration.
| Strategy | First HTML response | Content is ready | Search risk | Typical use |
|---|---|---|---|---|
| Client-side rendering (CSR) | An almost empty shell: <div id="app"></div> plus a script bundle | Only after the JavaScript runs and its API calls succeed | Highest: everything depends on rendering | Dashboards, apps behind a login |
| Server-side rendering (SSR) | Full HTML with text, links and meta tags, built per request | Immediately | Low | Shops, news, pages with frequently changing data |
| Static generation (SSG) | Full HTML built at deploy time | Immediately | Lowest, as long as rebuilds keep content current | Blogs, documentation, marketing pages |
| Hydration | Full HTML from SSR or SSG; JavaScript then makes it interactive | Immediately; interactivity follows | Low, if the hydrated page shows the same content | Modern frameworks in SSR or SSG mode |
| Dynamic rendering | Prerendered HTML for crawlers, CSR for users | Immediately for crawlers | A workaround with extra moving parts | Legacy CSR sites that cannot move to SSR yet |
Client-side rendering
With CSR the server sends a shell and a script bundle; the browser builds the page. Google can index CSR pages, but every failure point is yours: a script error, a slow or blocked API, a resource disallowed in robots.txt. Any of them leaves Google with an empty or partial page.
Server-side rendering and static generation
With SSR the server runs the framework and returns complete HTML for each request. With SSG the HTML is generated once at build time and served as files. In both cases the text, links, title, canonical and robots meta tag are in the first response, so they do not wait for the render queue and other crawlers can read them too.
Hydration
Hydration is not a separate rendering mode but the step after SSR or SSG: the browser attaches JavaScript to the HTML that is already there. Keep the server output and the hydrated page the same. If the client replaces content, titles or canonicals after hydration, you are back to depending on the render step.
Dynamic rendering
Dynamic rendering detects crawlers and serves them a prerendered version while users get the client-side app. Google is clear about its status: “Dynamic rendering is a workaround and not a recommended solution, because it creates additional complexities”. It also warns: “Using dynamic rendering to serve completely different content to users and crawlers can be considered cloaking.” Treat it as a temporary bridge, not as an architecture.
Links Google can follow
Google's rule for links is short: “Generally, Google can only crawl your link if it's an <a> HTML element”, and “Google can't reliably extract URLs from <a> elements that don't have an href attribute”. A <span> or <div> with a click handler is not a link to Google, however much it behaves like one for users.
✕ Not reliably crawlable
<a routerLink="products/category">Shoes</a>
<span href="https://example.com">Home</span>
<a onclick="goto('https://example.com')">
Home</a>
<div @click="go('/sale')">Sale</div>
- No
href, orhrefon the wrong element - The URL exists only inside JavaScript
- Fine for users, invisible as a link to crawlers
✓ Crawlable
<a href="https://example.com">Home</a>
<a href="/products/category/shoes">
Shoes</a>
<!-- a click handler is fine next to href -->
<a href="/products/category/shoes"
onclick="javascript:goTo('shoes')">
Shoes</a>
- A real
<a>element - An
hrefthat resolves to a real URL - Anchor text between the tags
Inserting links with JavaScript is allowed: “Links are also crawlable when you use JavaScript to insert them into a page dynamically”, as long as they are real <a href> elements. They are only found after rendering, though. Google adds: “If you are using JavaScript to insert anchor text, use the URL Inspection Tool to make sure it's present in the rendered HTML.”
In practice, most frameworks have a link component (router link, Link, NuxtLink and similar) that outputs a normal <a href> in the final HTML. Check the rendered output, not the component code. Navigation, pagination, faceted filters and “load more” buttons are the places where real links most often go missing.
URLs in single-page apps: fragments and the History API
“In client-side rendered single-page apps, routing is often implemented as client-side routing.” How that routing builds URLs decides whether Google sees one page or many.
Hash routes such as https://example.com/#/products put the route in the URL fragment. The fragment is never sent to the server, and for Google the whole app is effectively one URL. Google says: “To ensure that Googlebot can parse and extract your URLs, don't use fragments to load different page content.” The old workaround is gone too: “The AJAX-crawling scheme has been deprecated since 2015, so you can't rely on URL fragments to work with Googlebot.”
<!-- Avoid: every view lives behind # -->
<a href="#/products/shoes">Shoes</a>
<!-- Use: a real path, handled with the History API -->
<a href="/products/shoes">Shoes</a>
<script>
// intercept the click, update the view, keep a real URL
history.pushState({}, '', '/products/shoes');
</script>
Google's recommendation is the History API: “We recommend using the History API to load different content based on the URL in a SPA.” Two rules make it work for search. Each route must also work when it is opened directly, so the server has to answer /products/shoes with the right page (ideally server-rendered), not just the home page. And fragments are still fine for what they were made for: jumping to a section within one page, as in /guide/#install.
Title, meta description, canonical and robots set by JavaScript
Many apps change the <head> on every route. Google handles this, but each tag has its own rules.
Title and meta description
“You can use JavaScript to set or change the meta description as well as the <title> element.” Google will use the rendered values. Still, put a correct title and description in the server HTML when you can: they are then available before rendering and to crawlers that never run JavaScript. Every route should get its own title, not the app's default one.
Canonical
Google's advice: “The best way to set the canonical URL is to use HTML”. A canonical injected by JavaScript is accepted: “Google Search will pick up the injected canonical URL when rendering the page.” The danger is two different values, one in the server HTML and another after rendering. “Conflicting or multiple rel="canonical" link tags may lead to unexpected results.” If you must set it with JavaScript, make sure the server HTML has no canonical at all, or the same one.
Robots meta tag and the noindex trap
“You can use JavaScript to add a robots meta tag to a page or change its content.” Adding a noindex with JavaScript works. Removing one does not work reliably, because of this rule: “When Google encounters the noindex tag, it may skip rendering and JavaScript execution”. If the server HTML says noindex, your script that would remove it may never run.
✓ Works
- Server HTML has no
noindex; the page is indexable by default - JavaScript adds
noindexonly to views that should stay out, such as error states - Pages you want indexed never carry
noindexin the original code
✕ Trap
- Server HTML ships
noindexas a “safe default” - JavaScript is supposed to remove it once data loads
- Google may skip rendering, sees only
noindex, and the page stays out of the index
Status codes and soft 404s in SPAs
Status codes are read before rendering, so they have to be right on the server: “To tell Googlebot if a page can't be crawled or indexed, use a meaningful status code”. A client-side app often answers every route with 200 and the same shell, then shows “Product not found” in JavaScript. To Google that is a normal page with error text, a soft 404. Google warns: “In a single-page application (SPA), this can be especially difficult.” and “This can lead to error pages being indexed and possibly shown in search results.”
When the server cannot return a real 404, Google gives two strategies:
- “Use a JavaScript redirect to a URL for which the server responds with a 404 HTTP status code (for example /not-found).”
- “Add a <meta name="robots" content="noindex"> to error pages using JavaScript.”
fetch('/api/products/' + id)
.then(r => {
if (r.status === 404) {
// option 1: go to a URL that really returns 404
window.location.href = '/not-found';
// option 2: keep the URL, keep it out of the index
// const m = document.createElement('meta');
// m.name = 'robots'; m.content = 'noindex';
// document.head.appendChild(m);
}
});
The best fix is on the server: with SSR, a missing product returns 404 (or 410) directly, and a moved one returns a 301 redirect instead of a JavaScript redirect.
Content Googlebot may not see
Rendering is not the same as a person using the page. These are the usual reasons content is missing from the rendered HTML.
Content behind interaction
Text loaded only after a click on a tab, a “show more” button or a scroll event may never be loaded during rendering. Do not assume Googlebot clicks or scrolls like a user. Put content in the HTML and hide or show it with CSS, or load it on page load.
Lazy-loading done wrong
Google recommends lazy-loading for images: “A good strategy is to use lazy-loading to only load images when the user is about to see them.” Use native loading="lazy" or IntersectionObserver, and do not lazy-load the main content or the first screen of a page on scroll events.
Permission prompts
“Expect Googlebot to decline user permission requests.” Content that appears only after the visitor allows location, notifications or camera access is not rendered for Google.
Stored state
“Don't rely on data persistence to serve content.” Each page load starts clean: “Local Storage and Session Storage data are cleared across page loads.” and “HTTP Cookies are cleared across page loads.” A page that needs a stored region, preference or session to show its content shows Google the empty version.
Non-HTTP connections
“Googlebot uses HTTP requests to retrieve content from your server.” “It does not support other types of connections, such as WebSockets or WebRTC connections.” Provide an HTTP way to fetch the content that matters for search.
Shadow DOM
“Google supports web components.” “When Google renders a page, it flattens the shadow DOM and light DOM content.” What counts is whether the content ends up visible in the rendered HTML.
Stale cached scripts
“Googlebot caches aggressively in order to reduce network requests and resource usage.” and “WRS may ignore caching headers.” If you deploy a new bundle under the same filename, Google may render with the old one. Use content fingerprinting in filenames, such as main.2f9c1a.js: “The fingerprint depends on the content of the file, so updates generate a different filename every time.”
Blocked resources
“Google Search won't render JavaScript from blocked files or on blocked pages.” A robots.txt rule written years ago to “save crawl budget” can now break rendering for the whole site, because the page itself is allowed but the bundle or the API that fills it is not.
# Breaks rendering: the page needs these files
User-agent: *
Disallow: /static/
Disallow: /api/
# Better: block only what really should not be crawled
User-agent: *
Disallow: /admin/
Disallow: /cart/
Check every script, stylesheet and API endpoint a template needs. Third-party scripts on another host follow that host's robots.txt, which you cannot change, so do not load critical content from them. The URL Inspection tool lists page resources that could not be loaded.
Other crawlers and AI bots
Everything above describes Google. Other crawlers vary, and in practice many of them do not execute JavaScript at all. That includes many AI crawlers, SEO tools and the bots that build link previews for messengers and social networks. For them, a client-side rendered page is the empty shell: no text, no links, sometimes not even a title.
If you want your content to be read by AI assistants, previewed correctly when shared, or analysed by crawling tools, the safest choice is the same one Google recommends: put the content, links and meta tags in the server HTML with SSR or SSG. Check what a non-rendering crawler gets by looking at the raw HTML (view source, or a request with curl), not the page in your browser.
How to test what Google sees
“To test how Google crawls and renders a URL, use the Rich Results Test or the URL Inspection Tool in Search Console.” Both tools render the page the way Google does. “You can see loaded resources, JavaScript console output and exceptions, rendered DOM, and more information.” The URL Inspection tool works for verified properties and shows the indexed version; its live test checks the current page. The Rich Results Test works for any public URL.
- Look at the raw HTML. View source or fetch the URL with
curl. Is the main text there? The title, canonical, robots meta tag and links? - Run a live test in URL Inspection and open the rendered HTML. Search it for a unique sentence from the page.
- Read the extra information: resources that failed to load, console errors, the HTTP status.
- Compare raw and rendered title, canonical and robots tags. They should match, or the raw HTML should have none of them.
- Check the index: the indexed status in URL Inspection and the Page indexing report for soft 404s, “Excluded by ‘noindex’ tag” and similar statuses.
- Do not use client-side analytics as evidence of Google's visits. Use the server logs and the Crawl stats report.
Check how your JavaScript site is crawled
OurSEO Tools runs a 69-point checklist that includes rendering and speed, plus a technical crawl of up to 1,000 pages with the status code, canonical, indexability and load time of every URL.
Common mistakes
- ✕Links without
hrefButtons, spans and divs with click handlers instead of<a href>in menus, filters and pagination. - ✕Hash routing
/#/productsturns the whole app into one URL for Google. - ✕
noindexin the server HTML, removed by JavaScriptGoogle may skip rendering and never see the removal. - ✕Two canonicalsOne in the server HTML, a different one injected after rendering.
- ✕200 for every routeMissing pages show an error message but return 200 and become soft 404s.
- ✕Scripts or APIs blocked in robots.txtThe page is allowed, but it cannot be rendered.
- ✕Content behind clicks, permissions or stored stateTabs loaded on click, location prompts, content that needs localStorage or a cookie.
- ✕Same title on every routeThe app's default title is never replaced per view.
- ✕Dynamic rendering as the long-term planGoogle calls it a workaround; serving different content to bots can be cloaking.
- ✕Testing in your own browserYour browser has cookies, permissions and a warm cache. Check raw HTML and URL Inspection instead.
FAQ
Can Google index JavaScript content?
Yes. Google renders pages in headless Chromium and indexes the rendered HTML. The condition is that the content actually appears in that rendered HTML without clicks, permissions or blocked resources.
Is client-side rendering bad for SEO?
Not automatically, but it is the riskiest option. Everything depends on rendering succeeding, content waits for the render queue, and crawlers that do not run JavaScript see an empty page. For pages that must rank, SSR or SSG is the safer choice.
How long does rendering take?
Google does not give a fixed time: “The page may stay on this queue for a few seconds, but it can take longer than that.” Content that is already in the server HTML does not have to wait for it.
Should I use dynamic rendering?
Only as a temporary bridge for a client-side app that cannot move to SSR yet. Google calls it a workaround and recommends server-side rendering, static rendering or hydration instead.
Are links added with JavaScript followed?
Yes, if they are real <a> elements with an href and appear in the rendered HTML. They are found later than links in the server HTML, and crawlers that do not render never see them.
How do I see what Google rendered?
Run the URL Inspection tool in Search Console (or the Rich Results Test for any public URL) and open the rendered HTML, the loaded resources and the console messages. Compare it with the raw HTML from view source.
Sources
- Google Search Central — Understand the JavaScript SEO basics
- Google Search Central — Fix Search-related JavaScript problems
- Google Search Central — Dynamic rendering as a workaround
- Google Search Central — Make your links crawlable
Quotes checked against the official pages on 30 October 2026.