Why your Lovable site isn't showing up on Google
A Lovable app not indexed on Google is usually not a rendering bug, but check anyway. Seven causes ranked by likelihood, with the curl test that proves which one.
16 min read
The most common reason a Lovable site isn’t on Google is that it was never publicly published. The second is that not enough time has passed. JavaScript rendering (the cause almost every article on this topic leads with) is still not where you start. On Lovable’s new TanStack Start stack it is no longer a long shot either: in a study of 82 Lovable apps, only 4.4% of page routes loaded their data on the server, and nearly half fetched it in the browser, where server rendering never runs it. Where your dynamically loaded content (CMS posts, product pages, directory listings: anything that isn’t written into the page’s source code) reaches the HTML depends on where the fetch lives, and that is a different question from whether server rendering is switched on.
This guide walks the causes in that order: most likely first. Each one has a test you can run from a terminal, so you can rule it out in seconds instead of guessing. One exception to the ordering, and our study is the reason for it: if the pages missing from Google have dynamically loaded content on a project created after 13 May 2026, read cause 7 before causes 3 to 6. Content fetched in a component hook, which never reaches the HTML, is more common on that stack than any of the paperwork causes ranked above it. If you want the wider picture rather than a specific fix, start with the Lovable SEO overview and come back here.
Run these four commands before you change anything
Everything below depends on knowing what a crawler actually receives at your live URL. Replace the domain and run these against the published custom domain.
# 1. Is the page reachable, and does it redirect anywhere?
curl -sIL https://yourdomain.com/ | grep -Ei '^(HTTP/|location:)'
# 2. What does a plain client get in the raw body?
curl -sS https://yourdomain.com/pricing | grep -oE '<title>[^<]*|<h1[^>]*>' | head
# 3. Does your own content reach the HTML? Run this against a page
# with dynamically loaded content - products, listings, posts - never the homepage, and grep for a
# phrase that isn't in your source code.
curl -sS -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' \
https://yourdomain.com/products > /tmp/page.html
grep -qiF 'Ceramic Pour-Over Kettle' /tmp/page.html \
&& echo 'server-rendered' || echo 'client-fetched - crawlers never see it'
# 4. Is anything telling Google to stay away?
curl -sI https://yourdomain.com/pricing | grep -i 'x-robots-tag'
curl -sS https://yourdomain.com/pricing | grep -iE '<meta[^>]+name="robots"'
Keep that output. Most of the sections below refer back to it.
Two things to get right in command 3. The search phrase has to be one contiguous run of text, because markup splits phrases across elements and grep matches raw bytes. And the homepage is the wrong place to run it: a hand-written hero section proves nothing about the route that lists rows from your database.
Which stack are you on? It changes two of the answers
On 13 May 2026 Lovable changed the default stack for new projects from React + Vite to TanStack Start with server-side rendering, running on Cloudflare Workers. Projects created before that date were not migrated and still run as client-rendered SPAs.
| React + Vite (projects before 13 May 2026) | TanStack Start (new default) | |
|---|---|---|
| What a browser gets | SPA shell, content painted by JS | Server-rendered HTML of the component tree |
| What a verified crawler gets | Pre-rendered HTML, according to Lovable’s docs (can’t be verified from outside) | The same HTML: loader data included; data fetched in component hooks missing |
| What Screaming Frog or Ahrefs gets | The SPA shell | The same HTML every other client gets |
| Off Lovable hosting | Shell only: pre-rendering doesn’t come with you | Server rendering travels with the code, and so does the loader-versus-hook split |
The right-hand column needs its asterisk spelled out, because “SSR is enabled” and “my data is in the HTML” are two independent facts. Server rendering runs your component tree on the server. It does not run your effects. React’s documentation is blunt about it: effects “only run on the client. They don’t run during server rendering.” TanStack Query says the same thing about queries that were never prefetched: they “wont be server rendered, instead they will be fetched on the client after the application is interactive.” A page can therefore be 100% server-rendered and contain zero rows from your database. The HTML is a faithful render of the loading skeleton.
What decides the outcome is where the fetch lives. A route loader or a createServerFn runs on the server, and its result is serialized into the HTML. A useEffect, or a useQuery that nothing prefetched, runs after hydration, and its result is not. TanStack Start’s own Query guide states the rule in one line: the route awaits the query for content that must appear in the initial HTML. Lovable’s blog describes its Workers deployment with the same care: the server “runs the React tree, executes the loaders, and streams the result.” Loaders. True of the framework; the question is what the generator emits.
Lovable’s docs don’t answer that. Our study suggests the browser: across 82 Lovable apps, most routes that fetch data do it in useEffect or useQuery, and most of the apps query Supabase straight from the browser. For your own site, command 3 above is the verdict.
The Screaming Frog row is the reason so many Lovable owners get contradictory readings. Lovable’s own docs say third-party SEO scanners see the regular SPA shell rather than the pre-rendered HTML, so your audit tool can report an empty page while Google indexes the site perfectly. An audit tool screaming “no content” is not, by itself, evidence of an indexing problem.
Worth saying plainly: Lovable’s docs contradict each other on the stack. The deployment/hosting page and llms.txt still describe projects as Vite + React. The blog post, the SEO docs and the TanStack upgrade page are the current ones.
Cause 1: the project isn’t publicly published
This is the hard limit, and it is documented. Only publicly published projects are indexable. Private projects and branded workspace URLs remain unindexable regardless of how much SEO work you do: no meta tag, sitemap or Search Console submission changes that.
Two adjacent traps sit next to it:
- Publishing is a snapshot. Changes you make after publishing do not reach the live site until you publish again. If you fixed your titles four days ago and never republished, Google is still looking at the old HTML.
- The editor preview is not the live site. Verifying your fix in the preview panel proves nothing about what a crawler receives.
Command 2 above is the test. Run it against the public domain. If the title you see is the one you fixed last week, you’re published and current. If it’s the old one, republish and stop debugging.
Cause 2: not enough time has passed, and Google was never told
A new domain with no inbound links is not entitled to fast indexing. Google has to discover the URL, crawl it, render it, evaluate it, and then decide. Days to weeks is normal for a site nobody links to; two weeks of silence on a week-old domain is not a bug.
What is a bug is never telling Google the site exists. The minimum:
- Verify the property in Search Console. DNS TXT verification at your registrar is the most durable method because it survives republishes and host changes. A meta-tag verification has to live in the first-response HTML of the exact host you registered, which is why it fails so often when people register the custom domain and deploy under
*.lovable.appor vice versa. - Submit your sitemap. Lovable generates
sitemap.xmlandrobots.txtautomatically, but the docs hedge that they are “not always generated up front”, so verify, don’t assume.curl -sI https://yourdomain.com/sitemap.xmlshould return 200 with an XML content type. An HTML content type means you’re getting the app shell. There’s more on that failure mode in the sitemap and robots.txt guide. - Use URL Inspection on three representative pages: the homepage, one marketing page, one deep page. Encited’s index tracking does this for every page and refreshes on demand. The status it reports is the single most useful piece of information in this entire process.
Lovable also ships a Search Console connector that can verify the property and submit the sitemap from chat, which removes the host-mismatch problem.
Cause 3: “Crawled - currently not indexed” is a verdict on your content
This is the status that sends people down the wrong path fastest. It means Googlebot fetched the URL, evaluated what it got, and decided not to index it.
Two root causes account for nearly all of it on a Lovable site:
Thin content. The route exists and renders, but there are eighty words on it. A pricing page with three cards and no prose is a thin page whether or not React built it.
Duplicate content across routes. This is the Lovable-specific one. A client-rendered SPA ships one index.html for every route, so every URL on your site can arrive with the same <title>, the same meta description, and | worst of all: the same canonical. Google sees twelve URLs asserting they are the same page and indexes one.
TanStack Start removes the shared-index.html mechanism but not the symptom, because per-route metadata is something the generated code has to opt into. In the same study, 63% of route files set no head: at all, so no per-page title and no meta description. A new-stack project can therefore ship the same title on every route for a completely different reason than an old-stack one, and the check is identical.
Tell them apart with URL Inspection → View Crawled Page → HTML, then compare that HTML across three routes. Identical <title> on all three is your answer, and the fix is per-route titles and descriptions plus deleting the competing static tags baked into index.html.
While you’re in there, check for a hardcoded canonical. A single <link rel="canonical" href="https://example.com/"> served on every route tells Google to consolidate your entire site into the homepage. That is strictly worse than having no canonical at all, and it’s the default state of any SPA where someone added a canonical tag to the shell. Google’s guidance is a self-referencing canonical on each page, using absolute URLs, in the <head>.
Cause 4: Google indexed your old *.lovable.app URL instead of your domain
Classic and under-diagnosed. You launched on yourapp.lovable.app, Google indexed it, you later attached a custom domain, and now search results still show the old host, or worse, both hosts are indexed and splitting signals between them.
Run command 1 against the old URL and read the status line:
curl -sIL https://yourapp.lovable.app/ | grep -Ei '^(HTTP/|location:)'
Three outcomes:
- 200: both hosts serve the site independently. Google has no reason to prefer either, and you have genuine duplicate content.
- 302: a temporary redirect tells Google the old URL is still the canonical one and to keep checking back. It’s the wrong signal for a permanent move.
- 301: correct. Google will consolidate to the target over the following crawls.
The full fix is three things, not one: a permanent 301 from the old host to the matching path on the new one (not all paths to the homepage, which Google treats as a soft 404); a self-referencing canonical on every custom-domain route pointing at the clean URL without tracking parameters; and both hosts registered as Search Console properties so you can watch the index swap rather than guess at it. Expect the swap to take weeks.
If the .lovable.app version picked up real backlinks before you moved, the 301 is what passes that equity along, which is another reason not to leave it on a 302.
Cause 5: a noindex is still in place
Cheap to check, embarrassing to miss. Command 4 covers both places it hides: the X-Robots-Tag response header and the <meta name="robots"> tag in the HTML.
Staging protection is the usual origin. Someone adds noindex while the site is being built, the site launches, nobody removes it. Search Console will tell you directly (the page status reads “Excluded by ‘noindex’ tag”) which is why URL Inspection beats speculation every time.
One non-obvious detail if your app injects the robots tag with JavaScript: Google says that when it encounters noindex, it may skip rendering and JS execution entirely. So adding a noindex client-side works, but removing one client-side may not. If a noindex was ever in the static HTML, take it out of the static HTML.
Cause 6: robots.txt is blocking something it shouldn’t
Read the file, don’t assume its contents:
curl -sS https://yourdomain.com/robots.txt
Three things to check, in order of damage:
- A blanket
Disallow: /. Obvious, and it happens. - Blocking
/assets/or your JS bundle. This is the one way to genuinely make Google unable to see a React app. Googlebot needs those files to render, and Google’s docs are explicit that it is allowed to crawl them. Blocking them is self-inflicted invisibility, and on a client-rendered app it also kills social link previews. - Using
Disallowto keep a page out of the index. It doesn’t work that way. Google can’t index the content of a disallowed page but may still index the URL and show it without a snippet. Worse, a page you disallowed can never be read for itsnoindextag: the two directives cancel each other out.noindexis the index directive;Disallowis a crawl directive.
Google caches robots.txt for up to about 24 hours, so a fix here is not instant.
Cause 7: the HTML a crawler gets is missing your content
Now the JavaScript question. On React + Vite it belongs here, near the bottom, because Lovable’s hosting pre-renders for verified crawlers. On TanStack Start, for pages with dynamically loaded content, it belongs much higher, for the reason the study numbers gave above: fetching in a hook rather than a loader is what the generated code usually does. Two versions follow, because the symptom shows up differently on each stack.
Google’s render queue is fast. Measurements from Vercel and MERJ across 37,000+ matched fetches put the median render delay at 10 seconds, with p25 around 4 seconds and p75 around 26 seconds. There’s a real long tail (p90 near 3 hours, p99 around 18 hours) but “the render queue takes weeks” is obsolete, and in the same study every HTML page sampled on a large Next.js site resulted in a full render. So on Lovable’s hosting, “Googlebot can’t execute my JavaScript” is rarely the right diagnosis. “The content was never in the first response, and what Google eventually rendered didn’t earn an index slot” still can be, and that is a different failure with a different fix.
The old stack, self-hosted: an empty shell
Leaving Lovable’s hosting changes the picture completely. Pre-rendering is a property of Lovable’s deployed public URLs, so it stays behind when your code leaves. Export to GitHub and deploy to Vercel, Netlify, Cloudflare Pages or your own box and (on the React + Vite stack) every crawler now receives the shell. The self-hosting guide covers the rest of what changes; for indexing, this is the part that bites.
Confirm it before acting. On a self-hosted deployment there is no verified-crawler gate, so curl tells you the truth:
curl -sS https://yourdomain.com/pricing | grep -c '<h1'
# 0 with visible content in the browser = crawlers get nothing
And check that a nonexistent route returns the right status, because an SPA catch-all rewrite will happily serve index.html with a 200 for every typo:
curl -sI -o /dev/null -w '%{http_code}\n' https://yourdomain.com/this-does-not-exist
# 200 = soft 404s at scale. 404 = correct.
The new stack: full HTML, no data
Here the response is not empty. It has your nav, your footer, your layout, your headings, and, if the route fetches from Supabase inside a component hook, an empty product grid where the indexable content was supposed to be. Nothing is broken. The server rendered exactly what the component renders before its data arrives.
One app in the study shows the failure at full strength. Its public blog post route (blogs.$slug.tsx) has no loader:, no head:, a useEffect that queries the blogs table, and loading initialized to true. The server-rendered HTML for every post on that site is a spinner, with no title, no description and no og tags. It’s the exact shape command 3 is looking for.
Command 3 is the whole diagnostic. Run it per route type, not once per site: a hand-written marketing page and a dynamically loaded listing can behave differently inside the same project, and only the second one matters here.
One wrinkle when you read the output. With streaming SSR, resolved content can arrive late in the response inside a <div hidden id="S:0">…</div> block, followed by a $RC(...) script that moves it into place. Your grep will find the phrase, because it genuinely is in the bytes. Whether a particular crawler counts content sitting in a hidden div as page content is a separate question, and for Google the only thing that settles it is the rendered HTML in URL Inspection.
What to do about either version
If command 3 says client-fetched, the cheapest fix is to move that fetch into the route’s loader so its result is serialized into the first response. That is a code change you can ask for in chat, it costs nothing at runtime, and command 3 tells you immediately whether it worked.
If that isn’t available to you (you’re off Lovable hosting, or still on React + Vite, or the route genuinely can’t be loader-fetched) four honest remedies, cheapest first:
- Upgrade the project to TanStack Start before you leave, then self-host. The upgrade is triggered from chat, costs credits, and is reversible from version history. Two caveats. From Lovable’s own docs: browser-only libraries can break server rendering in ways the upgrade’s checks don’t catch, so test afterwards. And from the section above: the upgrade turns server rendering on, it does not by itself move your data fetches into loaders. Re-run command 3 when it finishes.
- Build-time static generation. You can’t do this on Lovable. Lovable runs its own heavily modified Vite setup, and its build servers aren’t meant for heavy jobs like prerendering: try it and you can crash the project. It only works once you export the code and build it on your own machine or CI. If your public routes are enumerable, prerender them at build time. Every crawler gets real HTML, there’s no runtime cost, and there’s nothing to keep in sync. This is the right answer for a marketing site and the wrong one for high-cardinality or per-user routes.
- Framework SSR. React Router in framework mode (
ssr: true, optionally with aprerender()list) or TanStack Start’s static prerendering both give you real HTML per route. More work, correct for everything. - Edge pre-rendering. A worker in front of your origin that serves cached rendered HTML to crawlers and the SPA to humans. Encited is the managed option we’d use; a self-hosted headless Chrome behind a Worker is the DIY version. It needs no change to your app code, and a managed service keeps the snapshots fresh and shows you what each bot received. Google’s docs call dynamic rendering a workaround because of the extra complexity and resources it takes to run yourself, and its spam policies don’t treat it as a violation. Bing recommends it and says it isn’t cloaking when the content matches.
On UA-split rendering and cloaking: Google defines cloaking by intent to mislead plus content divergence. A prerender cache that drifts months out of date while the live app shows something else is how a legitimate setup turns into a real one, so treat your recache TTL as a correctness requirement.
If you go the pre-rendering route, pick a service that shows you what each bot actually received. Without that you’re left guessing, because on the React + Vite stack what verified crawlers receive can’t be verified from outside. Encited keeps per-bot, per-URL crawl logs recording whether Googlebot or GPTBot got rendered HTML or fell through to the shell, and installs as middleware on Vercel, Cloudflare Workers or Netlify.
One more, if your interior pages specifically are missing
If the homepage is indexed and nothing else is, check how your navigation is built. A Lovable-generated SPA often navigates with onClick={() => navigate('/pricing')} on a <button> rather than an <a href>. Humans can’t tell the difference; crawlers can’t follow a button. Grep the export for useNavigate and convert anything that is navigation rather than an action into a real anchor. Hash routes (/#/pricing) are the more severe version of the same problem: the fragment is never sent to the server, so no crawler, CDN or prerender layer can ever see which route was requested. More on both in the crawlability guide.
What to do next
- Open Search Console and run URL Inspection on one page you expect to rank. Write down the exact status string.
- Match it: Excluded by ‘noindex’ → cause 5. Blocked by robots.txt → cause 6. Crawled - currently not indexed → cause 3. Discovered - currently not indexed → cause 2, plus internal links. Duplicate, Google chose a different canonical → cause 4.
- Run
View Crawled Page → HTMLon three routes and compare the<title>values. If they match, fix per-route metadata before anything else. - Run command 3 against a page with dynamically loaded content. If the phrase isn’t there, move that fetch into the route’s
loader, and if that isn’t an option, pick one of the four remedies in cause 7. - Republish, then request indexing once, for the changed page only.
Frequently asked questions
- Why is my Lovable app not indexed on Google after two weeks?
- Check publication state first. Lovable's docs state that only publicly published projects are indexable: private projects and branded workspace URLs stay unindexable no matter what SEO work you do. If the project is public, the next most likely cause is simply that Google hasn't finished evaluating it: a brand-new domain with no inbound links can take weeks to move from discovered to indexed.
- Does 'Crawled - currently not indexed' mean Google can't render my JavaScript?
- No. That status means Googlebot fetched the URL, evaluated it, and chose not to index it. If rendering had failed you'd normally see the page indexed with missing content, or flagged as a soft 404. 'Crawled - currently not indexed' is almost always a content verdict: the page is thin, or it's near-identical to other URLs on the site because every route ships the same title and description.
- Will Request Indexing in Search Console force Google to index my page?
- No. Request Indexing pushes the URL into the crawl queue sooner; it does not override the quality evaluation that follows. If Google already crawled the page and declined it, re-requesting without changing the page usually produces the same verdict. Change the page first, then request.
- Google indexed my .lovable.app URL instead of my custom domain. How do I fix it?
- Run curl -sIL on the old URL and read the status line. If it returns 200 or a 302, Google has no instruction to move. You want a permanent 301 to the matching path on the custom domain, a self-referencing canonical on every custom-domain route, and both hosts added as Search Console properties so you can watch the swap happen.
- Do I need a third-party prerendering service for a Lovable-hosted site?
- Lovable's documentation says external pre-rendering services are unnecessary for Lovable-hosted projects, but check one thing yourself first: server rendering only includes data a route loader fetched, so content fetched inside a component hook is absent from the HTML a crawler receives. That pattern is common. In a study of 82 Lovable apps, only 4.4% of page routes loaded their data in a route loader. Curl a page with dynamically loaded content and grep for a phrase that isn't in your source code, such as a product name. The picture also changes the moment you export the code and deploy it yourself, because Lovable's request-time pre-rendering runs on its own deployed URLs and does not travel with your repository.
Read next
Keep going
-
Lovable SEO: the complete 2026 guide
Lovable SEO splits across two stacks, and SSR alone does not put your data in the HTML. Test both on your own routes, then close the gaps Lovable leaves to you.
-
What 82 Lovable TanStack apps actually server-render
Lovable TanStack SSR, measured: in 82 Lovable apps, only 4.4% of pages load their data on the server. The layout reaches the HTML; the data usually doesn't.
-
Pre-rendering a Lovable app: when you need it
Lovable prerendering, honestly: SSR puts your page layout in the HTML but usually not your data. Why that happens, and a 30-second test for your own site.
-
sitemap.xml and robots.txt on a Lovable site
Lovable generates sitemap.xml and robots.txt automatically, but not always up front. Verify your Lovable sitemap, kill invented URLs, and ship a correct robots.txt.