Vite and React give you a great app and a poor website

This is not a criticism of the stack, which is excellent for building an application. It is that the defaults which make it excellent for an application are the same defaults that make it invisible as a website.

Run the free check

A default Vite and React app serves one nearly empty HTML file for every route, with one title and one description, and builds the page in the browser afterwards. That produces four problems at once: crawlers may receive nothing, every route shares one title, there is no sitemap or robots.txt, and there is no structured data. All four are fixable without leaving the stack.

Why you probably have this stack

Ask Lovable, Bolt, v0 or Claude Code for a web app and, unless you specified otherwise, you very likely received Vite as the build tool, React for the interface, and a client-side router handling the pages.

There are good reasons this is the default. It is fast to develop, the tooling is mature, it deploys to any static host, and it produces an application that feels responsive because navigation happens without a round trip to a server.

The trouble is that the same architecture, viewed as a website rather than as an application, has a specific and consistent set of problems. Because the stack is so common, those problems are the most common problems in this whole field, which is why this page exists separately from the platform pages.

Problem one: one HTML file for everything

Vite produces a single index.html containing an empty container and a script tag. Every route your app serves returns that same file, and the router decides in the browser which page to draw.

So a crawler requesting your pricing page and a crawler requesting your homepage receive byte-identical HTML, with no content in it. Whether that matters depends on whether the crawler executes your JavaScript, waits long enough, and does so reliably.

Check rather than assume, because your host or platform may already handle this. Fetch a deep route without a browser and look for that page's actual text in the response. If it is there, something is pre-rendering for you. If all you see is an empty container, the crawler sees that too.

Problem two: one title for every page

The title and meta description live in that single index.html, which means every route shares them.

This is the most common real defect in apps of this shape and it is entirely invisible while you use the site, because nobody studies the browser tab. It shows up in search results, in shared links, and to anything reading your pages programmatically.

A search engine fetching ten of your addresses and receiving ten identical titles has good reason to treat them as one page repeated, which is a pattern it is specifically built to collapse. Your ten distinct pages end up competing with each other rather than adding up.

The fix is a hook that sets the document title and meta tags per route, called at the top of each page component. It is an afternoon of work and it is covered in full on the titles page in this section.

Problem three and four: nothing is generated for you

Vite is a build tool, not a website framework, so it has no opinion about any of this and generates none of it.

There is no sitemap.xml unless you write one or generate one. There is no robots.txt unless you add one. There is no structured data. There is no per-route social preview image.

None of these are hard. What makes them a problem is that nothing tells you they are missing. The app builds, deploys, and works perfectly, and the absence is silent.

This is the difference between the stack being wrong and the stack being neutral. It is neutral. Everything it does not do is something you have to notice.

Fixing it without leaving the stack

You do not need to migrate to another framework. Four changes cover it, in this order.

  • Add build-time pre-rendering. A script that visits each route in a headless browser after the build and writes the resulting HTML to a file per route. Your host then serves real HTML, and the app still behaves as a single-page app afterwards.
  • Add per-route metadata. A small hook setting the title, description and Open Graph tags, called from each page component with values specific to that page.
  • Generate the sitemap during the build, from the same route list the pre-render script uses, so the two cannot disagree.
  • Add a robots.txt, and read it afterwards to confirm it is not blocking anybody you want.
  • Then verify the domain in Search Console and submit the sitemap, since none of the above tells Google anything by itself.

The detail that catches everybody

When you add pre-rendering, the route list has to come from one place.

The common failure is a hand-maintained list of routes for the pre-render script and a separate hand-maintained list for the sitemap. They agree on the day you write them and drift apart within a month, at which point you are pre-rendering pages that are not in your sitemap and listing pages in your sitemap that do not exist.

Derive both from the same source, which is usually the router configuration you already have. Anything describing generated content should itself be generated, or it will eventually be wrong and nobody will notice.

The second thing people miss: test a deep route rather than only the homepage. Pre-render setups that handle the entry point and silently skip everything else are common, and it is the version of this bug that survives longest because the page everybody checks works fine.

When to consider a different stack

Rarely, if you have a working app. Migration is a real cost and the four fixes above solve the problem for far less effort.

The case for a framework that renders on the server is genuine when your public pages show data that changes and you need that changing data indexed: listings, profiles, user-generated content you want found. Pre-rendering freezes content at build time, so it suits fixed pages and not live data.

If you are starting something new rather than repairing something that exists, it is worth knowing this choice is made at the beginning and is expensive to revisit. Lovable's documentation states that apps created from 13 May 2026 use TanStack Start with server-side rendering, which removes the whole question.

But do not migrate a working product for this reason alone. Pre-render it, fix the titles, generate the sitemap, and spend the time you saved writing pages instead.

Part of a larger guide

This page is one part of Vibe coding SEO. The other parts:

Questions people ask

Why is my React app not showing up on Google?
A default Vite and React app serves one nearly empty HTML file for every route and builds the page in the browser afterwards, so a crawler may receive nothing. Every route also shares one title, and there is no sitemap, robots.txt or structured data unless you added them.
Do I need to migrate off Vite and React to be findable?
Almost never, if the app already works. Four changes fix it inside the stack: build-time pre-rendering, per-route metadata, a generated sitemap, and a robots.txt. Migration is a real cost that solves the same problem for considerably more effort.
How do I know if my app is being pre-rendered already?
Fetch a deep route without a browser and look for that page's actual text in the response. If it is there, something is pre-rendering for you, which several platforms now do. If you see only an empty container, the crawler sees that too.
Why does every page in my app have the same title?
Because the title lives in the single index.html that Vite produces, and every route returns that same file. It is invisible while you use the site and shows up in search results, shared links, and to anything reading your pages programmatically.
When is pre-rendering not enough?
When your public pages show data that changes and you need that changing data indexed, such as listings or user-generated content. Pre-rendering freezes content at build time, so it suits fixed pages. In that case server-side rendering is worth its operational cost.
What is the most common mistake when adding pre-rendering?
Keeping two hand-maintained route lists, one for the pre-render script and one for the sitemap. They agree the day you write them and drift within a month. Derive both from the router configuration you already have, and always test a deep route rather than only the homepage.

Find out what your stack is currently serving

The free Get Found Check fetches your routes the way a crawler does and reports which of the four problems apply to your app.

Run the free check Have Licheo do it for you