Skip to content

Make the public pages readable for AI answer engines (AEO) - #156

Merged
Programcode merged 8 commits into
mainfrom
seo/aeo-agent-readability
Oct 22, 2026
Merged

Programcode merged 8 commits into
mainfrom
seo/aeo-agent-readability

Conversation

@Programcode

@Programcode Programcode commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Preview (tailnet): https://codex-remote.tailab0ab3.ts.net:9100/ serves the production build of this branch. Guide pages open at /<slug>/index.html, for example /debatelab-pricing/index.html. Sign-in does not work on the preview origin. Screenshots and recordings are in this comment.

Review: Codex rounds 1 and 2 found 7 checker issues, all fixed in 77f1765 and 00c5f56. Round 3 on 00c5f56 found none.

Implements Rob Hoffman's 11 answer-engine (AEO) recommendations on the public site: the landing page, the 36 guide pages, pricing, the comparison pages, robots.txt and llms.txt. AI agents such as Meta's crawler, ChatGPT, Perplexity and Claude now find what DebateLab is, who it is for, what it costs and when another tool fits better in the raw HTML, without running JavaScript.

Do not merge yet. Adam asked for one PR, held for review. GitHub Actions is out of minutes, so every check below ran locally on the head commit. A local pass is not a CI pass.

The 11 items

  • 1. Accessibility tree. The hamburger already had "Open menu"/"Close menu" and the language button already had "Language". Every decorative landing icon is now aria-hidden, so screen readers and agents skip them. The hero's mock-UI title is no longer a stray <h3> between the H1 and the first H2. Every form field on public pages already had a <label> (hero motion field, login email and code). The only images are the logo (alt "DebateLab logo") and the social card. No product fact appears only inside an image. A new build check (see item 9) fails on an image without alt text, a button without a name or a form field without a label.
  • 2. Key facts in raw HTML. The landing FAQ moved from the Radix accordion to native <details>. Radix renders a closed answer as an empty element, so none of the FAQ answers were in the prerendered HTML before. They are now. Checked with curl on the served build: the definition, "$5/month + tax", "3-day free trial", "No credit card required", "no permanent free plan", "11 prep modes", the three languages, the cancellation rule and "As of October 2026" are all in dist/index.html. The guide pages are static HTML and already had their copy inline.
  • 3. First H2 is the reader's question. The landing page now opens with "What is the best AI debate practice tool?" Comparison pages open with "Which is better, DebateLab or WinDebate?" (and DebateClub, ChatGPT). Pricing opens with "How much does DebateLab cost?" Each of the 31 other guides has its own question, such as "How do you weigh arguments in a debate?" The build fails if an indexed page other than /terms and /privacy lacks this H2 (#short-answer) as its first H2.
  • 4. An honest "it depends" answer of 100 words or fewer. Each answer starts with the answer and names when something else fits better. The landing page names debatelab.ai, which says it is free, for free World Schools and Asian Parliamentary practice, and ChatGPT for general brainstorming. The WinDebate page says to pick WinDebate for Policy, Congress or season tracking. The DebateClub page says DebateClub is for workplace persuasion practice. The generator refuses an answer over 100 words. Guide answers in scripts/seo_answers.py use only facts their own page already states.
  • 5. A definition instead of a slogan. The landing H1 "Prep Faster. Argue Smarter. Win More Rounds." becomes "Prepare Cases and Practice Rounds with AI". The subheadline is now the definition: "DebateLab is an AI debate prep and practice app for students and competitive debaters that builds structured cases, finds the weak points in arguments, researches evidence with citations and runs timed practice rounds against an AI opponent." The same sentence closes every guide in an "About DebateLab" block, opens llms.txt and is the JSON-LD description. It is translated into Spanish and Hungarian.
  • 6. Testimonials as plain text. Skipped. The repo has no real testimonials, user counts or ratings, and I did not invent any.
  • 7. "As of" dates. Prices and plan facts say "As of October 2026" on the landing short answer, the pricing section, the cost FAQ, every guide's About block, the pricing guide and llms.txt. Competitor facts on the comparison pages carry the same date. The date comes from factsCheckedOn in src/config/site.json. Copy that names the month itself (in seo_guides.py and seo_answers.py) needs a manual edit, and docs/operations.md says so.
  • 8. robots.txt and llms.txt. robots.txt names meta-webindexer, meta-externalagent, meta-externalfetcher, GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User, ClaudeBot, Claude-SearchBot, Claude-User, Google-Extended, Applebot, Applebot-Extended, CCBot, Amazonbot, DuckAssistBot and MistralAI-User. A crawler that matches a named group ignores User-agent: *, so the AI group repeats the private-route disallows (/app, /login, /share/ and the rest). Without that, the named group would have opened the app to those bots. The Sitemap line stays. The generator now writes llms.txt with the definition, dated key facts, a note that DebateLab is a separate product from debatelab.ai, and every guide.
  • 9. Broken internal links. The new scripts/check-site.mjs runs in npm run build. It reads every HTML file in dist/, the sitemap and llms.txt, resolves each internal link through the vercel.json redirects and rewrites, and checks #fragments against the target page's ids. I proved it with planted faults: it caught a missing page, a missing fragment, a missing question H2, an image without alt, an unnamed button and an unlabelled field, and it passed a redirected link, a trailing-slash link and an app route. Run against main's guide pages, it found no broken links. It did find HTML entities (&amp;, &#x27;) inside the JSON-LD of /ai-debate-coach and /debate-rebuttal-practice, because the generator passed the JSON through html.escape. That is fixed. On production, all 13 consolidated-guide redirects return a single 301 to the right page, and every sitemap URL returns 200.
  • 10. Semantic HTML. The landing nav sits inside <header>, the footer links sit in a labelled <nav>, and the FAQ uses <details>/<summary>. Guides now use <header>, <main>, <article> (with a <header> for the hero), a breadcrumb <nav>, an <aside> for About, a <nav> for related guides and <footer>. No public page has a clickable div. Every control is a real <a> or <button>.
  • 11. Freshness. Every guide shows "Published July 5, 2026 · Updated October 20, 2026" in <time> elements. July 5 is the date of the commit that first added each guide (git log --diff-filter=A); all 36 share it. JSON-LD datePublished and dateModified, article:published_time and article:modified_time, and the sitemap <lastmod> all match. CONTENT_DATE moves to 2026-10-20 because every guide's copy changed.

JSON-LD. The landing SoftwareApplication now lists both offers (the $0 trial and Pro at $5 a month before tax, as a UnitPriceSpecification), featureList (the 11 modes) and inLanguage (en, es, hu). It also gets a FAQPage built from the visible questions. Guides add the lead Q&A as the first FAQPage entry. Pricing, the three comparison pages and best-ai-debate-tools also carry the SoftwareApplication.

Competitor facts and where they come from

I checked each claim on the vendor's own site on 2026-10-20:

  • WinDebate (windebate.ai) names six formats: PF, LD, Congressional, Policy, World Schools and BP. It has a free tier with monthly limits, Plus at $19.99/month and Pro at $39.99/month, a Live Round Assistant, replay review and progress tracking.
  • DebateClub (debateclub.app) is voice-only sparring for high-stakes conversations such as negotiations and pitches. It has opponent personas and names no debate formats. Free gets one credit, and Pro is $19/month. The old page described it as competitive-debate sparring, and this PR corrects that.
  • debatelab.ai says "DebateLab is completely free to use" in the FAQ in its app bundle. It was built by Yale Debate Association members and offers Casual, WSDC and Asian Parliamentary practice. I only mention it on the landing answer, the best-tools answer and llms.txt. I did not write a dedicated comparison page; that is a possible follow-up.

Other changes

  • @radix-ui/react-accordion and src/components/ui/accordion.tsx are removed (the FAQ was their only user), along with the accordion keyframes. The FAQ loses its height animation. The chevron still rotates.
  • The pricing guide's old "Student plans" card is gone, because no student plan exists. Its copy no longer suggests saved cases are Pro-only.
  • The guide nav and footer link to /debatelab-pricing. The landing footer links to the pricing and comparison guides with plain <a> tags, because these are static pages, not SPA routes.
  • The shell index.html and the landing meta description now use the definition.
  • docs/operations.md and docs/testing.md describe the answers file, the dates, llms.txt, robots and the new build check.

Verification (local, head commit)

  • npm run docs:check, lint (including knip), typecheck: pass.
  • npm test: 305 passed. npm run test:collaboration: 30 passed. npm run test:edge: 356 passed.
  • npm run build: passes, and check-site reports 41 HTML files with no problems. The sitemap has 39 URLs, all with lastmod.
  • npm run test:sw: passes.
  • Playwright @smoke on desktop Chromium: 30 passed. loading-performance, public-pages and hungarian-parity on desktop and mobile Chromium: 44 passed. I updated two specs that pinned the old headline. The Hungarian spec now also checks the localized "2026. október állapot szerint" date.
  • In a real browser on vite preview, the landing, short answer and FAQ open/close work on desktop and iPhone 12. Mobile has no horizontal overflow. The pricing and comparison guides render with their dates and question H2.

Not run: the agentic/ suite, which calls paid OpenRouter models to judge pages (the copy changed, so its replay cache may need re-recording), and the verify-debatelab stack. No recipe covers the landing page, and this PR does not touch the login and app code. Nothing has reached staging or production.

Screenshots and recordings are attached below.

🤖 Generated with Claude Code

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 &amp; and &#x27; 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>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 16:14
@vercel

vercel Bot commented Oct 20, 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 20, 2026 4:25pm UTC
debatelab-staging Ready Ready Preview Oct 20, 2026 4:25pm 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.

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

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 45 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
📥 Commits

Reviewing files that changed from the base of the PR and between 311ee3e and 29b2213.

⛔ Files ignored due to path filters: 1
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing: 71
  • docs/operations.md
  • docs/testing.md
  • e2e/hungarian-parity.spec.ts
  • e2e/loading-performance.spec.ts
  • index.html
  • package.json
  • public/ai-debate-case-generator/index.html
  • public/ai-debate-coach/index.html
  • public/ai-debate-practice/index.html
  • public/ai-rebuttal-generator/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-block-file-guide/index.html
  • public/debate-clash-points/index.html
  • public/debate-collapsing-strategy/index.html
  • public/debate-evidence-finder/index.html
  • public/debate-fallacies/index.html
  • public/debate-judge-adaptation/index.html
  • public/debate-motion-analysis/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/debatelab-pricing/index.html
  • public/debatelab-vs-chatgpt/index.html
  • public/debatelab-vs-debateclub/index.html
  • public/debatelab-vs-windebate/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/lincoln-douglas-debate/index.html
  • public/llms.txt
  • public/online-debate-timer/index.html
  • public/policy-debate/index.html
  • public/public-forum-debate/index.html
  • public/robots.txt
  • public/seo-pages.css
  • public/world-schools-debate/index.html
  • scripts/check-site.mjs
  • scripts/generate_seo_content.py
  • scripts/seo_answers.py
  • scripts/seo_guides.py
  • src/components/landing/DeepDive.tsx
  • src/components/landing/FAQ.tsx
  • src/components/landing/Features.tsx
  • src/components/landing/FinalCTA.tsx
  • src/components/landing/Footer.tsx
  • src/components/landing/Hero.tsx
  • src/components/landing/HowItWorks.tsx
  • src/components/landing/Navbar.tsx
  • src/components/landing/Pricing.tsx
  • src/components/landing/ShortAnswer.tsx
  • src/components/landing/UseCases.tsx
  • src/components/landing/WhatsNew.tsx
  • src/components/ui/accordion.tsx
  • src/config/site.json
  • src/lib/seo/pages.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/LandingPage.tsx
  • src/prerender/entry.tsx
  • src/test/vercel-deployment-config.test.ts
  • tailwind.config.ts
  • 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

Screenshots and recordings from the served production build (vite preview, external scripts blocked). Each recording shows the landing hero, the short answer, the FAQ opening and the WinDebate comparison guide.

Desktop: hero with definition

Desktop: question H2 and short answer

Desktop: native FAQ, open

Desktop: comparison guide with dates

Mobile: hero

Mobile: short answer

Mobile: guide

desktop-flow.webm
mobile-flow.webm

@Programcode

Copy link
Copy Markdown
Owner Author

CI on b7fe946: every Actions job (quality, docs, browser-smoke, database-tests, the four suite legs) failed in 2-3 seconds with zero steps. GitHub's annotation says: "The job was not started because recent account payments have failed or your spending limit needs to be increased." These are billing failures, not code failures. The same checks ran locally on b7fe946: docs:check, lint, typecheck, test (305), test:collaboration (30), test:edge (356), build (with check-site), test:sw, Playwright @smoke desktop (30) and the landing, public-page and Hungarian specs on desktop and mobile (44). database-tests was not run locally because this PR changes no migrations or database code. Both Vercel deployments built successfully.

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>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review round 1 (b7fe946) had three findings, all in scripts/check-site.mjs, and all fixed in 77f1765:

  1. Medium: a field nested inside its <label> was reported as unlabelled. Fixed: fields inside a <label> element now count as labelled.
  2. Low: /share/:path* became ^/share/.*$, so /share failed, unlike Vercel. Fixed: /:name* now converts to (?:/.*)?.
  3. Low: any non-empty aria-labelledby counted as a name. Fixed: every referenced id must exist on the page and its element must have text. A blank aria-label no longer counts either.

Proof: planted /share, /share/, /share/abc, an implicitly labelled input, a button labelled by a real id, a button and an input labelled by missing ids, and a button with aria-label=" " into a built page. The checker flagged exactly the last three and passed the rest. The real build passes, and eslint is clean on the file. Round 2 starts on 77f1765.

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>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review round 2 (77f1765) had four findings, all in scripts/check-site.mjs, and all fixed in 00c5f56:

  1. Medium: aria-labelledby text stopped at the first closing tag, so <span id=n><span aria-hidden></span>Name</span> read as empty. Fixed: text is now read to the element's own closing tag, counting nested tags of the same name.
  2. Medium: ids.every rejected a valid combined name such as aria-labelledby="name extra" with an empty extra. Fixed: the field is named if any referenced element has text.
  3. Low: implicit labels were matched by opening-tag text, so an identical unlabelled input elsewhere on the page passed. Fixed: a field counts as wrapped only if its position falls inside a <label> element.
  4. Low: aria-label="" passed because \S matched the closing quote. Fixed: the attribute value is captured and trimmed.

Proof: planted all four cases plus the round-1 cases in a built page. The checker reported exactly the five broken ones: the button labelled by a missing id, the button with a blank aria-label, the unwrapped twin input, the input with aria-label="" and the input labelled by a missing id. It passed the nested, combined, wrapped and real-id cases. The real build passes, and eslint is clean. Round 3 starts on 00c5f56.

@Programcode

Copy link
Copy Markdown
Owner Author

Codex review round 3 on 00c5f56 found no issues. It ran 22 focused probes of the round-2 checker fixes. Every finding from rounds 1 and 2 is fixed (see the replies above). PR is ready for Adam's review; holding unmerged as requested. GitHub Actions jobs still do not start because of the account billing block, so all checks are local.

@Programcode
Programcode merged commit 71c083e into main Oct 22, 2026
2 of 11 checks passed
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