You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 71c083e
Browse filesBrowse the repository at this point in the historyBrowse files
Make the public pages readable for AI answer engines (AEO) (#156)
* Make the public pages readable for AI answer engines
Implements the 11 answer-engine recommendations across the landing page,
the 36 guide pages, robots.txt and llms.txt.
- Every indexed page except /terms and /privacy opens with the reader's
main question as its first H2 (#short-answer) and an answer of at most
100 words that says when another tool fits better. Guide answers live
in scripts/seo_answers.py and use only facts their page already states.
- The landing H1 slogan becomes "Prepare Cases and Practice Rounds with
AI", and the subheadline is the definition: "DebateLab is an AI debate
prep and practice app for students and competitive debaters that ...".
The same sentence closes every guide and opens llms.txt.
- Prices, plan facts and competitor facts carry "As of October 2026",
driven by factsCheckedOn in src/config/site.json.
- The landing FAQ uses native <details>, so its answers are in the
prerendered HTML. Radix rendered closed answers as empty, which kept
them out of the raw HTML; the accordion and its dependency are removed.
- The pricing, WinDebate and DebateClub pages are rewritten from facts
checked on the vendors' own sites. DebateClub turns out to target
workplace conversations, not competitive debate, and the page now says so.
- Guides get header/main/article/aside/footer landmarks, a breadcrumb
nav, visible Published and Updated dates that match their JSON-LD,
datePublished, and the lead Q&A in FAQPage. JSON-LD no longer passes
through html.escape, which had put & and ' into two pages' data.
- Landing JSON-LD adds both offers (trial and Pro), featureList,
inLanguage and a FAQPage built from the visible questions.
- robots.txt names meta-webindexer and the other AI crawlers in their own
group, which repeats the private-route rules because a named group
replaces *. llms.txt is now generated with the key facts and every guide.
- scripts/check-site.mjs runs in the build and fails on broken internal
links, fragments, sitemap or llms.txt URLs (applying the vercel.json
redirects), bad JSON-LD, a missing question H2, images without alt,
unnamed buttons and unlabelled form fields.
- Decorative landing icons are aria-hidden; the mock-UI title in the hero
preview is no longer an H3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Accept implicit labels and bare wildcard prefixes in check-site
Codex review r1 on #156 found three checker gaps:
- a field nested in its <label> was reported as unlabelled;
- /share/:path* did not match /share, unlike Vercel;
- any non-empty aria-labelledby counted as a name, even pointing at no element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve accessible names in check-site the way browsers do
Codex review r2 on #156:
- labelledby text now includes nested elements, not just text before the
first closing tag;
- a labelledby list names the element if any referenced element has text;
- implicit labels count by field position, so an identical unlabelled
input elsewhere on the page is still caught;
- aria-label="" no longer passes by matching into the next attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Exempt the /tools pages from the #short-answer check until they get one
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/operations.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,16 +50,20 @@ Vercel builds and serves the frontend from the Git repository. A push to `main`
50
50
-`noindex` headers for `/app`, `/login`, `/share` and `/embed/debate-timer`;
51
51
- Content Security Policy and the other response security headers, including the `Permissions-Policy` that allows the microphone for the app's own origin only. Every path except `/embed/debate-timer` refuses to be framed (`X-Frame-Options: DENY` and `frame-ancestors 'none'`). `/embed/debate-timer` allows any site to frame it and gets its own CSP, which permits only same-origin scripts and requests. The two rules use complementary sources (`/((?!embed/debate-timer$).*)` and `/embed/debate-timer`) so no path gets both, a new embed needs its path added to both, and `src/test/vercel-deployment-config.test.ts` fails if two matching rules ever set the same header.
52
52
53
-
After `vite build`, [`scripts/prerender.mjs`](../scripts/prerender.mjs) renders the landing page, `/terms`, `/privacy`, and the 404 page in Node through [`src/prerender/entry.tsx`](../src/prerender/entry.tsx). It writes them to `dist/index.html`, `dist/terms/index.html`, `dist/privacy/index.html`, and `dist/404.html`, each with its own title, description, and canonical URL from [`src/lib/seo/pages.ts`](../src/lib/seo/pages.ts); the landing page also carries the Organizationand SoftwareApplication JSON-LD. The built shell is kept as `dist/app-shell.html`. On load, the client replaces the prerendered HTML with the same page. A new public SPA page needs an entry in `PAGE_HEADS`, in the prerender entry, and a route kept out of the 404.
53
+
After `vite build`, [`scripts/prerender.mjs`](../scripts/prerender.mjs) renders the landing page, `/terms`, `/privacy`, and the 404 page in Node through [`src/prerender/entry.tsx`](../src/prerender/entry.tsx). It writes them to `dist/index.html`, `dist/terms/index.html`, `dist/privacy/index.html`, and `dist/404.html`, each with its own title, description, and canonical URL from [`src/lib/seo/pages.ts`](../src/lib/seo/pages.ts); the landing page also carries the Organization, SoftwareApplication (with the trial and Pro offers) and FAQPage JSON-LD. The built shell is kept as `dist/app-shell.html`. On load, the client replaces the prerendered HTML with the same page. A new public SPA page needs an entry in `PAGE_HEADS`, in the prerender entry, and a route kept out of the 404.
54
54
55
55
The free tools under `/tools` ([`src/pages/tools/`](../src/pages/tools/)) are prerendered the same way, listed in [`src/prerender/tool-pages.tsx`](../src/prerender/tool-pages.tsx) with heads in [`src/lib/seo/tool-pages.ts`](../src/lib/seo/tool-pages.ts). They render their own JSON-LD in the page body, so the dev server and the prerendered HTML carry the same structured data, and their `dateModified` is `TOOL_PAGES_MODIFIED` in that file: bump it with a substantive content change. Their speech times come from [`src/lib/free-tools/formats.ts`](../src/lib/free-tools/formats.ts), which cites a league source per format.
56
56
57
57
`/embed/debate-timer` is a second Vite HTML entry ([`embed/debate-timer/index.html`](../embed/debate-timer/index.html)), not an app route: it loads only the timer, never the app shell, Jam, analytics or the service worker, so a site that frames it sees no third-party request. It is `noindex` with its canonical URL on `/tools/debate-timer`, and the service worker leaves `/embed/` alone.
58
58
59
59
[`scripts/sitemap.mjs`](../scripts/sitemap.mjs) then writes `dist/sitemap.xml` from every page in `dist/` that names itself as canonical and is not `noindex`. Each `<lastmod>` is the `dateModified` the page declares in its JSON-LD: the guide pages' `CONTENT_DATE` in [`scripts/generate_seo_content.py`](../scripts/generate_seo_content.py), the legal pages' "last updated" dates, the tool pages' `TOOL_PAGES_MODIFIED`, and the landing page's last source commit. A page without a date is listed without `<lastmod>`.
60
60
61
+
[`scripts/check-site.mjs`](../scripts/check-site.mjs) runs next and fails the build when an internal link, a `#fragment`, a sitemap URL or an `llms.txt` URL does not resolve (it applies the `vercel.json` redirects and rewrites), when JSON-LD does not parse or carries HTML entities, when an indexed page other than `/terms`, `/privacy` and the `/tools` pages lacks its question H2 (`#short-answer`) as the first H2, or when an image has no alt text, a button has no name or a form field has no label.
62
+
61
63
The guide pages in `public/<slug>/index.html` are generated, not hand-edited: run `python3 scripts/generate_seo_content.py` and commit its output. Short pages live in its `PAGES` list; pages with long-form copy, their own highlight-card descriptions, and their own FAQs (which also feed the page's FAQPage JSON-LD) live in [`scripts/seo_guides.py`](../scripts/seo_guides.py). Keep `PAGES` in order, because each page's related-guide links are picked by position. A guide entry can set its own `modified` date, which overrides `CONTENT_DATE` for that page alone.
62
64
65
+
Every guide opens with the reader's main question as its first H2 and an answer of at most 100 words, kept in [`scripts/seo_answers.py`](../scripts/seo_answers.py); the generator refuses a page without one or with a longer answer. Each guide shows visible "Published" and "Updated" dates (`PUBLISHED_DATE` or a `published` key, and the `modified` date) that match its Article JSON-LD. Prices and plan facts are stated "as of" the month in `factsCheckedOn` in `src/config/site.json`; move that date when you recheck them, and the generated About line on every guide, `llms.txt` and the landing page follow. Copy in `seo_guides.py` and `seo_answers.py` that names the month itself (prices and competitor facts) needs the same edit: search those files for "As of". The generator also writes `public/llms.txt`, the plain summary for AI agents. `public/robots.txt` allows AI crawlers by name in their own group, which repeats the private-route rules because a crawler that matches a named group ignores `*`.
66
+
63
67
To retire a guide, delete its `PAGES` entry and its `public/<slug>/` folder (the generator never deletes), add its redirects to `vercel.json`, and repoint any link to it. Two guides that answer the same search should become one page, not two thin ones.
64
68
65
69
The site origin, product name, brand line, and organization name live in [`src/config/site.json`](../src/config/site.json). The app's head tags, the prerender and sitemap scripts, and the guide generator all read it, and guide copy names the product with a `{product}` token. Do not hardcode those strings in new code or copy.
Copy file name to clipboardExpand all lines: docs/testing.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -102,7 +102,7 @@ This sends one tiny typed choice to OpenRouter's Decisions endpoint using the pi
102
102
npm run build
103
103
```
104
104
105
-
This proves the Vite production bundle compiles and then walks the emitted Vite manifest to reject feature-only PDF, DOCX, chart, ReactFlow, export, and Admin modules from the static `index.html` dependency graph. It does not prove authentication, database behavior, provider access, or production deployment.
105
+
This proves the Vite production bundle compiles, prerenders the public pages, checks the built site's links, fragments, JSON-LD and basic accessibility with `scripts/check-site.mjs`, and then walks the emitted Vite manifest to reject feature-only PDF, DOCX, chart, ReactFlow, export, and Admin modules from the static `index.html` dependency graph. It does not prove authentication, database behavior, provider access, or production deployment.
Copy file name to clipboardExpand all lines: index.html
+3-3Lines changed: 3 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -9,7 +9,7 @@
9
9
<!-- The shell for app routes. scripts/prerender.mjs writes the public pages
10
10
from it with their own title, description, canonical URL and JSON-LD. -->
11
11
<title>DebateLab</title>
12
-
<metaname="description" content="DebateLab by Pi-Stack helps students and competitive debaters practice with AI opponents, case generation, rebuttal drills, evidence leads, coaching, timers, and argument flowcharts." />
12
+
<metaname="description" content="DebateLab is an AI debate prep and practice app for students and competitive debaters: cases, rebuttals, evidence, coachingand timed practice rounds against an AI opponent." />
<metaproperty="og:site_name" content="DebateLab by Pi-Stack" />
23
23
<metaproperty="og:title" content="DebateLab by Pi-Stack | AI Debate Prep, Cases & Rebuttals" />
24
-
<metaproperty="og:description" content="Practice debate with AI opponents, case generation, rebuttal drills, evidence leads, coaching, timers, and argument flowcharts built for competitive debaters." />
24
+
<metaproperty="og:description" content="DebateLab is an AI debate prep and practice app for students and competitive debaters: cases, rebuttals, evidence, coaching and timed practice rounds against an AI opponent." />
<metaname="twitter:title" content="DebateLab by Pi-Stack | AI Debate Prep" />
28
-
<metaname="twitter:description" content="Practice debate with AI opponents, case generation, rebuttal drills, evidence leads, coaching, timers, and argument flowcharts." />
28
+
<metaname="twitter:description" content="DebateLab is an AI debate prep and practice app for students and competitive debaters: cases, rebuttals, evidence, coaching and timed practice rounds against an AI opponent." />
0 commit comments