Skip to content

Link every guide from its topic cluster and the home page, name the product in the H1 and JSON-LD - #234

Merged
Programcode merged 4 commits into
mainfrom
seo/internal-links-brand-entity
Oct 26, 2026
Merged

Programcode merged 4 commits into
mainfrom
seo/internal-links-brand-entity

Conversation

@Programcode

@Programcode Programcode commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Spec 1 from the SEO sprint (seo-sprint/cody-specs.md): no sitemap page is an orphan any more, the home page links to the guides, the H1 and JSON-LD name the product, and the IndexNow script works on a clean checkout. This is a draft, so please don't merge it until Adam has reviewed it. Nothing here deploys or pings IndexNow.

Update: merged with main after #233 and #235

This is a normal merge of origin/main (04e1a78), with no rebase or force-push.

What changed (original PR)

Related links by topic cluster (scripts/generate_seo_content.py). internal_links() used to take PAGES[:8] (or PAGES[8:16]), so pages 17 onward were never linked. CLUSTERS now puts every page in one of four clusters: practice and product, formats, skills, compare. The "Related" block shows up to 6 pages from the page's own cluster plus one page from each of two other clusters, and both lists start just after the current page, so no two pages show the same eight. The link labels are now the guide H1s; the old labels were trimmed <title>s such as "How to Write a Debate Case Guide". Generation stops if a PAGES slug is missing from CLUSTERS, or if CLUSTERS or LINK_PHRASES names a slug that is not in PAGES.

In-body links. LINK_PHRASES turns the first match of a phrase in a paragraph into a link to its guide. For example, "final focus" links to Public Forum, "reply speech" to World Schools and "rebuttal blocks" to how to practice rebuttals. Pages get at most 3. Specific phrases are tried first, and single words like "clash" and "evidence" only fill pages that are still short. Links skip the page itself and pages its copy already links. Headings, lists, examples, the short answer and FAQs are left alone. python3 scripts/generate_seo_content.py --links lists what each page got. Three thin pages get only one link because their text has nothing else to anchor on: debate-clash-points, debate-fallacies and debate-motion-analysis, about 65 to 75 words each. I didn't add sentences to them because Spec 2 rewrites all three.

Free tool pages. Each one now links to two guides. Timer: how to flow, speech times by format. Flow sheet: how to flow, argument flowchart. Speech time calculator: speech structure, speech times. Hub: speech times, how to flow. The motion generator already linked three guides and is unchanged. TOOL_PAGES_MODIFIED moves to 2026-10-26 for the four pages whose copy changed.

Home page hub. New src/components/landing/GuideHub.tsx, placed above the FAQ and prerendered. It has five plain <a href> lists: Practice with AI (5), Debate formats (6), Skills guides (6), Free tools (/tools and the 4 tools), Compare (best AI debate tools, vs ChatGPT). The practice and skills picks favour the old orphans. Strings are in guideHub in en, es and hu. The es and hu subtitles say the guides are in English. Format names stay in English there, per the glossary, and are on the i18n-untranslated allowlist.

Home page brand signal. This is a text-only change through locale keys, and Hero.tsx, Navbar.tsx and Footer.tsx are not touched.

  • H1: "{{product}}: AI debate prep and practice". The es and hu versions say the same in those languages.
  • Subheading: it starts with the old H1 sentence ("Prepare cases and practice rounds with AI.") and keeps every fact from the old definition line.
  • New FAQ q7 asks "Is {{product}} the same as debatelab.ai?" and answers "No. {{brand}} is a separate product at {{host}}." It also appears in the FAQPage JSON-LD.
  • <title> is unchanged.
  • src/i18n.ts now sets product, brand and host from site.json as i18next defaultVariables, so any string can use them.

The spec's literal wording was "keep the current sentence as the subheading". I kept the definition facts as well, so the subheading is the old H1 sentence followed by the definition. Adam, if you'd rather the subheading were only that one sentence, it's a one-line locale change.

Entity JSON-LD.

  • WebSite.name is now the product, with alternateName ["DebateLab by Pi-Stack", "debate.pi-stack.org"].
  • SoftwareApplication.name is the product, with the brand line as alternateName.
  • Organization.sameAs is ["https://pi-stack.org/"], read from a new organizationSameAs field in site.json. I didn't add a GitHub profile because site.json has no field for one.
  • Every guide's publisher, author and about still point at the same #organization and #software @ids, and the guides' inline Organization carries the same sameAs.

Guards.

  • scripts/check-links.mjs runs in npm run build, right after check-site.mjs. It fails the build when a sitemap page other than /terms, /privacy or /expocit/sources has fewer than 3 inbound links from other sitemap pages, or when / links to fewer than 15 sitemap pages. --report prints every count. It runs at build time rather than in npm test because npm test has no dist/; npm run verify runs the build, so it's covered there.
  • scripts/check-site.mjs now also fails on:
    • a FAQPage question that isn't visible on its page;
    • an unresolved @id in the home-page graph;
    • a WebSite or SoftwareApplication that isn't named for the product;
    • a missing Organization.sameAs;
    • a publisher, author, about or isPartOf that names an @id the home page doesn't define.

IndexNow (scripts/indexnow_submit.py, npm run seo:indexnow). It reads dist/sitemap.xml when a build exists and otherwise fetches ${origin}/sitemap.xml. It posts once to https://api.indexnow.org/indexnow. --only <url...> accepts paths or URLs on our host and rejects any other host. --dry-run prints the request and sends nothing. Host and key location come from site.json. The key and key file are unchanged, and it's not wired into CI. I only ran it with --dry-run.

Dates. Every guide is dated 2026-10-26 at the earliest, through main's CONTENT_DATE, which also covers the Related block and in-body links.

Tests and docs.

  • e2e/loading-performance.spec.ts and e2e/hungarian-parity.spec.ts match the new H1.
  • scripts/test-service-worker-offline.mjs fills {{product}} in the Spanish headline before matching.
  • vercel-deployment-config.test.ts pins the build command with check-links.mjs added.
  • docs/operations.md covers the clusters, phrase table, link guard, entity checks and IndexNow.

Acceptance evidence

All of this comes from a local npm run build of this branch, against a build of origin/main (6f93aa5) for "before".

Homepage links to distinct sitemap URLs: 5 before, 27 after.

Inbound links from other sitemap pages, per URL. "main 6f93aa5" is the count before this PR, and "now" is this branch after the merge. 13 pages had none before; now no page has none except /expocit/sources, which the spec exempts:

main 6f93aa58  now  url
            0    7  /ai-rebuttal-generator
            0   12  /debate-clash-points
            0    9  /debate-fallacies
            0    9  /debate-judge-adaptation
            0    7  /debatelab-vs-chatgpt
            0    5  /debatelab-vs-debateclub
            0    4  /debatelab-vs-windebate
            0    0  /expocit/sources
            0   11  /how-to-practice-rebuttals
            0    9  /how-to-prepare-for-a-debate-tournament
            1    8  /debate-motion-analysis
            1    9  /debate-speech-structure
            1    7  /debate-topics-for-students
            2   10  /how-to-flow-in-debate
            5    5  /tools
            5    6  /tools/debate-flow-sheet
            6   23  /best-ai-debate-tools
            6    9  /tools/debate-motion-generator
            6    6  /tools/speech-time-calculator
            8   18  /british-parliamentary-debate
            8    8  /congress-debate
            8   16  /how-to-cross-examine-in-debate
            8   14  /how-to-weigh-in-debate
            8   21  /lincoln-douglas-debate
            8    9  /policy-debate
            8   22  /public-forum-debate
            8   10  /world-schools-debate
           10   14  /tools/debate-timer
           28    8  /ai-debate-coach
           28    8  /argument-flowchart
           28   11  /online-debate-timer
           29   14  /how-to-write-a-debate-case
           35   31  /ai-debate-case-generator
           35   31  /ai-debate-practice
           35   31  /debate-evidence-finder
           35   30  /debate-rebuttal-practice
           41   36  /debatelab-pricing
           42   37  /privacy
           42   37  /terms
           44   39  /

Guard output after the merge: check-links: 40 sitemap URLs, inbound links 0-39, homepage links to 27; no problems. The 0 is /expocit/sources. On main the same script reports 19 problems: 13 orphans, 5 pages with 1 link, how-to-flow-in-debate with 2, and the home page at 5.

JSON-LD. check-site: 43 HTML files, sitemap and llms.txt resolve; no problems, with the new entity checks. To make sure they can fail, I edited dist/index.html to rename WebSite and to break a FAQ question, and both were reported:

index.html: FAQPage question "Not on the page? Is DebateLab the same as debatelab.ai?" is not on the page
index.html: WebSite.name is "Wrong", not the product name "DebateLab"

Home-page entity nodes as built:

{"@type": "Organization", "@id": "https://debate.pi-stack.org/#organization", "name": "Pi-Stack", "sameAs": ["https://pi-stack.org/"]}
{"@type": "WebSite", "@id": "https://debate.pi-stack.org/#website", "name": "DebateLab", "alternateName": ["DebateLab by Pi-Stack", "debate.pi-stack.org"], "publisher": {"@id": "https://debate.pi-stack.org/#organization"}}
{"@type": "SoftwareApplication", "@id": "https://debate.pi-stack.org/#software", "name": "DebateLab", "alternateName": "DebateLab by Pi-Stack", "publisher": {"@id": "https://debate.pi-stack.org/#organization"}}
FAQPage: 8 questions, q7 "Is DebateLab the same as debatelab.ai?"
Guide Article (policy-debate): publisher @id https://debate.pi-stack.org/#organization, about {"@id": "https://debate.pi-stack.org/#software"}

I didn't run Google's Rich Results Test because it fetches production, and this branch isn't deployed.

IndexNow --dry-run after the merge (40 URLs: the 45 in the spec minus the five #235 retired; nothing sent):

{
  "endpoint": "https://api.indexnow.org/indexnow",
  "source": "dist/sitemap.xml",
  "urls": 40,
  "dryRun": true
}
{
  "host": "debate.pi-stack.org",
  "key": "4edb6e55e44b9a2ccae55ceb1a4d6e6e",
  "keyLocation": "https://debate.pi-stack.org/4edb6e55e44b9a2ccae55ceb1a4d6e6e.txt",
  "urlList": [
    "https://debate.pi-stack.org/",
    "https://debate.pi-stack.org/ai-debate-case-generator",
    "https://debate.pi-stack.org/ai-debate-coach",
    "https://debate.pi-stack.org/ai-debate-practice",
    "https://debate.pi-stack.org/ai-rebuttal-generator",
    "https://debate.pi-stack.org/argument-flowchart",
    "https://debate.pi-stack.org/best-ai-debate-tools",
    "https://debate.pi-stack.org/british-parliamentary-debate",
    "https://debate.pi-stack.org/congress-debate",
    "https://debate.pi-stack.org/debate-clash-points",
    "https://debate.pi-stack.org/debate-evidence-finder",
    "https://debate.pi-stack.org/debate-fallacies",
    "https://debate.pi-stack.org/debate-judge-adaptation",
    "https://debate.pi-stack.org/debate-motion-analysis",
    "https://debate.pi-stack.org/debate-rebuttal-practice",
    "https://debate.pi-stack.org/debate-speech-structure",
    "https://debate.pi-stack.org/debate-topics-for-students",
    "https://debate.pi-stack.org/debatelab-pricing",
    "https://debate.pi-stack.org/debatelab-vs-chatgpt",
    "https://debate.pi-stack.org/debatelab-vs-debateclub",
    "https://debate.pi-stack.org/debatelab-vs-windebate",
    "https://debate.pi-stack.org/expocit/sources",
    "https://debate.pi-stack.org/how-to-cross-examine-in-debate",
    "https://debate.pi-stack.org/how-to-flow-in-debate",
    "https://debate.pi-stack.org/how-to-practice-rebuttals",
    "https://debate.pi-stack.org/how-to-prepare-for-a-debate-tournament",
    "https://debate.pi-stack.org/how-to-weigh-in-debate",
    "https://debate.pi-stack.org/how-to-write-a-debate-case",
    "https://debate.pi-stack.org/lincoln-douglas-debate",
    "https://debate.pi-stack.org/online-debate-timer",
    "https://debate.pi-stack.org/policy-debate",
    "https://debate.pi-stack.org/privacy",
    "https://debate.pi-stack.org/public-forum-debate",
    "https://debate.pi-stack.org/terms",
    "https://debate.pi-stack.org/tools",
    "https://debate.pi-stack.org/tools/debate-flow-sheet",
    "https://debate.pi-stack.org/tools/debate-motion-generator",
    "https://debate.pi-stack.org/tools/debate-timer",
    "https://debate.pi-stack.org/tools/speech-time-calculator",
    "https://debate.pi-stack.org/world-schools-debate"
  ]
}

--only https://example.com/x exits 1 with --only takes URLs on debate.pi-stack.org.

Browser check. I built the site, served it with vite preview, and loaded / in Playwright Chromium four ways: desktop light (en), desktop dark (en), phone light (en) and phone dark (hu). The H1, FAQ q7 and the 24 hub links rendered in each one. No page errors. The only console errors were 404s for /_vercel/insights and /_vercel/speed-insights, which exist only on Vercel.

Verification

npm run verify passed locally at the merge commit a5a25569 (Node 22), exit 0:

  • docs:check, lint (eslint, knip, repo rules) and typecheck passed
  • npm test: 90 files, 654 tests passed, including main's guide-content test
  • test:collaboration: 30 passed
  • test:edge: 482 passed, 0 failed
  • build passed, including check-site (43 HTML files) and check-links (40 URLs, 27 homepage links)
  • test:sw passed
  • desktop-chromium @smoke: 47 passed

Before the merge, the two e2e specs that check the new H1 passed on desktop and mobile Chromium. The only failure was loading-performance.spec.ts:63, a known failure on main. Not run: the full e2e matrix, WebKit, the database gate (no SQL changes) and staging. GitHub Actions is billing-blocked, so these are local results, not a CI pass.

🤖 Generated with Claude Code

Programcode and others added 2 commits October 26, 2026 16:52
…roduct in the H1 and JSON-LD

Related guides now come from four topic clusters (practice, formats, skills,
compare) instead of the first 16 entries of PAGES, so the 13 guides that no
page linked to get 4 to 18 inbound links. Paragraphs get up to three in-body
links from a phrase table, specific phrases first.

The home page gets a guide and tool hub above the FAQ, an H1 that names the
product, and a FAQ entry separating it from debatelab.ai. WebSite and
SoftwareApplication are named for the product with the brand line as an
alternate name, and the Organization lists pi-stack.org in sameAs.

check-links.mjs fails the build when a sitemap page has fewer than 3
inbound links or the home page links to fewer than 15 sitemap pages, and
check-site.mjs now checks the entity graph and that FAQPage questions are
visible. The IndexNow script reads the built or live sitemap, posts once to
api.indexnow.org, and takes --only and --dry-run (npm run seo:indexnow).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playwright loads specs as Node ESM, which refuses a JSON import without an
import attribute; localization.spec.ts reads its catalogs the same way.

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

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

  • 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 and others added 2 commits October 26, 2026 17:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#233 and #235 deepened the guides and retired five of them. The generator
keeps main's per-page FAQs, primary-source links, tables, short About block
and CONTENT_DATE floor, and this branch's clusters, in-body links and entity
JSON-LD. LINKS_DATE is gone: CONTENT_DATE already dates every page from
2026-10-26, which covers the shared related links.

The retired slugs leave CLUSTERS; their phrases now link the guides their
301s point to. In-body links also go in list and step items, since the
deepened guides put much of their prose there. GuideHub renders nothing on
/es and /hu routes, which do not exist yet on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode
Programcode marked this pull request as ready for review October 26, 2026 18:31
Copilot AI balanced review requested due to automatic review settings October 26, 2026 18:31
@Programcode
Programcode merged commit 92d7113 into main Oct 26, 2026
3 of 11 checks passed

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 insufficient remaining quota.

Programcode added a commit that referenced this pull request Oct 26, 2026
Conflicts resolved:
- i18n.ts: keep both default variable sets, product/brand/host from
  main and trialDays/proMonthly from this branch.
- en/es/hu.json: main's new hero headline and subheadline, this
  branch's {{trialDays}} trial badge.
- prerender/entry.tsx, check-site.mjs, docs/operations.md: keep both
  sides (main's entity graph and FAQ checks, this branch's localized
  pages and hreflang check).

GuideHub on /hu and /es now shows only the free tools at their
localized addresses, plus one link to the English guides on the home
page ("Angol nyelvű útmutatók", "Guías en inglés"). The Hungarian tool
labels use this branch's names (Vitaidőmérő, Vitatétel-generátor,
Jegyzetlap), and "cáfolást" follows the review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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