Skip to content

SEO: prerender public pages, real 404s, 301 brand pages, built sitemap - #146

Merged
Programcode merged 3 commits into
mainfrom
seo/fixes
Oct 16, 2026
Merged

Programcode merged 3 commits into
mainfrom
seo/fixes

Conversation

@Programcode

@Programcode Programcode commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Fixes the SEO problems from the audit: thin JavaScript-only public pages, soft 404s, seven near-duplicate brand pages, a hand-edited sitemap stamped with one date, and structured data copied onto every route.

What changed

Prerendered public pages. After vite build, scripts/prerender.mjs loads src/prerender/entry.tsx through Vite's SSR loader and renders /, /terms, /privacy and the 404 page in Node, in English and signed out. Each gets real body HTML plus its own <title>, description, canonical URL and og:*/twitter:* tags from src/lib/seo/pages.ts. The landing page now reads the same with JavaScript off. I used neither vite-react-ssg (it would replace the router) nor a Playwright pass (the Vercel build would need Chromium and its system libraries). The only app change Node needed was moving AuthContext/useAuth into auth-context.ts, so public pages no longer import the Supabase client.

On load, mountApp keeps the prerendered page on screen until that page's chunk has loaded, then replaces it in one flushSync render. That avoids both a skeleton flash and hydration-mismatch errors for Spanish and Hungarian visitors. On that first render the landing Reveal skips its entrance animation, so prerendered content never fades out and back in. Later visits animate as before. usePageHead keeps the title, description, canonical URL and robots tag right during client navigation, and applyAppLanguage no longer overwrites the title.

Real 404s. vercel.json rewrites only /app, /app/*, /login, /share/* and /expocit/sources to app-shell.html, the untouched built shell. Every other unknown path gets 404.html with a 404 status, noindex and no canonical. Rendered client-side, NotFound does the same.

Brand pages. /debatelab, /debate-lab, /debatelab-ai, /debatelab-app, /what-is-debatelab, /debatelab-by-programcode and /about-debatelab now 301 to /, and their files are gone. The guide generator (scripts/generate_seo_content.py) no longer emits them. It now calls the brand "DebateLab by Pi-Stack" in the nav, footer, og:site_name and JSON-LD publisher, and I regenerated the 49 guide pages from it. llms.txt, humans.txt and the footer copyright say the same.

Sitemap. scripts/sitemap.mjs builds dist/sitemap.xml from dist/. It lists every page that names itself as canonical and isn't noindex, which comes to 52 URLs. Each <lastmod> is the dateModified the page declares in its own JSON-LD:

  • the guide pages' fixed CONTENT_DATE (2026-07-06), which the generator no longer replaces with today's date;
  • the "last updated" dates on Terms (2026-05-01) and Privacy (2026-10-01);
  • the date of the last commit to the landing sources.

In a shallow clone where that date can't be known, the landing page goes into the sitemap without <lastmod> rather than with an invented date. public/sitemap.xml is deleted.

JSON-LD on / only. The landing page carries Organization (Pi-Stack), WebSite, SoftwareApplication (price from the locale file, operatingSystem: Web) and WebPage nodes. The app shell carries none. I dropped the old SearchAction, because /ai-debate-practice?q= is not a search endpoint.

Verification

  • npm run verify passed locally at this head: docs, lint/knip, typecheck, unit tests (282), collaboration (30), edge (334), build, the service-worker offline test, and Playwright smoke (30 passed).
  • Served dist/ with Vercel's file-first routing and drove it in Chromium:
    • /, /terms and /privacy return 200 with their own titles and canonicals;
    • /nope returns 404 with noindex and no canonical;
    • /app and /app/settings get the shell;
    • /debatelab and /debatelab-by-programcode/ return 301 to /.
  • During load I saw no route skeleton, no empty root and no hidden hero, and no console errors apart from the Vercel-only analytics scripts.
  • Spanish visitors get the Spanish page after load. Client navigation from / to /terms to /login updates the title and canonical.
  • The JavaScript-off screenshot of / matches the hydrated page.
  • I'll repeat the routing checks against the Vercel preview in a comment below.

The audit file named in the task (/workspace/debatelab-seo-audit.md) was not on the VPS. This PR follows the five fixes listed in the task.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Public pages, including the landing, Terms, Privacy, and not-found pages, are now available as prerendered pages with page-specific metadata.
    • App and login routes are served separately from public pages. Unmatched paths show a 404, while retired brand pages redirect to the home page.
    • The sitemap is generated from canonical, indexable pages.
  • Updates

    • Site branding now reads “DebateLab by Pi-Stack,” with refreshed navigation links across public guides.

The landing page, /terms and /privacy now ship as real HTML with their own
title, description, canonical URL and JSON-LD. scripts/prerender.mjs renders
them in Node through Vite after the build, plus a 404 page with noindex and
no canonical. The client keeps the prerendered page on screen until its
module loads, then replaces it with the same page in one render.

vercel.json rewrites only app routes to app-shell.html, so any other path
gets 404.html with a 404 status. The seven brand pages (/debatelab,
/debatelab-by-programcode and the rest) redirect 301 to /, and the guide
pages, llms.txt and footer say "DebateLab by Pi-Stack".

sitemap.xml is now built from dist/: every self-canonical, indexable page,
with the dateModified each page declares as its lastmod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 16:13
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Oct 16, 2026 4:17pm UTC
debatelab-staging Ready Ready Preview Oct 16, 2026 4:17pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Programcode

Copy link
Copy Markdown
Owner Author

Local screenshots of the built output: the landing page hydrated, the same page with JavaScript off, and the 404 page.

Landing, hydrated

Landing, JavaScript off

404 page

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Free
  • Run ID: ebfbe339-fee8-4f25-b440-5e32276919fa
📥 Commits

Reviewing files that changed from the base of the PR and between ffffd5f and 095f5f0.

📒 Files selected for processing: 4
  • docs/operations.md
  • src/lib/seo/use-page-head.ts
  • src/test/vercel-deployment-config.test.ts
  • vercel.json
ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Free
  • Run ID: 6a6f03e8-21c9-4b6a-a843-9fd725d13cf6
📥 Commits

Reviewing files that changed from the base of the PR and between d733459 and ffffd5f.

📒 Files selected for processing: 90
  • README.md
  • docs/architecture.md
  • docs/operations.md
  • index.html
  • knip.json
  • package.json
  • public/about-debatelab/index.html
  • public/ai-debate-case-generator/index.html
  • public/ai-debate-coach/index.html
  • public/ai-debate-evidence-research/index.html
  • public/ai-debate-practice/index.html
  • public/ai-rebuttal-generator/index.html
  • public/ai-tools-for-debate-students/index.html
  • public/ai-vs-traditional-debate-prep/index.html
  • public/argument-flowchart/index.html
  • public/best-ai-debate-tools/index.html
  • public/british-parliamentary-debate/index.html
  • public/congress-debate/index.html
  • public/debate-argument-flowchart-tool/index.html
  • public/debate-block-file-guide/index.html
  • public/debate-case-generator/index.html
  • public/debate-clash-points/index.html
  • public/debate-collapsing-strategy/index.html
  • public/debate-cross-examination-tips/index.html
  • public/debate-evidence-finder/index.html
  • public/debate-evidence-verification/index.html
  • public/debate-fallacies/index.html
  • public/debate-impact-calculus/index.html
  • public/debate-judge-adaptation/index.html
  • public/debate-lab/index.html
  • public/debate-motion-analysis/index.html
  • public/debate-note-taking-flowing/index.html
  • public/debate-prep-time-management/index.html
  • public/debate-pronunciation-and-delivery/index.html
  • public/debate-rebuttal-practice/index.html
  • public/debate-speech-structure/index.html
  • public/debate-topics-for-students/index.html
  • public/debate-weighing-guide/index.html
  • public/debatelab-ai/index.html
  • public/debatelab-alternatives/index.html
  • public/debatelab-app/index.html
  • public/debatelab-by-programcode/index.html
  • public/debatelab-pricing/index.html
  • public/debatelab-vs-chatgpt/index.html
  • public/debatelab-vs-debateclub/index.html
  • public/debatelab-vs-windebate/index.html
  • public/debatelab/index.html
  • public/how-to-cross-examine-in-debate/index.html
  • public/how-to-flow-in-debate/index.html
  • public/how-to-practice-rebuttals/index.html
  • public/how-to-prepare-for-a-debate-tournament/index.html
  • public/how-to-weigh-in-debate/index.html
  • public/how-to-write-a-debate-case/index.html
  • public/how-to-write-a-rebuttal-speech/index.html
  • public/humans.txt
  • public/lincoln-douglas-debate/index.html
  • public/llms.txt
  • public/online-debate-timer-tool/index.html
  • public/online-debate-timer/index.html
  • public/policy-debate/index.html
  • public/public-forum-debate/index.html
  • public/sitemap.xml
  • public/what-is-debatelab/index.html
  • public/world-schools-debate/index.html
  • public/wsdc-debate-practice/index.html
  • scripts/generate_seo_content.py
  • scripts/prerender.mjs
  • scripts/seo_hour_watch.py
  • scripts/sitemap.mjs
  • src/App.tsx
  • src/components/landing/Reveal.tsx
  • src/components/layout/PublicPageShell.tsx
  • src/i18n.ts
  • src/lib/route-modules.ts
  • src/lib/seo/pages.ts
  • src/lib/seo/use-page-head.ts
  • src/lib/supabase/AuthProvider.tsx
  • src/lib/supabase/auth-context.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/main.tsx
  • src/pages/LandingPage.tsx
  • src/pages/NotFound.tsx
  • src/pages/PrivacyPage.tsx
  • src/pages/TermsPage.tsx
  • src/prerender/entry.tsx
  • src/test/vercel-deployment-config.test.ts
  • vercel.json
  • vite.config.ts
💤 Files with no reviewable changes: 9
  • public/sitemap.xml
  • public/debatelab-ai/index.html
  • public/what-is-debatelab/index.html
  • public/about-debatelab/index.html
  • public/debatelab-app/index.html
  • public/debatelab-by-programcode/index.html
  • public/debatelab/index.html
  • public/debate-lab/index.html
  • src/i18n.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The build now prerenders selected public routes, generates a sitemap from built pages, and routes app paths to a separate app shell. Public-page branding, metadata, navigation, and deployment documentation also change.

Changes: Public Page Delivery

Layer / File(s) Summary
Public page identity and content
public/*/index.html, scripts/generate_seo_content.py, public/humans.txt, public/llms.txt, src/locales/*, scripts/seo_hour_watch.py
Public pages and generated SEO content use the Pi-Stack publisher identity and revised site branding, navigation, and related-guide links. Several brand-focused landing pages are removed.
Prerendered page rendering and hydration
index.html, src/prerender/entry.tsx, src/lib/seo/*, src/lib/route-modules.ts, src/App.tsx, src/main.tsx, src/pages/*, src/lib/supabase/*, src/components/*, src/i18n.ts
The landing, terms, privacy, and fallback routes receive prerendered HTML and page-specific metadata. Client startup preloads a matching route before rendering.
Build output and deployment routing
scripts/prerender.mjs, scripts/sitemap.mjs, package.json, knip.json, vite.config.ts, vercel.json, src/test/vercel-deployment-config.test.ts, README.md, docs/*
The build creates prerendered pages and a sitemap. Vercel uses explicit app-shell rewrites, retired-path redirects, and cache and indexing headers; deployment documentation and tests describe these rules.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Build as Build script
  participant Prerender as scripts/prerender.mjs
  participant Vite as Vite SSR server
  participant Entry as prerenderPages
  participant Browser as Client application
  Build->>Prerender: Run prerender script
  Prerender->>Vite: Create production SSR server
  Vite->>Entry: Load prerender entry
  Entry-->>Prerender: Return rendered page records
  Prerender-->>Build: Write page HTML and app-shell.html
  Browser->>Browser: Preload matching route and render
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode

Copy link
Copy Markdown
Owner Author

GitHub Actions did not run on ffffd5f. Every job stopped in 2-3 s with "The job was not started because recent account payments have failed or your spending limit needs to be increased" (run 38930216979). That is account billing, not this code. In its place, npm run verify passed locally at ffffd5f:

  • docs, lint/knip, typecheck
  • vitest: 282 tests passed
  • collaboration: 30 tests passed
  • edge: 334 tests passed
  • build: prerender wrote 4 pages; the sitemap has 52 URLs
  • service-worker offline test passed
  • Playwright desktop smoke: 30 passed

This is a local pass, not a CI pass.

On the Vercel preview, /debatelab/ fell through to the 404 page: Vercel
matched the redirect source without its optional trailing segment. The
bare, trailing-slash and subpath forms now each have their own source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Checked the debatelab-staging Vercel preview at 8446117 (https://debatelab-staging-git-seo-fixes-adam-gaborjanis-projects.vercel.app) with curl and Chromium.

  • /, /terms, /privacy: 200, each with its own <title> and canonical URL. /terms/ also returns 200.
  • /nope and /some/deep/path: 404, serving 404.html with noindex and no canonical.
  • /app, /app/settings, /login, /share/abc, /expocit/sources: 200 with the app shell (title DebateLab, no canonical).
  • The seven brand pages return 301 to / in their bare, trailing-slash and subpath forms (/debatelab, /debatelab/, /debatelab/x). /debatelabs returns 404. The first preview returned 404 for /debatelab/; 8446117 fixes that.
  • /sitemap.xml lists 52 URLs. The landing page's lastmod is 2026-10-16, the date of its latest source commit. The guide pages show 2026-07-06; Terms and Privacy show their own "last updated" dates.
  • In Chromium, / and /terms load with no console errors and no route skeleton, and the prerendered page is replaced in place.

Preview deployments add X-Robots-Tag: noindex to every response. That header comes from Vercel and does not reach production.

Review found that /app/settings/, /login/ and /share/abc/ now 404: the old
catch-all rewrite absorbed them, and the new app-route rewrites match
exactly. trailingSlash: false redirects them to the slashless path first.

The JSON-LD and og:url the build writes describe only the page first
loaded, so usePageHead removes them when that page unmounts during client
navigation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review, round 1 (ffffd5f), two findings:

  1. Medium: trailing-slash app URLs return 404. Fixed in 095f5f0, after confirming on the preview that /app/, /app/settings/, /login/ and /share/abc/ all returned 404. The old catch-all rewrite had absorbed those URLs, and the new app-route sources match exactly. vercel.json now sets trailingSlash: false, so Vercel sends a 308 to the slashless path before routing. The /debatelab/ part of this finding was already fixed in 8446117. Test: the vercel-deployment-config test now asserts trailingSlash: false. I'll recheck these paths on the new preview.
  2. Low: JSON-LD from the first page survives client navigation. Fixed in 095f5f0. When its page unmounts, usePageHead removes the prerendered application/ld+json and og:url. Crawlers load each URL fresh, so this only matters for consistency.

@Programcode

Copy link
Copy Markdown
Owner Author

Codex review, round 2 (095f5f0): the round 1 fixes hold, and there is one new finding.

  • Low: after client navigation, the og:* and twitter:* title and description still describe the first page loaded. Valid, accepted as is. Social scrapers (Facebook, X, LinkedIn, Slack) don't run JavaScript. They read the HTML served for each URL, and the build writes the right tags into each page. Google renders JavaScript, but it loads each URL fresh, so it never sees the DOM left behind by client navigation. Updating those tags in the browser would add code that nothing reads.

@Programcode
Programcode merged commit 3f4f22f into main Oct 16, 2026
3 of 11 checks passed
@Programcode
Programcode deleted the seo/fixes branch October 16, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants