A React app can rank in Google, but only if Google gets real HTML for your public pages. This guide covers that first, then Search Console (what Google sees), then Google Analytics 4 (what visitors do), with two pitfalls specific to apps that also have sign-in.
1. Give crawlers real HTML
A classic single-page app serves the same empty index.html for every URL and fills it in with JavaScript. Google can render JavaScript, but slowly and not always; other search engines, link previews (WhatsApp, LinkedIn, X) and AI crawlers mostly don't.
Pre-render the public pages (home, pricing, blog, docs) to HTML at build time, each with its own <title>, description, canonical link and Open Graph tags, and keep the signed-in app as a single-page app. Frameworks like TanStack Start, Astro or Next.js do this. Then generate:
-
sitemap.xmllisting every public page -
robots.txtthat allows crawling and points to the sitemap
2. Verify your site in Search Console
Go to search.google.com/search-console and Add property. Two kinds:
-
Domain (
example.com): covers every subdomain and protocol. Verify with a DNS TXT record. On Cloudflare, Google can add it for you. -
URL prefix (
https://example.com/): covers just that address. Verify by uploading an HTML file Google gives you (put it in your frontend'spublic/folder so every build includes it), a meta tag, or Google Analytics.
Keep the verification file or record forever: Google re-checks, and removing it loses access.
3. Submit the sitemap and request indexing
-
Sitemaps: submit
https://example.com/sitemap.xml. Search Console shows how many URLs it found. - URL Inspection: paste your most important URLs (home, pricing) and click Request indexing. There's a small daily quota, so use it on pages that matter.
First data appears in two or three days; a new site can take a few weeks to be fully indexed. Watch Pages for URLs Google found but didn't index, and why.
Also add Bing Webmaster Tools (bing.com/webmasters): it imports your Search Console site in a minute, and Bing's index feeds several AI search products.
4. Add Google Analytics 4
In analytics.google.com, create a property and a Web data stream for your domain. Copy the Measurement ID (G-XXXXXXXXXX).
Keep Enhanced measurement on: its "page changes based on browser history events" option counts client-side navigation in a single-page app as page views, so you don't need router code.
Load the tag from your public pages:
let loaded = false
function loadAnalytics(id: string) {
if (loaded) return
loaded = true
const w = window as unknown as { dataLayer?: unknown[] }
const dataLayer = (w.dataLayer ??= [])
function gtag(..._args: unknown[]) {
dataLayer.push(arguments) // gtag.js expects the arguments object, not an array
}
gtag("js", new Date())
gtag("config", id)
const script = document.createElement("script")
script.async = true
script.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
document.head.append(script)
}
Keep the ID in configuration, not in code, if other people or projects use the same codebase.
Pitfall 1: sign-in tokens in the URL
Some auth flows (Supabase's implicit flow, for example) return the user to your site with #access_token=... in the address. If analytics loads on that page, the token can end up in Google's reports. Skip loading analytics when the URL carries a token:
if (!location.hash.includes("access_token")) loadAnalytics(GA_ID)
Pitfall 2: another tag already created dataLayer
Cloudflare's Google tag gateway, a Tag Manager container or a plugin may create window.dataLayer before your code runs. Don't treat "dataLayer exists" as "analytics is loaded", or your tag never loads; track your own loaded flag and share the existing array, as in the snippet above. And if a Tag Manager container already fires your GA4 tag, don't load it twice, or every visit counts double.
5. Check it
- GA4 Reports → Realtime : open your site and see yourself within a minute.
- Search Console Sitemaps : status "Success".
- Share a page link in a chat app: the preview shows its own title, description and image.
If you have visitors from the EU, Google Analytics' cookies need consent there. Cookie-free alternatives such as Cloudflare Web Analytics avoid the banner.
Using it with FastAPI Supabase Starter
The public site is already pre-rendered, with per-page titles, descriptions, share images, structured data, sitemap.xml, robots.txt and an RSS feed. Set GA_MEASUREMENT_ID in .env and rebuild: the tag loads on public pages only, never on a URL carrying a session, and shares any existing dataLayer. Put Google's verification file in frontend/public/. See Blog and help pages.
Top comments (0)