Skip to content

Commit 298fbc1

Browse files
Programcodeclaude
andauthored
Give the /tools pages a #short-answer section and drop their exemption (#191)
Each free tool page and the /tools hub now opens with a question H2 (#short-answer) and a short, direct answer, in English, Spanish and Hungarian. On the four tool pages that answer also leads the FAQPage JSON-LD, the same way the guide pages do. With every indexed page answering a question, the temporary /tools exemption in scripts/check-site.mjs and its note in docs/operations.md are gone. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a238ba6 commit 298fbc1

11 files changed

Lines changed: 99 additions & 12 deletions

File tree

‎docs/operations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The free tools under `/tools` ([`src/pages/tools/`](../src/pages/tools/)) are pr
5858

5959
[`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>`.
6060

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.
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` and `/privacy` 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.
6262

6363
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.
6464

‎scripts/check-site.mjs‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@ import { join, relative, sep } from 'node:path';
1212
const DIST = 'dist';
1313
const SITE_ORIGIN = JSON.parse(readFileSync('src/config/site.json', 'utf8')).origin;
1414
const vercel = JSON.parse(readFileSync('vercel.json', 'utf8'));
15-
// Indexed pages that answer no question of their own. The /tools pages lead
16-
// with the interactive tool itself; they have no #short-answer section yet.
15+
// Indexed pages that answer no question of their own.
1716
const NO_SHORT_ANSWER = new Set(['/terms', '/privacy']);
18-
const NO_SHORT_ANSWER_PREFIX = '/tools';
19-
const answersNoQuestion = (route) =>
20-
NO_SHORT_ANSWER.has(route) || route === NO_SHORT_ANSWER_PREFIX || route.startsWith(`${NO_SHORT_ANSWER_PREFIX}/`);
2117

2218
const problems = [];
2319
const report = (where, message) => problems.push(`${where}: ${message}`);
@@ -141,7 +137,7 @@ for (const file of htmlFiles(DIST)) {
141137

142138
const robots = html.match(/<meta name="robots" content="([^"]*)"/)?.[1] ?? '';
143139
const indexed = !/noindex/i.test(robots) && html.includes('<link rel="canonical"');
144-
if (indexed && !answersNoQuestion(routeOf(file))) {
140+
if (indexed && !NO_SHORT_ANSWER.has(routeOf(file))) {
145141
const firstH2 = body.match(/<h2\b([^>]*)>([\s\S]*?)<\/h2>/);
146142
if (!firstH2 || !/\sid="short-answer"/.test(firstH2[1])) report(where, 'the first H2 is not the #short-answer question');
147143
else if (!textOf(firstH2[2]).endsWith('?')) report(where, 'the #short-answer H2 is not a question');

‎src/components/free-tools/ToolPageLayout.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,16 @@ interface ToolPageLayoutProps {
8282
/** The visible h1. */
8383
title: string;
8484
intro: ReactNode;
85+
/** The reader's question and a direct answer: the first H2, which answer engines quote. */
86+
shortAnswer: FaqEntry;
8587
/** The working tool, shown right under the intro. */
8688
tool?: ReactNode;
8789
/** Long-form sections, FAQ and CTA. */
8890
children: ReactNode;
8991
structuredData: object;
9092
}
9193

92-
export function ToolPageLayout({ route, title, intro, tool, children, structuredData }: ToolPageLayoutProps) {
94+
export function ToolPageLayout({ route, title, intro, shortAnswer, tool, children, structuredData }: ToolPageLayoutProps) {
9395
const { t } = useTranslation('tools');
9496
usePageHead(route);
9597
const crumb = FREE_TOOLS.find((entry) => entry.route === route)?.name;
@@ -128,6 +130,11 @@ export function ToolPageLayout({ route, title, intro, tool, children, structured
128130
<div className="mx-auto mt-6 max-w-[68ch] print:hidden">
129131
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">{title}</h1>
130132
<div className="mt-3 text-lg leading-8 text-muted-foreground">{intro}</div>
133+
{/* Before the tool: the timer, calculator and hub cards carry H2s of their own. */}
134+
<section aria-labelledby="short-answer" className="mt-8">
135+
<h2 id="short-answer" className="text-xl font-semibold tracking-tight text-foreground">{shortAnswer.question}</h2>
136+
<p className="mt-2 leading-7 text-foreground">{shortAnswer.answer}</p>
137+
</section>
131138
</div>
132139
{tool && <div className="mx-auto mt-8 max-w-5xl">{tool}</div>}
133140
<ToolArticle className="mt-14 print:hidden">

‎src/locales/tools/en.json‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@
1010
"ctaPricing": "Start with a 3-day free trial, no card needed. After that DebateLab costs $5 a month plus tax.",
1111
"ctaButton": "Start the free trial"
1212
},
13+
"shortAnswer": {
14+
"hub": {
15+
"question": "What free debate tools can I use without an account?",
16+
"answer": "Four, all free and all usable without signing up: a round timer that knows the speech order and times of seven formats, a motion generator that draws from {{total}} original practice motions, a printable flow sheet with a column for every speech, and a speech time calculator. They run in your browser and save your rounds and drafts there, not on a server."
17+
},
18+
"timer": {
19+
"question": "Is there a free debate timer that knows each format's speech times?",
20+
"answer": "Yes, this one. Pick Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary or the school 4-6-6-3 format, and it loads the speech order and lengths, shows the protected time when points of information are not allowed, and gives each side a prep bank in Public Forum, Lincoln-Douglas and Policy. It is free, needs no account, and keeps a running round through a page reload."
21+
},
22+
"motions": {
23+
"question": "Where can I get random debate motions for practice?",
24+
"answer": "Draw them here. The generator picks one motion, or three for a veto drill, from {{total}} original practice motions written for this site, filtered by topic, difficulty and motion type, in This House or Resolved wording. It is free, needs no account, and lists every motion by topic further down the page."
25+
},
26+
"flow": {
27+
"question": "How do I make a debate flow sheet?",
28+
"answer": "Choose your format and paper size here, then print. The sheet has a column for every speech in speaking order, labelled by speaker and coloured by side, and a strip along the bottom for cross-examination or point-of-information notes. For a PDF, choose Save as PDF in the print dialog. It is free and needs no account."
29+
},
30+
"calc": {
31+
"question": "How long will my speech take to deliver?",
32+
"answer": "Divide its word count by your speaking rate. At an average 130 words per minute, a 520-word speech takes about 4 minutes; at a fast 160, about 3 minutes 15 seconds. Paste the speech into the calculator below to get the time at slow, average, fast and your own pace, and to see which debate speech slots it fits. The text never leaves your browser."
33+
}
34+
},
1335
"timer": {
1436
"region": "{{format}} round timer",
1537
"format": "Format",

‎src/locales/tools/es.json‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@
1010
"ctaPricing": "Empieza con 3 días de prueba gratis, sin tarjeta. Después, DebateLab cuesta 5 $ al mes más impuestos.",
1111
"ctaButton": "Empezar la prueba gratis"
1212
},
13+
"shortAnswer": {
14+
"hub": {
15+
"question": "¿Qué herramientas de debate gratis puedo usar sin crear una cuenta?",
16+
"answer": "Cuatro, todas gratis y sin registro: un cronómetro de rondas que conoce el orden y la duración de los discursos de siete formatos, un generador que saca mociones de un banco de {{total}} mociones de práctica originales, una hoja de flujo imprimible con una columna por discurso y una calculadora de duración de discursos. Funcionan en tu navegador y guardan ahí tus rondas y borradores, no en un servidor."
17+
},
18+
"timer": {
19+
"question": "¿Hay algún cronómetro de debate gratis que conozca los tiempos de cada formato?",
20+
"answer": "Sí, este. Elige Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary o el formato escolar 4-6-6-3, y carga el orden y la duración de los discursos, muestra el tiempo protegido en que no se admiten puntos de información y, en Public Forum, Lincoln-Douglas y Policy, da a cada lado su reserva de tiempo de preparación. Es gratis, no necesita cuenta y conserva la ronda en curso aunque recargues la página."
21+
},
22+
"motions": {
23+
"question": "¿Dónde consigo mociones de debate al azar para practicar?",
24+
"answer": "Aquí. El generador saca una moción, o tres para un ejercicio de veto, de {{total}} mociones de práctica originales escritas para este sitio, con filtros de tema, dificultad y tipo de moción, y redacción «Esta Casa» o «Resolved». Es gratis, no necesita cuenta, y más abajo están todas las mociones ordenadas por tema."
25+
},
26+
"flow": {
27+
"question": "¿Cómo hago una hoja de flujo para un debate?",
28+
"answer": "Elige aquí tu formato y el tamaño del papel, e imprime. La hoja tiene una columna por discurso en orden de intervención, rotulada con el turno de cada orador y coloreada según el lado, y una franja abajo para notas de preguntas cruzadas o puntos de información. Para obtener un PDF, elige Guardar como PDF en el cuadro de impresión. Es gratis y no necesita cuenta."
29+
},
30+
"calc": {
31+
"question": "¿Cuánto tardaré en pronunciar mi discurso?",
32+
"answer": "Divide el número de palabras entre tu ritmo de habla. A un ritmo medio de 130 palabras por minuto, un discurso de 520 palabras dura unos 4 minutos; a un ritmo rápido de 160, unos 3 minutos y 15 segundos. Pega el discurso en la calculadora de abajo para ver la duración a ritmo lento, medio, rápido y al tuyo, y en qué turnos de debate cabe. El texto nunca sale de tu navegador."
33+
}
34+
},
1335
"timer": {
1436
"region": "Cronómetro de ronda de {{format}}",
1537
"format": "Formato",

‎src/locales/tools/hu.json‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@
1010
"ctaPricing": "Kezdd 3 napos ingyenes próbával, bankkártya nélkül. Utána a DebateLab havi 5 dollár plusz adó.",
1111
"ctaButton": "Ingyenes próba indítása"
1212
},
13+
"shortAnswer": {
14+
"hub": {
15+
"question": "Milyen ingyenes vitaeszközöket használhatok regisztráció nélkül?",
16+
"answer": "Négyet, mind ingyenes, és egyikhez sem kell fiók: egy fordulóidőmérőt, amely hét formátum beszédsorrendjét és beszédidejét ismeri, egy tételgenerátort, amely {{total}} eredeti, gyakorlásra írt vitatételből húz, egy nyomtatható jegyzetlapot, amelyen minden beszédnek saját oszlopa van, és egy beszédidő-kalkulátort. A böngésződben futnak, és a fordulóidat és piszkozataidat ott tárolják, nem szerveren."
17+
},
18+
"timer": {
19+
"question": "Van olyan ingyenes vitaidőmérő, amely ismeri az egyes formátumok beszédidejét?",
20+
"answer": "Igen, ez. Válaszd ki a Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary vagy az iskolai 4-6-6-3 formátumot, és az időmérő betölti a beszédek sorrendjét és hosszát, mutatja a védett időt, amikor nem lehet közbekérdezni, a Public Forum, Lincoln-Douglas és Policy formátumban pedig mindkét oldalnak saját felkészülési időkeretet ad. Ingyenes, nem kell hozzá fiók, és a futó forduló az oldal újratöltése után is megmarad."
21+
},
22+
"motions": {
23+
"question": "Hol találok véletlenszerű vitatételeket gyakorláshoz?",
24+
"answer": "Itt. A generátor egy tételt húz, vétógyakorlathoz hármat, {{total}} eredeti, ehhez az oldalhoz írt gyakorló tételből. Szűrhetsz téma, nehézség és tételtípus szerint, a megfogalmazás pedig lehet This House vagy Resolved. Ingyenes, nem kell hozzá fiók, és lejjebb az összes tétel megtalálható téma szerint."
25+
},
26+
"flow": {
27+
"question": "Hogyan készíthetek jegyzetlapot egy vitafordulóhoz?",
28+
"answer": "Válaszd ki itt a formátumot és a papírméretet, majd nyomtasd ki. A lapon minden beszédnek saját oszlopa van, elhangzási sorrendben, a beszélő szerepével felcímkézve és az oldal színével jelölve, alul pedig egy sáv a keresztkérdésekhez vagy a közbekérdezésekhez. PDF-hez a nyomtatási ablakban válaszd a Mentés PDF-ként lehetőséget. Ingyenes, és nem kell hozzá fiók."
29+
},
30+
"calc": {
31+
"question": "Mennyi ideig tart elmondani a beszédemet?",
32+
"answer": "Oszd el a szavak számát a beszédtempóddal. Átlagos, percenként 130 szavas tempóban egy 520 szavas beszéd nagyjából 4 perc, gyors, 160 szavas tempóban körülbelül 3 perc 15 másodperc. Illeszd be a beszédet a lenti kalkulátorba, és megmutatja az időt lassú, átlagos, gyors és saját tempóban, valamint azt, hogy mely vitabeszéd-időkbe fér bele. A szöveg nem hagyja el a böngésződet."
33+
}
34+
},
1335
"timer": {
1436
"region": "{{format}} vitaidőmérő",
1537
"format": "Formátum",

‎src/pages/tools/DebateTimerToolPage.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,15 @@ function EmbedSnippets() {
7979
}
8080

8181
export default function DebateTimerToolPage() {
82+
const { t } = useTranslation('tools');
83+
const shortAnswer = { question: t('shortAnswer.timer.question'), answer: t('shortAnswer.timer.answer') };
8284
return (
8385
<ToolPageLayout
8486
route="/tools/debate-timer"
8587
title="Free debate timer for every format"
8688
intro={<p>Choose a format and the timer already knows the speech order, the protected minutes and each side&apos;s prep time. It needs no account, runs entirely in this tab, and keeps your round if the page reloads.</p>}
87-
structuredData={toolStructuredData('/tools/debate-timer', NAME, FAQ)}
89+
shortAnswer={shortAnswer}
90+
structuredData={toolStructuredData('/tools/debate-timer', NAME, [shortAnswer, ...FAQ])}
8891
tool={(
8992
<DebateTimer
9093
storageKey="debatelab-free-timer-v1"

‎src/pages/tools/FlowSheetToolPage.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Link } from 'react-router-dom';
2+
import { useTranslation } from 'react-i18next';
23
import { FlowSheet } from '@/components/free-tools/FlowSheet';
34
import { ToolCta, ToolFaq, ToolPageLayout } from '@/components/free-tools/ToolPageLayout';
45
import { FORMATS, questioningSegments, speakingSegments } from '@/lib/free-tools/formats';
@@ -30,13 +31,16 @@ const FAQ: FaqEntry[] = [
3031
];
3132

3233
export default function FlowSheetToolPage() {
34+
const { t } = useTranslation('tools');
35+
const shortAnswer = { question: t('shortAnswer.flow.question'), answer: t('shortAnswer.flow.answer') };
3336
const pf = FORMATS.pf;
3437
return (
3538
<ToolPageLayout
3639
route="/tools/debate-flow-sheet"
3740
title="Printable debate flow sheet"
3841
intro={<p>Pick your format and paper, and print a one-page flow with a column for every speech, labelled by speaker and coloured by side, plus a strip for cross-examination notes. Nothing to install; your browser&apos;s print dialog makes the PDF.</p>}
39-
structuredData={toolStructuredData('/tools/debate-flow-sheet', NAME, FAQ)}
42+
shortAnswer={shortAnswer}
43+
structuredData={toolStructuredData('/tools/debate-flow-sheet', NAME, [shortAnswer, ...FAQ])}
4044
tool={<FlowSheet />}
4145
>
4246
<section>

‎src/pages/tools/MotionGeneratorToolPage.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Link } from 'react-router-dom';
2+
import { useTranslation } from 'react-i18next';
23
import { MotionGenerator } from '@/components/free-tools/MotionGenerator';
34
import { ToolCta, ToolFaq, ToolPageLayout } from '@/components/free-tools/ToolPageLayout';
45
import { PRACTICE_MOTIONS } from '@/lib/free-tools/practice-motions';
@@ -61,12 +62,15 @@ function MotionList() {
6162
}
6263

6364
export default function MotionGeneratorToolPage() {
65+
const { t } = useTranslation('tools');
66+
const shortAnswer = { question: t('shortAnswer.motions.question'), answer: t('shortAnswer.motions.answer', { total: PRACTICE_MOTIONS.length }) };
6467
return (
6568
<ToolPageLayout
6669
route="/tools/debate-motion-generator"
6770
title="Debate motion generator"
6871
intro={<p>Draw a practice motion, or three for a veto drill, from {PRACTICE_MOTIONS.length} original motions across twelve topics. Filter by difficulty and motion type, and switch between &quot;This House&quot; wording for BP and World Schools and &quot;Resolved&quot; wording for US formats.</p>}
69-
structuredData={toolStructuredData('/tools/debate-motion-generator', NAME, FAQ)}
72+
shortAnswer={shortAnswer}
73+
structuredData={toolStructuredData('/tools/debate-motion-generator', NAME, [shortAnswer, ...FAQ])}
7074
tool={<MotionGenerator />}
7175
>
7276
<section>

‎src/pages/tools/SpeechTimeCalculatorToolPage.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Link } from 'react-router-dom';
2+
import { useTranslation } from 'react-i18next';
23
import { SpeechTimeCalculator } from '@/components/free-tools/SpeechTimeCalculator';
34
import { ToolCta, ToolFaq, ToolPageLayout } from '@/components/free-tools/ToolPageLayout';
45
import { BUILT_IN_FORMATS, formatDuration, segmentRole, speakingSegments } from '@/lib/free-tools/formats';
@@ -67,12 +68,15 @@ function TargetTable() {
6768
}
6869

6970
export default function SpeechTimeCalculatorToolPage() {
71+
const { t } = useTranslation('tools');
72+
const shortAnswer = { question: t('shortAnswer.calc.question'), answer: t('shortAnswer.calc.answer') };
7073
return (
7174
<ToolPageLayout
7275
route="/tools/speech-time-calculator"
7376
title="How long is my speech? Speech time calculator"
7477
intro={<p>Paste a speech to see its word count, how long it takes to deliver at a slow, average or fast pace, and which debate speech slots it fits. Everything runs in your browser; the text never leaves this page.</p>}
75-
structuredData={toolStructuredData('/tools/speech-time-calculator', NAME, FAQ)}
78+
shortAnswer={shortAnswer}
79+
structuredData={toolStructuredData('/tools/speech-time-calculator', NAME, [shortAnswer, ...FAQ])}
7680
tool={<SpeechTimeCalculator />}
7781
>
7882
<section>

0 commit comments

Comments
 (0)