The question on a framework migration is not whether Google can execute JavaScript. It can, and has been able to for years. The question is whether what Google ends up with still contains what your old pages contained - and a rebuild changes enough moving parts that the answer needs testing rather than assuming.

Two labels mark evidence boundaries. Documented means the guidance that follows is directly supported by Google's own published documentation, quoted. Recommendation means a threshold, workflow or pass rule that we prescribe and the documentation does not. Statements from Google staff in interviews or podcasts are attributed in the prose rather than tagged. Untagged text is ordinary explanation, or a conclusion following from something already labelled.

What this page is not

This page covers migration parity and validation only. Which crawlers execute JavaScript and which do not, how the various AI crawlers behave, and how to choose between server-side rendering, static generation, prerendering and hybrid approaches are covered in our AI crawlability pillar. If you are still choosing an architecture, start there. If the architecture is chosen and you need to know whether the rebuild kept what the old site had, you are in the right place.

Step 1: What Actually Changes

Documented Google describes processing JavaScript in three phases - crawling, rendering, indexing. Two properties of that pipeline matter for a migration. Rendering is queued rather than immediate: The page may stay on this queue for a few seconds, but it can take longer than that. And the queue is entered conditionally - pages with a 200 status are queued for rendering, and where the status is not 200, rendering may be skipped entirely.

So a framework migration can fail in three distinct places, and they need separating because they have different fixes: the server response can change, what executes can change, and what the execution produces can change. A page that looks correct in a browser has only told you about the third.

Step 2: The Three-Artifact Comparison

Recommendation For every template that matters, compare three things. Two artifacts is the common mistake - it is the comparison that produces confident wrong answers.

ArtifactWhat it answersHow to get it
1. Raw server responseWhat arrives before any JavaScript executes. What a non-rendering client receives.A direct fetch with automatic redirect-following off, recording each hop's status and Location in order, then a second request to the final URL for the body. Headers and body inspected separately.
2. Browser-rendered DOMWhat your code produces when everything worksBrowser devtools on the built page
3. Google's rendered outputWhat Google actually ends up with - the artifact that answers the Google-rendering questionDocumented Google's instruction: use the Rich Results Test or the URL Inspection Tool and look at the rendered HTML.

Recommendation On artifact 1, do not let the fetching tool follow redirects silently. A client that resolves the chain for you returns the final page's status and body and discards everything in between, which hides the two things a migration most needs to see: how many hops there were, and whether the first hop was the status the map said it would be. Collect the chain, then fetch the final URL. Those are the same values phase 2 records as expected_initial_status, expected_final_status, expected_final_url and expected_max_hops; collecting them here means one dataset answers both questions.

Documented The verdict rests on the third: If the content isn't visible in the rendered HTML, Google won't be able to index it.

Read that sentence in the direction it is written. It makes presence in the rendered HTML a necessary condition, not a sufficient one - content that is missing there cannot be indexed, but content that is present there has not thereby been indexed. Indexing also depends on the indexing directives, on canonical selection, and on Google's own selection decisions, none of which artifact 3 settles. Documented Google says as much about its own strongest indexing status: even URL is on Google in the URL Inspection tool doesn't actually guarantee that your page will appear in Search results. Artifact 3 answers the rendering question. Whether a URL is indexed is answered after launch, by the indexing reports.

Content absent from the raw response is not by itself a failure

This is the misdiagnosis worth guarding against most deliberately, because it routes engineering effort at something that may not be broken. Content that is missing from artifact 1 and present and correct in artifact 3 is an architectural observation, not a defect - Google executes JavaScript, and client-side rendering is not by itself proof that Google could not see the page. Record it as a delivery change and a risk for non-rendering clients, and keep investigating.

Declare a rendering defect when content, crawlable links, canonical values or indexing directives are missing or wrong in artifact 3, or when a resource needed to produce it failed or was blocked.

Full Google-rendered testing is constrained on protected staging

Google's two tools do not carry the same access requirements, and treating them as one tool produces the wrong plan. Documented:

RouteNeeds the URL in a property you ownNeeds anonymous reachabilityNeeds the URL crawlable
URL Inspection, live testYes - The URL must be in the currently opened property.Yes - the page must be accessible from the internet without any login requirements.Not stated as a bar; the live test reports blocked and excluded states
Rich Results Test, URL modeNo. Google names it the non-owner alternative: To test a URL in a property that you don't own, use the appropriate non-owner test, such as the Rich Results test or AMP test.Yes - Any resources that are behind a firewall or password-protected will not be available to the test.Yes - If Google is prevented from crawling the page as part of its regular crawl cycle (for example, is prevented from crawling by a robots.txt rule or noindex directive), the page cannot be tested with this tool.
Rich Results Test, pasted codeNoNoNo

So the constraint is not a single one. Anonymous reachability is what a protected staging environment fails, and it blocks both tools; property membership is an additional requirement that only URL Inspection carries. Documented Google documents the reachability fix itself, for its testing tools generally: If your URL is behind a firewall, or is hosted on a local computer, you can use a tunnelling solution to expose your page to the testing tool. Its debug guidance adds the caveat that some tunneling solutions (not ngrok) automatically protect your temporary public URL with robots.txt, which will prevent you from running Google tests on them.

Two consequences worth planning around. A tunnel on whatever hostname the tunnelling tool assigns is enough for the Rich Results Test and its rendered source code - that route gives you Google-rendered output without owning the hostname. It is not enough for URL Inspection, which still needs the exposed URL to sit inside a property you own. And a staging environment that keeps its noindex or its blanket Disallow in place while tunnelled satisfies reachability and still fails the Rich Results Test's crawlability condition, so the exposure has to be planned as a deliberate, scoped, short-lived exception rather than a port-forward.

Recommendation Choose the route before you need it: pasted code for markup validity only, a tunnel for Google-rendered output via the Rich Results Test, a tunnel on a controlled hostname inside a verified property if you also want URL Inspection's live test results, or defer artifact 3 to immediately after cutover. Staging QA sets out the routes in full; whichever you take, record it at go/no-go rather than implying the check was done. Note that the pasted-code route validates markup you supply - it does not render your page, so it cannot answer any question on this list except a structured-data one.

Step 3: Failure Modes Specific to a Rebuild

Each block: what goes wrong, how to check it, and the condition for passing. Recommendation The pass rules are ours unless a block says otherwise.

1. A noindex that JavaScript was supposed to remove

Documented This is the one to check first, because the failure is silent and the fix is not what teams expect. Google: When Google encounters the noindex tag, it may skip rendering and JavaScript execution, which means using JavaScript to change or remove the robots meta tag from noindex may not work as expected. If you do want the page indexed, don't use a noindex tag in the original page code.

The migration shape of this: a build ships noindex in the initial HTML - inherited from the staging configuration, or set behind a flag production is expected to flip - with client-side logic intended to remove it. Because the tag can cause rendering to be skipped, the logic that would have removed it may never run. The page stays out of the index and the code that was supposed to fix it looks correct in a browser.

Pass rule: no template ships noindex in the raw server response with the intention of removing it client-side. Indexing state is decided server-side. Cross-check against the staging-only control inventory from planning.

2. Content that needs an interaction

Documented Google: Google won't load content that requires user interactions (for example, swiping, clicking, or typing) to load, and Don't lazy-load primary content upon user interaction. Viewport-triggered loading is fine - Load content when it's visible in the viewport - and the recommended methods don't rely on user actions, such as scrolling or clicking, to load content, which is important as Google Search does not interact with your page. On pagination controls: Google's crawlers don't "click" buttons and generally don't trigger JavaScript functions that require user actions to update the current page contents.

Tabs, accordions and "load more" controls arrive in rebuilds as design decisions, and nobody records them as content decisions.

Pass rule: no critical content and no crawlable link requires a click, swipe or keystroke to appear in artifact 3.

3. Client-side routing that produces no crawlable links

Documented Google discovers links through <a> HTML elements with an href attribute, and its instruction for single-page applications with client-side routing is to use the History API to implement routing between different views of your web app. Fragments are named explicitly as the thing to avoid: do not use fragments to load different page content, because Googlebot cannot reliably resolve fragment-based URLs.

A rebuild that moves from server-rendered navigation to a client router can produce a site where every internal destination exists and none of them is discoverable.

Pass rule: every internal destination is reachable via an <a href> to a real path, and no view is addressed only by fragment. Compare the crawl of the new site against the approved inventory from phase 2, not against a crawl of the old site: the expected destination set is the URLs whose disposition is unchanged, plus the targets of every mapped, consolidated and rule-handled row. URLs marked intentionally retired or no equivalent, together with the source side of consolidations, are supposed to be absent from the new crawl - a comparison against the old crawl reports those as regressions and buries the ones that are. Test the old URLs separately against their recorded dispositions: retired and no-equivalent URLs should return the approved 404 or 410 response.

4. Soft 404s from client-side routing

Documented Google acknowledges the constraint directly: in a client-side routed single-page app, using meaningful HTTP status codes can be impossible or impractical. It gives two remedies. Either use a JavaScript redirect to a URL for which the server responds with a 404 HTTP status code (for example /not-found), or add a <meta name="robots" content="noindex"> to error pages using JavaScript.

Note that the second remedy is the mirror image of failure mode 1, and the distinction is directional: adding noindex via JavaScript to a page that should not be indexed is documented; removing it via JavaScript from a page that should be indexed is the thing Google warns may not work.

Pass rule: every not-found state resolves to either a real 404 response or a page carrying noindex. No not-found state returns a bare 200 with a friendly message.

5. Canonicals injected by JavaScript

Documented Google permits it and does not advise it: While we don't recommend using JavaScript for this, it is possible to inject a rel="canonical" link tag with JavaScript. Two conditions follow - the injected tag must be the only canonical tag on the page, and where the canonical also exists in the original HTML the JavaScript must set it identically. Google warns: Incorrect implementations might create multiple rel="canonical" link tag or change an existing rel="canonical" link tag. Conflicting or multiple rel="canonical" link tags may lead to unexpected results.

Check this specifically where the canonical is assembled from the same data layer that renders the page - a headless build that composes both from one API response can produce a second tag without anyone writing one.

Pass rule: scoped to URLs the inventory marks indexable; redirecting URLs, 404s and URLs carrying noindex are out of scope, which is phase 4's applicable-signal standard applied here. For each indexable URL expected to declare a canonical, artifact 3 contains exactly one canonical value; where artifact 1 already contains a canonical, the rendered value matches it. If the page is not an alternate and the approved inventory deliberately expects no canonical, absence is not a failure.

Documented That last clause is scoped, not general. Google's None is fine guidance applies specifically to pages that are not alternates: If your page is not an alternate page, the value None is fine here. If your page is one of a set of alternate pages, we recommend explicitly declaring the canonical URL. So a duplicate or alternate page that arrives at launch having quietly lost its canonical is a failure, not an acceptable absence - which is why the expectation has to come from the inventory rather than from what the page happens to contain.

6. Stale bundles served to the renderer

Documented A caching behavior that bites specifically at cutover: Googlebot caches aggressively in order to reduce network requests and resource usage. Its Web Rendering Service may ignore caching headers, which may lead WRS to use outdated JavaScript or CSS resources. Its recommended remedy is content fingerprinting - a content hash in the filename, so that changed content produces a different filename.

A migration that keeps stable bundle filenames while changing their contents is the exact condition this describes.

Pass rule: build output uses content-fingerprinted filenames for JavaScript and CSS, so no changed resource can be served from a stale cached copy under its old name.

7. Code that assumes state survives between pages

Documented Google is explicit that its renderer does not carry state forward: WRS does not retain state across page loads. Specifically, Local Storage and Session Storage data are cleared across page loads and HTTP Cookies are cleared across page loads. It also says to Expect Googlebot to decline user permission requests, and names WebSockets and WebRTC as unsupported connection types.

If your build stores locale, currency, consent state or a feature-flag payload in client storage on first load and reads it thereafter, that pattern does not survive contact with the renderer: every page is a first load to it.

Pass rule: every template produces complete, correct content on a cold load with no cookies, no storage and no prior session. Test each template as an entry point, not by navigating to it.

8. Gated content - two different cases

These get conflated, and they have opposite requirements. Decide which one a template is before applying a rule to it.

Case A - application content behind authentication, not intended for indexing. The account area, the dashboard, anything behind a login. Documented The gate has to be real rather than cosmetic: Make sure your paywall only provides the full content once the subscription status is confirmed, rather than relying on JavaScript to hide content that was already delivered. A rebuild that ships the full payload to the client and hides it in the view is the pattern to look for, because it is easy to write and it looks correct to a logged-out human. On permission-gated features, Google's advice is to provide a way for users to access your content without being forced to allow camera access - the principle being that content should not depend on a permission Googlebot will decline.

Case B - content behind a paywall or registration that you do want indexed. Here the requirement is not to hide it harder but to declare it. Documented Google's spam policies carve this out explicitly: If you operate a paywall or a content-gating mechanism, we don't consider this to be cloaking if Google can see the full content of what's behind the paywall just like any person who has access to the gated material and if you follow our Flexible Sampling general guidance. Google documents a structured-data pattern for exactly this - an isAccessibleForFree value and a cssSelector identifying the gated section - and states its purpose plainly: This structured data helps Google differentiate paywalled content from the practice of cloaking, which violates spam policies. Two conditions come with it: Only use .class selectors for the cssSelector property, and If you want Google to crawl and index your content, including the paywalled sections, make sure Googlebot, and Googlebot-News if applicable, can access your page.

The migration risk runs in both directions. A rebuild that moves case-B content behind a stricter gate without carrying the markup across turns a documented arrangement into an undeclared one. A rebuild that applies paywall markup broadly - to case-A application content that was never meant to be indexed - declares a gate on pages whose correct treatment is to be excluded server-side.

Pass rule: every gated template is classified A or B in the inventory. For A: the content is not present in artifact 1 or artifact 3 for an unauthenticated request, and no indexable content depends on a granted permission. For B: the gate is enforced server-side; artifact 3 contains the same full gated content available to an authorized user, and the paywalled-content markup accurately identifies the restricted sections; and the arrangement matches what the old site had unless the change was a deliberate decision recorded at planning.

The content condition is the decisive one, and it is the one a rebuild breaks. Markup does not make unavailable content indexable - the carve-out is conditional on Google seeing the full content of what's behind the paywall just like any person who has access to the gated material, and Google's own instruction is that if you want the paywalled sections crawled and indexed you must make sure Googlebot can access your page. A template that ships only the teaser to an unauthenticated request and carries correct isAccessibleForFree markup passes a markup validator and fails this rule.

9. Blocked render-critical resources

Documented Google Search won't render JavaScript from blocked files or on blocked pages. A new robots.txt, a new CDN path, or a new asset host can disallow something the old configuration allowed, and the symptom is a page that renders correctly for you and incompletely for Google.

Pass rule: no resource required to produce artifact 3 is disallowed in the production robots.txt, and artifact 3 shows no failed resource loads.

10. Web components and shadow DOM

Documented Google flattens shadow DOM and light DOM when rendering, so what it indexes is the flattened rendered output. Its instruction is to use slot elements to project light DOM content into the shadow DOM so both appear in the rendered result, and to verify with the Rich Results Test or URL Inspection Tool that the content is visible in the rendered HTML.

Pass rule: for any template using web components, the main content appears in artifact 3.

Step 4: If the Migration Also Changes Rendering Architecture

Architecture choice belongs to the AI crawlability pillar, not here. One piece of Google's current guidance is worth stating in a migration context, because it is where teams reach when a rebuild turns out not to be indexable and launch is close:

Documented On dynamic rendering - serving a prerendered version to crawlers - Google's position is unambiguous: Dynamic rendering is a workaround and not a recommended solution, because it creates additional complexities and resource requirements. And: Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines. What it recommends instead: we recommend that you use server-side rendering, static rendering, or hydration as a solution.

Documented The same preference appears in Google's general JavaScript guidance, with its reasoning: server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript. That last clause is the migration-relevant half - the rendering question is not only about Google.

Step 5: If URLs Are Changing Too

Recommendation A framework migration that also changes URLs compounds two independent sets of failures, and the diagnosis gets harder because either can produce a page that is no longer indexed. Where you can separate them, do - the reasoning is in the pillar checklist. Where you cannot:

  • Validate the redirect map against its recorded dispositions and run this page's comparison, and keep the two result sets separate.
  • Confirm that the canonical values in artifact 3 point at the new URLs, not the old ones. A data layer migrated with its stored paths intact will produce correct-looking pages that self-canonicalize to URLs you have just retired.
  • Check that client-side routing produces the new paths, not paths generated from stale content.

Where This Feeds

  • Staging QA runs this comparison as part of the pre-launch gate, and records which artifacts were unavailable.
  • Launch day completes the artifact-3 checks that could not run against protected staging.
  • Diagnosis routes here when a template group loses traffic while returning 200.

Sources

Sources: Google Search Central, JavaScript SEO basics - the crawl, render and index phases and the render queue; the noindex-skips-rendering warning; the soft-404 remedies for single-page apps; the History API instruction and the caution against fragments; canonical injection; caching and content fingerprinting; web components and shadow DOM; and the server-side or pre-rendering recommendation. Google Search Central, Fix search-related JavaScript problems - that the Web Rendering Service does not retain state across page loads, with local storage, session storage and cookies cleared; declined permission requests; unsupported connection types; and paywall implementation. Google Search Central, Dynamic rendering as a workaround (last updated 10 December 2025) - that dynamic rendering is a workaround rather than a recommended solution, and the three alternatives Google names. Google Search Central, Mobile-first indexing best practices and Fix lazy-loaded content - content requiring user interaction, and viewport-based loading; Pagination, incremental page loading, and Search - crawlers not clicking buttons. Google Search Console Help, URL Inspection tool - that the URL must be in the currently opened property and that the non-owner alternative is the Rich Results test or AMP test; the no-login requirement for a live test; the tunnel option for firewalled pages; that URL is on Google does not guarantee appearance in Search; and that None is a fine value for the user-declared canonical on a page that is not one of a set of alternates. Reviewed 28 August 2026; the page carries no publication date. Google Search Console Help, Rich Results Test - testing a URL or a pasted code snippet; that all page resources must be reachable by an anonymous user and that firewalled or password-protected resources are unavailable to the test; and that a page Google is prevented from crawling by a robots.txt rule or noindex directive cannot be tested with the tool. Reviewed 28 August 2026; the page carries no publication date. Google Search Central, Debug your website pages (last updated 10 December 2025) - testing local or firewalled pages by exposing a tunnel, and the caveat that some tunnelling solutions protect the temporary public URL with robots.txt, which prevents Google's tests from running. Google Search Central, Spam policies for Google web search - the definition of cloaking and the paywall carve-out quoted in failure mode 8. Google Search Central, Paywalled content (structured data) (last updated 10 December 2025) - the purpose of the markup in distinguishing paywalled content from cloaking, the isAccessibleForFree and cssSelector properties, the class-selector-only requirement, and the instruction to make the page accessible to Googlebot (and Googlebot-News, where applicable) if the paywalled sections are to be crawled and indexed.


About the author

Zarko Zivkovic is the founder of CoreAEX, building technical SEO, AEO, and AI-visibility systems for B2B SaaS companies. Connect on LinkedIn.