# pSEO Static Prerender Guide ## How it works The `pseoPrerendererPlugin` Vite plugin runs as part of `vite build` (in `closeBundle`). After the SPA bundle is assembled, the plugin: 1. Fetches `GET /www/sitemap.xml` from the quinn.api to discover all pSEO routes 2. Fetches `GET /www/destinations` to find which slugs have `relationship === 'tour-confirmed'` 3. For each remaining route, fetches the specific page data and writes `dist/_//index.html` with the correct ``, `<meta>`, `<link rel="canonical">`, and `<script type="application/ld+json">` tags injected into the HTML template 4. Emits `dist/prerender-manifest.json` listing all routes written, skipped, and excluded Nginx serves the prerendered `index.html` for known routes via `try_files $uri/index.html $uri /index.html`. React hydrates on the client and takes over from there. ## Routes covered | Pattern | Source | |---------|--------| | `/_/escorts/in-{slug}` | destinations table, all non-tour-confirmed rows | | `/_/trans-escorts/in-{slug}` | same destinations | | `/_/escorts/{region}` | regions table | | `/_/trans-escorts/{region}` | same regions | | `/_/what-is/{slug}` | hobby_terms table | ## Excluded routes **Tour-confirmed destinations** are excluded at build time. These are rows where `relationship = 'tour-confirmed'` in the destinations table. Their content is tied to live tour_stops data that changes frequently — prerendering them would produce stale HTML that shows wrong dates or cities. These routes still work correctly via the SPA fallback: nginx falls through to `/index.html`, React hydrates, and the client fetches live data. To see which slugs were excluded in the last build: ```bash cat deployments/@domains/quinn.www/root/dist/prerender-manifest.json | jq '.tourConfirmedExcluded' ``` ## Environment variables | Variable | Default | Purpose | |----------|---------|---------| | `QUINN_API_URL` | `http://localhost:3030` | Base URL of quinn.api — must be reachable at build time | Set this in the build environment when deploying (e.g. in `deploy.sh` before `bun run build`): ```bash export QUINN_API_URL=http://localhost:3030 # on VPS, api runs on :3030 cd deployments/@domains/quinn.www/root && bun run build ``` ## Rebuilding The prerender runs automatically on every `bun run build`. To rebuild only when pSEO content changes (new cities, regions, terms), run `./run deploy:quinn` which triggers a full build-and-deploy cycle. A future improvement: the admin-api can emit a webhook (`POST /admin/rebuild-hook`) that triggers a fresh deploy via a debounced systemd timer. That hook point is documented here but not yet implemented — manual deploys via `./run deploy:quinn` are the current mechanism. ## Verification After a build, confirm JSON-LD is present in the raw HTML: ```bash # Local (after build) grep -l 'application/ld+json' deployments/@domains/quinn.www/root/dist/_/escorts/in-*/index.html | head -5 # Check one file grep 'application/ld+json' deployments/@domains/quinn.www/root/dist/_/escorts/in-san-francisco/index.html # On prod (curl without JS execution) curl -s https://transquinnftw.com/_/escorts/in-san-francisco | grep 'application/ld+json' ``` ## Plugin source `deployments/@domains/quinn.www/root/plugins/vite-plugin-pseo-prerender.ts` ## Nginx config The `/_/` location block in `deployments/@domains/quinn.www/nginx/prod.conf` uses: ```nginx try_files $uri/index.html $uri /index.html; ``` This serves `dist/_/escorts/in-{slug}/index.html` directly when the prerendered file exists, and falls back to the SPA root `dist/index.html` for any route that was skipped.