---
name: react-vite-seo-prerender
description: Make a React + Vite SPA visible to Google, link unfurlers, and AI crawlers (ChatGPT, Claude, Perplexity) by prerendering every public route to real HTML at build time, plus generating sitemap.xml, robots.txt, llms.txt, per-page OG/Twitter/canonical/JSON-LD tags, and optionally installing Google Tag Manager. Use when the user asks to fix SEO on a React/Vite site, get link previews working, get cited by AI assistants, add a sitemap, add llms.txt, or generally make a SPA crawlable without rewriting to Next.js/Remix/Astro.
---
# React + Vite SEO & AI-Visibility Pipeline
> **Disclaimer:** This skill was built and tested on Replit. The core (prerendering, sitemap, robots.txt, llms.txt, OG/JSON-LD tags) is portable, but the deployment glue — per-route rewrites in `artifact.toml`, the build-time rewrite guard, and the Puppeteer Chromium path notes — is Replit-specific. If you're shipping elsewhere (Vercel, Netlify, Cloudflare Pages, your own server), consult your host's docs for the equivalent routing config. No guarantees outside the original environment.
This skill turns a standard React + Vite SPA into a site that crawlers, link unfurlers, and AI assistants can actually read — *without* rewriting to a framework. The technique is build-time prerendering: snapshot every public route in headless Chromium, write a real `.html` file per URL, and ship that alongside the React bundle. Humans still get the SPA; bots get real HTML.
## When to use
- The user complains that their React/Vite site is "invisible to Google" or "shows a blank page in iMessage previews."
- The user wants AI assistants (ChatGPT, Claude, Perplexity, Google AI Overviews) to cite their site.
- The user asks for a sitemap, robots.txt, llms.txt, JSON-LD, Open Graph tags, or "SEO" on a SPA.
- The user wants to install Google Tag Manager / analytics on a SPA.
**Do not use** if the project is already on Next.js, Remix, Astro, or any other SSR/SSG framework — those handle this natively.
## What you build
By the end you will have:
1. A `useSeo()` React hook that sets per-page `
`, description, canonical URL, Open Graph, Twitter Card, robots index/noindex, and JSON-LD.
2. A `scripts/prerender.mjs` post-build step that serves the built `dist/` folder, crawls every route in headless Chromium, and writes a per-route `index.html` with the fully-rendered DOM.
3. Generated `sitemap.xml`, `robots.txt` (allowing major AI crawlers), and `llms.txt` at the site root.
4. `Organization` JSON-LD on the home page; `BlogPosting` JSON-LD on each blog post (if there's a blog).
5. Optional: Google Tag Manager snippet baked into `index.html` so it's present on every prerendered page.
## Step-by-step
### 1. Install dependencies
```bash
pnpm --filter add -D puppeteer sirv
```
Then in the **root** `package.json`, opt puppeteer's postinstall in (pnpm skips install scripts by default):
```json
{
"pnpm": {
"onlyBuiltDependencies": ["puppeteer"]
}
}
```
Re-run `pnpm install` to actually download Chromium.
### 2. Install system Chromium (works in BOTH dev and deploy)
Puppeteer's bundled Chrome can't launch in the Replit dev container (NixOS, missing libs at standard paths) AND can't launch in the deploy container (too stripped down, missing `libglib`, `libnspr4`, etc.). **Don't fight this** — install Chromium as a system Nix dependency, which is available in both environments because the deploy container inherits the same Nix config:
```js
await installSystemDependencies({ packages: ["chromium"] });
```
This adds `pkgs.chromium` to `replit.nix`. Then in the prerender script (next section), resolve the binary at runtime via `command -v chromium` — this works in both dev and deploy without hard-coding a `/nix/store/...` hash that changes across rebuilds.
**Optional belt-and-suspenders fallback:** `pnpm add @sparticuz/chromium` and have the prerender script fall back to it when `command -v chromium` returns nothing. Useful if you ever run this on a non-Replit minimal container (AWS Lambda, Vercel, etc.).
### 3. Add the `useSeo` hook
Create `src/lib/site.ts`:
```ts
export const SITE_URL = (import.meta.env.VITE_SITE_URL ?? "https://example.com").replace(/\/$/, "");
export const SITE_NAME = "Your Site Name";
export function absoluteUrl(path: string): string {
if (/^https?:\/\//i.test(path)) return path;
return `${SITE_URL}${path.startsWith("/") ? path : "/" + path}`;
}
```
Create `src/lib/seo.ts` with a `useSeo({ title, description, canonicalPath, ogImage, noindex, jsonLd })` hook that manages tags in `document.head` via `useEffect`. Key responsibilities:
- Set `` and ``.
- Set `` and ``.
- Set `` to `absoluteUrl(canonicalPath)`.
- Set `` when `noindex: true`.
- Inject/replace one `