---
title: "Make a Replit Website Readable to Google and ChatGPT"
description: "A Replit site built with React, Vite and wouter serves crawlers an empty page. Here's a tested fix: build each page's HTML ahead of time, with the code."
canonical: https://trychad.ai/ai-built-websites/replit/
---

[Home](https://trychad.ai/)/[AI-built websites](https://trychad.ai/ai-built-websites/)/Replit

# Make a Replit Website
Readable to Google and ChatGPT

A Replit site built with React, Vite and wouter serves crawlers an empty page, with the same title on every page. Here's a tested fix: build each page's HTML ahead of time. It takes a small amount of code, and it's all below.

TestedReplitwouterPre-rendering

0

words of body text in the HTML before

312

words on the homepage after (367 and 205 on the other pages)

404

for a made-up URL after, on a static server (the live site gave 200)

0

console errors in the browser after

From our test of one Replit project, 16 October 2026.

## What we tested

We asked Replit for a three-page café website, published it, exported the code, and measured both. Then we applied the steps below and checked the result.

- **The published site:** 0 words of body text in the HTML on every page (309, 364 and 202 once JavaScript ran), the same title and description on every page, no canonical tag or structured data, and 200 for a made-up URL.
- **After the fix:** 312, 367 and 205 words in the HTML, a different title on each page, one description and one canonical tag each, a real 404 page, and a build that exits in about three seconds.
- **Versions:** Node 24, React 19.3, Vite 8.3 and wouter 3.13. Replit manages its own versions, so yours may differ.

**What we haven't tested.** Replit's agent making these changes, Replit's deployment and hosting of the result (including what a made-up URL returns there), and editing outside Replit. This is one project from one prompt.

## Before you start

- **Try Replit's SEO Agent first.** [Announced in June 2026](https://replit.com/blog/seo-agent), it scans a published app, ranks what it finds, and can recommend server-side rendering or static generation. We haven't tested it. If it fixes the empty page, you don't need the steps below. The 10-second check tells you.
- Confirm the problem with the [10-second check](https://trychad.ai/ai-built-websites/#check). The page source should show an empty `<div id="root">`.
- Check your router. If your code imports from `wouter`, this guide applies. For React Router, see [the React and Vite guide](https://trychad.ai/ai-built-websites/react-vite-pre-rendering/).
- Search your code for `document.title`. If titles are set in a `useEffect`, that's why every page shows the same title until JavaScript runs.
- **Make the changes inside Replit.** The exported copy won't build on its own: it relies on Replit's workspace for its dependencies and base configuration. We had to replace those to test it.

## The steps

### 1. Put titles and descriptions in one place

The build needs to know each page's title and description without running the page. Move them into one object that both the page and the build can read.

```
// src/App.tsx
export const pageMeta: Record<string, { title: string; description: string }> = {
  '/': { title: 'Harbour Street Coffee | Falmouth, Cornwall', description: 'A warm welcome, carefully made coffee and generous local food.' },
  '/menu': { title: 'The Menu | Harbour Street Coffee, Falmouth', description: 'Coffee, tea, baked things and a little more, with prices.' },
  '/contact': { title: 'Find Us | Harbour Street Coffee, Falmouth', description: 'Our address, opening hours, phone number and email.' },
  '/404': { title: 'Page not found | Harbour Street Coffee', description: 'Sorry, we could not find that page.' },
};

// then, in each page, replace the inline text with the shared values:
<PageMeta {...pageMeta['/menu']} />
```

### 2. Let the router render a given page on the server

wouter's `Router` accepts an `ssrPath`. Pass it through from your `App` component.

```
// src/App.tsx
function App({ ssrPath }: { ssrPath?: string }) {
  return (
    <QueryClientProvider client={queryClient}>
      <TooltipProvider>
        <WouterRouter base={import.meta.env.BASE_URL.replace(/\/$/, '')} ssrPath={ssrPath}>
```

### 3. Attach to the pre-rendered page in the browser

If the page already has content, hydrate it. Otherwise render from scratch, so the site still works before the first build.

```
import { createRoot, hydrateRoot } from 'react-dom/client';

import App from './App';
import { ErrorBoundary } from '@/components/error-boundary';

import './index.css';

const root = document.getElementById('root')!;
const app = (
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);
const options = {
  // Keeps caught errors off reportError(), which would raise the dev overlay.
  onCaughtError: (error: unknown, errorInfo: { componentStack?: string }) => {
    console.error(error, errorInfo.componentStack);
  },
};

// A prerendered page already has content in #root: attach to it. Otherwise render from scratch.
if (root.hasChildNodes()) hydrateRoot(root, app, options);
else createRoot(root, options).render(app);
```

### 4. Add a function that renders a page

```
import { renderToString } from 'react-dom/server';
import App, { pageMeta } from './App';

const SITE = 'https://harbourstreetcoffee.example';
const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');

// Returns the page's HTML and its head tags. Runs at build time, in Node.
export function render(url: string) {
  const meta = pageMeta[url] ?? pageMeta['/404'];
  const head = [
    `<title>${esc(meta.title)}</title>`,
    `<meta name="description" content="${esc(meta.description)}" />`,
    `<meta property="og:title" content="${esc(meta.title)}" />`,
    `<meta property="og:description" content="${esc(meta.description)}" />`,
    `<link rel="canonical" href="${SITE}${url}" />`,
  ].join('\n    ');
  return { html: renderToString(<App ssrPath={url} />), head };
}
```

### 5. Add a script that writes one file per page

It runs after the build, takes the built `index.html`, and writes a copy for each page with the content and head tags filled in. If you add a page, add its path to the list.

```
// Writes one HTML file per page into dist/public, using the app's own code.
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';

const { render } = await import(pathToFileURL(path.resolve('dist/server/prerender.js')).href);
const out = path.resolve('dist/public');
const template = fs.readFileSync(path.join(out, 'index.html'), 'utf8');

for (const url of ['/', '/menu', '/contact', '/404']) {
  const { html, head } = render(url);
  const page = template
    .replace('<div id="root"></div>', `<div id="root">${html}</div>`)
    .replace('</head>', `    ${head}\n  </head>`);
  const file = url === '/' ? 'index.html' : path.join(url.slice(1), 'index.html');
  fs.mkdirSync(path.dirname(path.join(out, file)), { recursive: true });
  fs.writeFileSync(path.join(out, file), page);
  console.log('prerendered', url);
}
// Many static hosts look for a root 404.html
fs.copyFileSync(path.join(out, '404/index.html'), path.join(out, '404.html'));
```

### 6. Change the build command

The first part is your normal build. The second builds the render function for Node. The third runs the script.

```
// package.json
"build": "vite build --config vite.config.ts && vite build --config vite.config.ts --ssr src/prerender.tsx --outDir dist/server && node scripts/prerender.mjs"
```

### 7. Remove the static tags from index.html

**This is easy to miss.** If the title, description and share tags stay in `index.html`, every page ends up with two of each. The build now writes them for each page.

```
<!-- index.html: delete these four lines. The build now writes them for each page. -->
<title>Harbour Street Coffee | Falmouth, Cornwall</title>
<meta name="description" content="..." />
<meta property="og:title" content="..." />
<meta property="og:description" content="..." />
```

### 8. Add a sitemap and point robots.txt at it

Put `sitemap.xml` in the `public` folder, and add the `Sitemap:` line to `robots.txt`.

```
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://yoursite.com/</loc></url>
  <url><loc>https://yoursite.com/menu</loc></url>
  <url><loc>https://yoursite.com/contact</loc></url>
</urlset>
```

```
User-agent: *
Allow: /

Sitemap: https://yoursite.com/sitemap.xml
```

## Check it worked

```
# Outside Replit, set the two variables its config expects
PORT=3000 BASE_PATH=/ npm run build
npx serve dist/public
```

Open the address it prints, then:

- View Page Source on each page. Your text should be there, with a different title on each page, one description, and a canonical link.
- Run these two commands. The first should print a match. The second should say 404.

```
curl -s http://localhost:3000/menu | grep -i "a word from the menu"
curl -I http://localhost:3000/no-such-page
```

- Click a link in the browser. The page and its title should change with no full reload, and the console should show no errors.
- Open `/sitemap.xml`. It should be XML. A status code alone isn't enough: on our live Replit site, `/sitemap.xml` and `/llms.txt` both returned 200, but the response was the homepage (content type `text/html`).

## Problems we hit

- **A pre-render plugin that never finished.** We first tried `vite-prerender-plugin`. It generated the pages, but the build never exited, which would stall a deployment. The cause was React's server renderer being bundled for a browser-like environment, which keeps Node running. Importing `react-dom/server.node` instead made the build fail. The script above avoids both.
- **Two titles per page.** See step 7.
- **Titles set in `useEffect` aren't in the HTML.** That was the original problem. They now come from the shared object at build time, and the component still updates them in the browser.
- **Made-up URLs.** The script copies the 404 page to `404.html`, which a static file server returned with a 404 status. Hosts differ, and we haven't tested Replit's.

## Will AI crawlers read it now?

The content is in the HTML, which is what a crawler that doesn't run JavaScript needs. We haven't run an AI-answer test on the result, and being readable doesn't guarantee a recommendation. The background is in [Is your AI-built website invisible?](https://trychad.ai/ai-built-websites/)

## Frequently Asked Questions

### Do I have to rewrite my pages?

No. In our test the content of the pages didn't change. We moved the titles and descriptions into one shared object, let the router render a given page on the server, changed the entry file and the build command, and added two small files.

### Can Replit's agent make these changes?

We haven't tested that. If you ask it, give it these steps, then check the result with the 10-second check and the commands below.

### Will the interactive parts still work?

In our test, yes. There were no errors in the browser console, and clicking a link still changed the page and its title without a reload.

### Why not use a pre-render plugin?

We tried one. It generated the pages, but the build never exited, which would stall a deployment. See the problems we hit, below.

## Sources

- [wouter](https://github.com/molefrog/wouter), the router the project we tested uses
- [Google Search Central: Understand JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)
- [Replit: SEO Agent](https://replit.com/blog/seo-agent)

## Check the
technical side

Chad's free scan checks how crawlers read your site, and gives every technical issue with the fix.

[Run the free scan →](https://trychad.ai/#cta)

### Related

[AI-built website launch checklistTen checks before you launch or hand over a site.](https://trychad.ai/ai-built-websites/launch-checklist/)[What a new Lovable site is missingWhat a server-rendered Lovable site still lacks, with tested code.](https://trychad.ai/ai-built-websites/lovable/)[Is your AI-built website invisible?The 10-second check, and what we found.](https://trychad.ai/ai-built-websites/)
