---
title: "What 82 Lovable TanStack apps actually server-render | Lovable Field Guide"
url: https://prerenderlovable.com/blog/lovable-tanstack-ssr-study
description: "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."
lang: en
---

Skip to content

# 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.

Published September 13, 2026 10 min read

We read 82 Lovable-generated TanStack Start repositories off disk and counted where every page route fetches its data. **Only 4.4% of 1,078 page routes define a route `loader:`: 2.3% in repos from the last four months.** About 46% fetch inside the component instead, with `useEffect` or `useQuery`.

That is the whole finding. Lovable’s pages are server-rendered, and most of their dynamically loaded content (CMS posts, product pages, directory listings: anything that isn’t written into the page’s source code) still arrives after the page has loaded.

## The short version

- **82 repos, 1,078 page route files**, found by GitHub code search and read in full. 45 of them were last updated in June 2026 or later, after Lovable made TanStack Start the default on 13 May 2026.
- **47 routes (4.4%) define a `loader:`.** In the recent subset it is 18 of 775 (2.3%). And 25 of those 47 loaders sit in one repository, all on `/admin/*` routes.
- **481 routes (44.6%) fetch via `useEffect` or `useQuery`** inside the component. Another 560 (52%) are static JSX with no data fetch at all.
- **48 of 78 repos import the browser Supabase client; 9 use the server client.** 676 files call `.from(table)` from browser code. One repo in 78 wires react-query into SSR.
- **672 of 1,063 route files (63%) define no `head:` at all**: no per-page title, no meta description.
- **The markup is server-rendered.** Live fetches returned 47,036 and 49,347 bytes of real HTML. The layout is there; the data mostly isn’t.

If you are arriving without the background, the Lovable SEO overview (https://prerenderlovable.com/blog/lovable-seo) is the place to start. The mechanism behind all of this (why server-side rendering renders your component tree and not your data) is worked through in TanStack Start SSR vs prerendering (https://prerenderlovable.com/blog/tanstack-start-ssr-vs-prerendering). This post is the evidence underneath it.

## Why we looked

Since 13 May 2026, new Lovable projects ship on TanStack Start with SSR enabled, deployed to Cloudflare Workers. The natural conclusion, and the one most people draw, is that the SEO problem is solved: the server returns HTML, crawlers read HTML, done.

The conclusion does not follow, and the reason is documented by the framework authors themselves. React’s `useEffect` documentation is blunt: _“Effects only run on the client. They don’t run during server rendering.”_ TanStack Query’s SSR guide says of queries you never prefetch that they _“wont be server rendered, instead they will be fetched on the client after the application is interactive.”_ TanStack Start’s own Query guide states the positive version: _“The route awaits the query for content that must appear in the initial HTML.”_

So “SSR is on” and “my data is in the HTML” are independent facts. The second one depends entirely on where the fetch lives: a route `loader` or a `createServerFn` invoked from one, versus a hook that runs after mount.

Lovable’s engineering blog is worded correctly on this, if you read it closely: the server “runs the React tree, **executes the loaders**, and streams the result.” Loaders. That is the hinge. It describes what the framework can do, and says nothing about what Lovable’s generator writes.

Nobody had checked what the generator emits. That is a countable question, and the code is public. So we counted.

## Method

### Finding the apps

Lovable-generated TanStack Start projects carry two unusual dependencies together in `package.json`: `@tanstack/react-start` and `lovable-tagger`. The second is Lovable’s own Vite plugin and is not something you would install by hand. GitHub code search for both, in the same file, restricted to `package.json`:

```
"@tanstack/react-start" "lovable-tagger" in:file filename:package.json
```

That produced 82 repositories. We cloned each one and counted from the route files themselves. 54 of the 56 Cloudflare configs still carry Lovable’s template name (`tanstack-start-app`), and the Supabase client opens with Lovable’s generated-file banner, which marks them as Lovable’s own output.

**45 of the 82 were last pushed in June 2026 or later**, after the switch to TanStack Start, so every table shows both the full set and those 45.

## Results: where the routes fetch

| Metric | All 82 repos | Recent 45 (Jun–Sep 2026) |
| --- | --- | --- |
| Page route files analyzed | 1,078 | 775 |
| Routes defining a route `loader:` | **47 (4.4%)** | **18 (2.3%)** |
| Routes fetching via `useEffect` / `useQuery` | **481 (44.6%)** | **359 (46%)** |
| Static JSX, no data fetch | 560 (52%) | 404 (52%) |
| Homepages with a loader | **1 of 76** | n/a |
| Route files with no `head:` (no title/meta) | **672 of 1,063 (63%)** | 60% |

Three things in that table deserve to be pulled out.

**The few loaders that exist are concentrated.** 25 of the 47 loaders live in a single repository, all on `/admin/*` routes: pages behind a login that no crawler will ever request. Strip that one repository out and the remaining 22 loaders are spread across 81 repositories. Most of the apps have no loaders at all.

**One homepage in 76 has a loader.** Homepages are usually the most static page on a site, so this is the least alarming number here, but it does tell you the pattern is not “loaders for the important pages, hooks for the rest”. The pattern is “hooks”.

**63% of route files define no `head:`.** That is a separate defect from the data question and it compounds it: a route with no `head:` has no per-page `<title>` and no meta description, which is the failure described in meta tags and titles on Lovable (https://prerenderlovable.com/blog/lovable-meta-tags-and-titles). A page can lose on both axes at once | no title in the head, no data in the body.

The recency split matters here. The loader rate roughly halves in recent repositories, from 4.4% to 2.3%, while the hook rate ticks up from 44.6% to 46%. Whatever the generator is doing, it is not trending toward loaders.

## Results: how the database gets queried

| | All repos | Recent |
| --- | --- | --- |
| Repos importing the **browser** Supabase client | **48 / 78** | 26 / 45 |
| Repos using the **server** Supabase client | **9 / 78** | 7 / 45 |
| Files calling `.from(table)` from browser code | **676** | 205 |
| Repos wiring react-query into SSR | **1 / 78** | n/a |

676 files issue Supabase queries from the browser. That is more than fourteen times the number of route files that have a loader of any kind, and it is the same finding from a different angle: the data layer in these apps runs after hydration, in the visitor’s browser, over an HTTP request the server never made.

The tables being queried from browser code are not all private-user data either:

| Table | Files querying it from the browser |
| --- | --- |
| `profiles` | 81 |
| `user_roles` | 45 |
| `habits` | 23 |
| `payments` | 21 |
| `invoices` | 16 |
| `blogs` | 14 |

`profiles`, `user_roles`, `payments` and `invoices` are behind-login data and belong on the client as far as indexing is concerned: nobody needs Googlebot reading an invoice. But `blogs` is public, indexable, long-tail content. Fourteen files fetch it from the browser. That is the category where the pattern costs you something real.

## Results: Lovable’s own example code

Every new Lovable project on the TanStack stack comes with an example file, `src/lib/api/example.functions.ts`, that shows how to run code on the server. Its comment reads:

```
// Example createServerFn. Server-side handler invoked from the client.
```

“Invoked from the client” means the browser calls that server code after the page has loaded, for example when someone submits a form. That’s a sensible way to handle forms and other actions. The catch is timing: anything fetched this way arrives after the page has already been sent, so it never appears in the HTML a crawler receives. To get data into that HTML, the server code has to be called from the page’s loader, which runs before the page is sent.

The apps in the study mostly use server code the way the example does:

- **Server functions set up to send data outnumber ones set up to fetch it, 188 to 85.** That suggests most of them handle actions like form submissions, which happen after the page loads.
- **The hook for calling server code after the page loads (`useServerFn`) appears in 63 files across 10 apps.** Anything it fetches arrives too late to be in the HTML.

## The single clearest artifact

One file in the sample makes every number above concrete: `agni-gaming-hub/src/routes/blogs.$slug.tsx`. It is a public blog post page: a canonical indexable route, the kind of page that exists to be found in search.

Here is its shape, abridged to the parts that matter (the JSX body and the styling are elided; the structure is as found):

```
// src/routes/blogs.$slug.tsx
export const Route = createFileRoute('/blogs/$slug')({
  component: BlogPost,
  // No loader:. No head:.
})

function BlogPost() {
  const { slug } = Route.useParams()
  const [post, setPost] = useState(null)
  const [loading, setLoading] = useState(true)   // <- initial state, and what SSR renders

  useEffect(() => {
    supabase
      .from('blogs')
      .select('*')
      .eq('slug', slug)
      .single()
      .then(({ data }) => {
        setPost(data)
        setLoading(false)
      })
  }, [slug])

  if (loading) return <Spinner />
  return /* the actual post */
}
```

Walk through what the server does with that on a request for `/blogs/anything`.

1. The route matches. There is no `loader:`, so there is nothing to await and no data to serialize into the document.
2. React renders `BlogPost` on the server. `loading` is `true`, because that is its initial state and `useEffect` does not run during server rendering.
3. The `if (loading)` branch is taken. The server renders `<Spinner />`.
4. There is no `head:`, so the document gets whatever the root route supplies: no per-post `<title>`, no description, no `og:title` or `og:description`.
5. The HTML is sent. The response is complete, valid, 200, and contains a spinner where the article should be.

**The server-rendered HTML for every blog post on that site is a spinner with no title, description or og tags.** Every post on that blog is the same document as far as a consumer that reads bytes and stops is concerned. Share one on Slack and the unfurl has nothing per-post to work with, for the reasons in why Lovable link previews break (https://prerenderlovable.com/blog/lovable-link-previews-broken).

Anything that reads the HTML and moves on, like link unfurlers and most AI crawlers, gets the spinner.

## What to do about it: check your own route

Checking your own site takes about thirty seconds.

Pick a page with dynamically loaded content: a product page, a blog post, a listing. Skip the homepage, which is usually static and will pass regardless. Choose a phrase that isn’t in your source code and is one contiguous run of text.

```
curl -s https://yourdomain.com/blogs/your-post-slug > /tmp/page.html

# Use a contiguous phrase that isn't in your source code.
grep -qiF 'a phrase from your post body' /tmp/page.html \
  && echo 'server-rendered' \
  || echo 'client-fetched: byte-reading consumers never see it'
```

Use a phrase that’s one contiguous run of text, because markup splits phrases across elements. On the older React + Vite stack the result can’t be verified, so run this on TanStack Start projects.

### If the phrase is missing: the fix options

In rough order of how much work they are.

**Move the fetch into a route `loader:`.** This is the actual fix and it is usually small. The data you are already fetching in `useEffect` gets fetched in the loader instead, on the server, before the response is written, and TanStack serializes it into the document.

```
export const Route = createFileRoute('/blogs/$slug')({
  loader: ({ params }) => getPost({ data: params.slug }),  // a createServerFn
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.title },
      { name: 'description', content: loaderData.excerpt },
      { property: 'og:title', content: loaderData.title },
    ],
  }),
  component: BlogPost,
})
```

Note that the `head:` can read `loaderData`. One edit fixes both the data and the title.

**Ask Lovable for it in the prompt.** Since the generator does not do this by default, say so explicitly: move this route’s data fetch out of the `useEffect` and into the route loader, and derive the page title and description from the loaded data. Check the diff afterward.

**Prefetch into react-query from the loader and dehydrate.** If you want to keep `useQuery` in the component, prefetch it in the loader so the cache is hydrated from the server response. One repository in 78 did this, so it is not what you will get by default, but it is the documented TanStack path and it preserves your existing component code.

**Use the server Supabase client for anything indexable.** Nine repositories out of 78 used it. If a query feeds content that should be findable, it belongs on the server side of the boundary.

**Pre-render the route in a headless browser.** If the fetch genuinely cannot move (a third-party widget, a component you do not control, a route where the refactor is not worth it) then the remaining option is to render the page in a real browser, wait for the network to settle (https://encited.com/docs/troubleshooting/prerender-ready-signal), and serve the resulting HTML to crawlers.

Encited (https://encited.com/) does this as a hosted service and adds crawl logs (https://encited.com/docs/crawl-logs/confirm-a-bot-got-your-content) showing which bots received the rendered version, so you can confirm the fix worked.

## What to do next

1. Today, run the `grep` test above against one page on your own site with dynamically loaded content, such as a product or listing page.
2. If the phrase is missing, open that route file and look for `useEffect` or a bare `useQuery`. That is the line to move.
3. Count how many of your route files define a `head:`. In the study, 63% did not, and that is the cheaper of the two fixes.
4. Re-run the test after the change and confirm the phrase appears. A fix you did not verify is a guess.

If you want the framework-level reasoning behind all of this before you touch anything, TanStack Start SSR vs prerendering (https://prerenderlovable.com/blog/tanstack-start-ssr-vs-prerendering) works through the mechanism, and SSR vs SPA for SEO (https://prerenderlovable.com/blog/ssr-vs-spa-seo) covers which crawlers execute JavaScript and which do not.

Found something wrong or out of date? Lovable changes fast and we'd rather fix a guide than let it rot.

Disclosure: the team behind this guide also builds Encited, mentioned above.

## Frequently asked questions

Do Lovable's TanStack Start apps actually server-render?

Yes, for the static parts. Your layout, navigation and any text written into the page itself are rendered on the server. Most dynamic content is still fetched and rendered in the browser. So if a page pulls its content from a CMS, your database or an API, like blog posts, products, listings or profiles, that content usually isn't in the HTML crawlers get.

How many Lovable-generated TanStack routes use a route loader?

Very few. Only 4.4% of the 1,078 pages we checked load their data on the server, and most of those sit in one app's admin pages. The rest either fetch their data in the browser or have no data at all.

Where does Lovable-generated code fetch Supabase data from?

Mostly from the browser. 48 of the 78 apps query Supabase straight from the browser, and only 9 do it on the server. That's why the data isn't in the HTML.

Does this mean Lovable pages ship as empty shells?

No. The page structure and static text are in the HTML. What's usually missing is the dynamic content: anything the page loads from a CMS, a database or an API, like blog posts, products or directory listings.

How do I check whether my own Lovable route server-renders its data?

Open a page that shows dynamic content, like a blog post or a product page, and copy a phrase from that content. Fetch the page with curl and search the result for the phrase. If it's there, crawlers can see it. If it isn't, the content is being loaded in the browser.

Read next

## Keep going

- ### TanStack Start SSR vs prerendering: https://prerenderlovable.com/blog/tanstack-start-ssr-vs-prerendering
  TanStack Start SSR vs prerendering: SSR only includes data a route loader fetched. Whether crawlers see content depends on where the fetch lives. How to check yours.
- ### Pre-rendering a Lovable app: when you need it: https://prerenderlovable.com/blog/lovable-prerendering
  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.
- ### Lovable SEO: the complete 2026 guide: https://prerenderlovable.com/blog/lovable-seo
  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.
- ### SSR vs SPA for SEO: what actually changes: https://prerenderlovable.com/blog/ssr-vs-spa-seo
  SSR vs SPA SEO, settled: Googlebot renders your SPA, but unfurlers and AI crawlers read raw bytes, and SSR only helps if your data is fetched in a route loader.

## Structured data

```json
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "WebSite",
      "@id": "https://prerenderlovable.com/#website",
      "url": "https://prerenderlovable.com",
      "name": "The Lovable Field Guide",
      "description": "A working journal on Lovable SEO: getting apps built with Lovable indexed, ranked and cited, plus the migration and integration work that comes with it.",
      "publisher": {
        "@id": "https://prerenderlovable.com/#organization"
      },
      "inLanguage": "en"
    },
    {
      "@type": "Organization",
      "@id": "https://prerenderlovable.com/#organization",
      "name": "The Lovable Field Guide",
      "url": "https://prerenderlovable.com"
    },
    {
      "@type": "BlogPosting",
      "@id": "https://prerenderlovable.com/blog/lovable-tanstack-ssr-study#article",
      "headline": "What 82 Lovable TanStack apps actually server-render",
      "description": "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.",
      "datePublished": "2026-09-13T00:00:00.000Z",
      "dateModified": "2026-09-13T00:00:00.000Z",
      "inLanguage": "en",
      "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "https://prerenderlovable.com/blog/lovable-tanstack-ssr-study"
      },
      "author": {
        "@type": "Organization",
        "name": "The Lovable Field Guide",
        "url": "https://prerenderlovable.com"
      },
      "publisher": {
        "@id": "https://prerenderlovable.com/#organization"
      },
      "keywords": "lovable tanstack ssr, lovable tanstack start loader, does lovable server render data, lovable supabase browser client, tanstack start route loader seo",
      "articleSection": "SEO"
    },
    {
      "@type": "BreadcrumbList",
      "@id": "https://prerenderlovable.com/blog/lovable-tanstack-ssr-study#breadcrumbs",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Home",
          "item": "https://prerenderlovable.com/"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "SEO",
          "item": "https://prerenderlovable.com/seo"
        },
        {
          "@type": "ListItem",
          "position": 3,
          "name": "What 82 Lovable TanStack apps actually server-render",
          "item": "https://prerenderlovable.com/blog/lovable-tanstack-ssr-study"
        }
      ]
    },
    {
      "@type": "FAQPage",
      "@id": "https://prerenderlovable.com/blog/lovable-tanstack-ssr-study#faq",
      "mainEntity": [
        {
          "@type": "Question",
          "name": "Do Lovable's TanStack Start apps actually server-render?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "Yes, for the static parts. Your layout, navigation and any text written into the page itself are rendered on the server. Most dynamic content is still fetched and rendered in the browser. So if a page pulls its content from a CMS, your database or an API, like blog posts, products, listings or profiles, that content usually isn't in the HTML crawlers get."
          }
        },
        {
          "@type": "Question",
          "name": "How many Lovable-generated TanStack routes use a route loader?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "Very few. Only 4.4% of the 1,078 pages we checked load their data on the server, and most of those sit in one app's admin pages. The rest either fetch their data in the browser or have no data at all."
          }
        },
        {
          "@type": "Question",
          "name": "Where does Lovable-generated code fetch Supabase data from?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "Mostly from the browser. 48 of the 78 apps query Supabase straight from the browser, and only 9 do it on the server. That's why the data isn't in the HTML."
          }
        },
        {
          "@type": "Question",
          "name": "Does this mean Lovable pages ship as empty shells?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "No. The page structure and static text are in the HTML. What's usually missing is the dynamic content: anything the page loads from a CMS, a database or an API, like blog posts, products or directory listings."
          }
        },
        {
          "@type": "Question",
          "name": "How do I check whether my own Lovable route server-renders its data?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "Open a page that shows dynamic content, like a blog post or a product page, and copy a phrase from that content. Fetch the page with curl and search the result for the phrase. If it's there, crawlers can see it. If it isn't, the content is being loaded in the browser."
          }
        }
      ]
    }
  ]
}
```