Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ The gate runs, in order:
2. the guide jargon check (`npm run seo:jargon`): no generated guide in `public/` other than `/lincoln-douglas-debate` and `/policy-debate` may use framework, contention, warrant or impact calculus more than once in its visible text or its JSON-LD ([`scripts/check_jargon.py`](../scripts/check_jargon.py))
3. lint
4. type checking
5. unit tests
5. unit tests, including the comparison-page templating check: no sentence of more than 12 words may appear on more than two of the seven comparison pages, apart from the pricing sentence and the byline and disclosure lines ([`scripts/check_templating.py`](../scripts/check_templating.py), run by `src/test/seo/comparison-templating.test.ts`)
6. collaboration service tests
7. native Edge Function tests
8. production build
Expand Down
21 changes: 14 additions & 7 deletions public/ai-debate-tools-for-teachers/index.html

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions public/best-ai-for-debate/index.html

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions public/best-ai-for-public-forum/index.html

Large diffs are not rendered by default.

24 changes: 13 additions & 11 deletions public/chatgpt-vs-claude-for-debate/index.html

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions public/debateowl-vs-debate-ladder/index.html

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions public/free-vs-paid-ai-debate-tools/index.html

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions public/how-to-prepare-for-a-debate-with-ai/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Free to use, no account needed, and they run in the browser.

## Compare

- [Which AI is best for debate?](https://debateowl.com/best-ai-for-debate): DebateOwl is the top pick for a case you can read out loud, If they say X, we answer Y replies, timed practice rounds, free tools and $5/month, with a dated comparison of Claude, ChatGPT, SchoolAI, Debate Ladder, ArguFight, HyperWrite and PublicForumAI
- [Which AI is best for debate?](https://debateowl.com/best-ai-for-debate): DebateOwl is the top pick for a case you can read out loud, "If they say X, we answer Y" replies and timed practice rounds (free tools; app $5/month after a 3-day trial), with a dated comparison of Claude, ChatGPT, SchoolAI, Debate Ladder, ArguFight, HyperWrite and PublicForumAI
- [Best AI Debate Tools Compared (2026)](https://debateowl.com/best-ai-debate-tools): six AI debate tools side by side on cases, replies, timed rounds and price, plus a one-hour test to run on any tool
- [DebateOwl vs ChatGPT for Debate Prep](https://debateowl.com/debateowl-vs-chatgpt): How DebateOwl and ChatGPT compare for debate prep, from structured case and rebuttal workflows to evidence research and timed practice rounds.
- [DebateOwl vs WinDebate](https://debateowl.com/debateowl-vs-windebate): How DebateOwl and WinDebate compare on case generation, rebuttal drills, evidence research, supported formats, pricing and day-to-day workflow.
Expand Down
112 changes: 112 additions & 0 deletions scripts/check_templating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Fail when the seven comparison pages share long sentences: python3 scripts/check_templating.py.

Each comparison page needs its own angle, so no sentence longer than MAX_WORDS
words may appear in the visible text of more than MAX_PAGES of them. The pricing
sentence (once per page by design) and the byline, disclosure and "Last updated"
lines are exempt.
"""

from collections import defaultdict
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
import re
import sys

PUBLIC = Path(__file__).resolve().parents[1] / "public"
PAGES = [
"best-ai-for-debate",
"ai-debate-tools-for-teachers",
"best-ai-for-public-forum",
"chatgpt-vs-claude-for-debate",
"debateowl-vs-debate-ladder",
"free-vs-paid-ai-debate-tools",
"how-to-prepare-for-a-debate-with-ai",
]
MAX_WORDS = 12
MAX_PAGES = 2
PRICING_START = "The DebateOwl app has no free plan"
# Elements whose text is the same on every page on purpose.
EXEMPT_CLASSES = {"byline", "disclosure", "updated", "dates"}
BLOCKS = {"p", "li", "h1", "h2", "h3", "h4", "td", "th", "dt", "dd", "summary", "caption", "label", "legend", "pre", "div", "section", "aside", "nav", "footer", "header", "tr", "ol", "ul", "dl", "table", "figcaption"}
VOID = {"br", "img", "input", "meta", "link", "hr", "source", "wbr"}


class Blocks(HTMLParser):
"""Visible body text, split into one string per block element."""

def __init__(self):
super().__init__(convert_charrefs=True)
self.in_body = False
self.hidden = 0
self.stack = []
self.blocks = []
self.current = []

def flush(self):
text = re.sub(r"\s+", " ", "".join(self.current)).strip()
if text:
self.blocks.append(text)
self.current = []

def handle_starttag(self, tag, attrs):
if tag == "body":
self.in_body = True
if tag in VOID:
return
classes = set((dict(attrs).get("class") or "").split())
exempt = tag in {"script", "style"} or bool(classes & EXEMPT_CLASSES)
if tag in BLOCKS:
self.flush()
self.stack.append((tag, exempt))
if exempt:
self.hidden += 1

def handle_endtag(self, tag):
if tag in VOID:
return
while self.stack:
open_tag, exempt = self.stack.pop()
if exempt:
self.hidden -= 1
if open_tag == tag:
break
if tag in BLOCKS:
self.flush()

def handle_data(self, data):
if self.in_body and not self.hidden:
self.current.append(data)


def sentences(html):
parser = Blocks()
parser.feed(html)
parser.flush()
for block in parser.blocks:
for sentence in re.split(r"(?<=[.!?])[\"”’)]*\s+(?=[\"“‘(]?[A-Z0-9$])", unescape(block)):
sentence = sentence.strip()
if sentence:
yield sentence


def main():
seen = defaultdict(set)
for slug in PAGES:
html = (PUBLIC / slug / "index.html").read_text()
for sentence in sentences(html):
if len(sentence.split()) <= MAX_WORDS or sentence.startswith(PRICING_START) or "Last updated" in sentence:
continue
seen[re.sub(r"\s+", " ", sentence).lower()].add(slug)
shared = {sentence: pages for sentence, pages in seen.items() if len(pages) > MAX_PAGES}
for sentence, pages in sorted(shared.items()):
print(f"On {len(pages)} pages ({', '.join(sorted(pages))}):\n {sentence}")
if shared:
print(f"\n{len(shared)} sentence(s) of more than {MAX_WORDS} words appear on more than {MAX_PAGES} comparison pages. Give each page its own wording.")
return 1
print(f"Templating check passed: no sentence of more than {MAX_WORDS} words is on more than {MAX_PAGES} of the {len(PAGES)} comparison pages.")
return 0


if __name__ == "__main__":
sys.exit(main())
21 changes: 16 additions & 5 deletions scripts/generate_seo_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import date
from pathlib import Path
import html
from html import unescape
import json
import re
import sys
Expand Down Expand Up @@ -34,6 +35,11 @@ def site_sentence(key):
# What a generated case contains, and the app-versus-free-tools pricing fact, as the home page states them.
CASE_FORMAT = site_sentence("caseFormat")
FREE_PLAN = site_sentence("freePlan")
# FREE_PLAN as /best-ai-for-debate shows it once, in its call-to-action area, with the free tools linked by name.
FREE_PLAN_HTML = html.escape(FREE_PLAN).replace(
"the separate tools at /tools are free",
'the separate <a href="/tools">timer, motion generator, flow sheet and speech-time calculator</a> are free',
)
# The SoftwareApplication description, the same as SOFTWARE_DESCRIPTION in src/lib/seo/pages.ts.
SOFTWARE_DESCRIPTION = f'{SITE["tagline"]} {CASE_FORMAT}'
if len(SOFTWARE_DESCRIPTION) > 300:
Expand Down Expand Up @@ -99,7 +105,7 @@ def site_sentence(key):
{"slug": "debate-speech-structure", "title": "Debate Speech Structure and Delivery | {product}", "description": "How to structure constructive, rebuttal and final debate speeches by format, with a timed outline and the delivery habits that judges notice.", "eyebrow": "Strategy guide", "h1": "Debate Speech Structure Guide", "lead": "Structure your speech so the judge can follow every argument and every comparison.", "features": ["Opening structure", "Rebuttal structure", "Final speech structure"], "sections": [["Why structure matters", ["A well-structured speech is easier to flow, easier to judge, and easier to win. The judge should never be confused about where you are."]], ["Opening speech structure", ["State your position, define key terms, present contentions with warrants and impacts, and preview weighing."]], ["Rebuttal and final speech structure", ["Respond to the key clash, extend your offense, weigh the comparison, and frame the ballot. End by telling the judge why you win."]]]},
{"slug": "debate-motion-analysis", "modified": "2026-10-23", "title": "Debate Motion Analysis | {product}", "description": "How to analyze a debate motion by its type, what each side must prove, fair definitions, stakeholders and clashes, with a worked example.", "eyebrow": "Strategy guide", "h1": "Debate Motion Analysis Guide", "lead": "From topic to strategy: define, clash, and build a winning case.", "features": ["Definition strategy", "Burden identification", "Clash mapping"], "sections": [["How to analyze a motion", ["Read the motion carefully, define each term, identify what each side must prove, and map the likely clashes.", "To practice on a motion you have not seen, draw one from the [free debate motion generator](/tools/debate-motion-generator)."]], ["Definition strategy", ["Definitions can frame the round. Choose definitions that are fair, defensible, and favorable to your side."]], ["Burden identification", ["Your burden is what you must prove to win. If you do not meet your burden, you lose regardless of how well you argue."]]]},
{"slug": "best-ai-debate-tools", "title": "Best AI Debate Tools Compared (2026) | {product}", "description": "A comparison of AI debate tools on case generation, rebuttal practice, evidence research, practice rounds and pricing, to help you choose one.", "eyebrow": "Comparison", "h1": "Best AI Debate Tools Compared (2026)", "lead": "See how AI debate tools compare on features, formats, and prep workflows."},
{"slug": "best-ai-for-debate", "published": "2026-10-29", "title": "Which AI Is Best for Debate? (2026 Guide) | {product}", "description": "DebateOwl is the top pick: a case you can read out loud, If they say replies, timed rounds, free tools, then $5/month.", "eyebrow": "Comparison", "h1": "Which AI is best for debate?", "lead": "DebateOwl is the top pick for a case you can read out loud and timed practice rounds."},
{"slug": "best-ai-for-debate", "published": "2026-10-29", "title": "Which AI Is Best for Debate? (2026 Guide) | {product}", "description": "Compare DebateOwl, ChatGPT, Claude and other AI tools for debate cases, \"If they say\" replies and timed practice, with facts as of October 2026.", "eyebrow": "Comparison", "h1": "Which AI is best for debate?", "lead": "DebateOwl is the top pick for a case you can read out loud and timed practice rounds."},
{"slug": "debateowl-pricing", "title": "Pricing and Plans | {product}", "description": "{product} has a {trial_days}-day free trial with no credit card required, then one Pro plan at {pro_price}, and this page lists what each includes.", "eyebrow": "Pricing", "h1": "{product} Pricing and Plans", "lead": "See what {product} offers and how to get started with AI debate practice."},
{"slug": "debateowl-vs-chatgpt", "title": "{product} vs ChatGPT for Debate Prep | {product}", "description": "How {product} and ChatGPT compare for debate prep, from structured case and rebuttal workflows to evidence research and timed practice rounds.", "eyebrow": "Comparison", "h1": "{product} vs ChatGPT for Debate Prep", "lead": "ChatGPT is a general chatbot. {product} is a debate practice workspace.", "features": ["Structured workflows", "Evidence leads", "Practice tools"], "sections": [["What ChatGPT does well", ["ChatGPT is a flexible general-purpose AI. It can brainstorm, draft, and answer questions about any topic, including debate."]], ["What {product} does differently", ["{product} is designed for debate prep workflows: case structure, targeted rebuttals, evidence leads, argument flowcharts, coaching prompts, and timed practice. Each workflow supports a specific prep task."]], ["When to use each", ["Use ChatGPT for general brainstorming and research. Use {product} for structured, repeatable debate practice that mirrors what you do in a round."]]]},
{"slug": "debateowl-vs-windebate", "title": "{product} vs WinDebate | {product}", "description": "How {product} and WinDebate compare on case generation, rebuttal drills, evidence research, supported formats, pricing and day-to-day workflow.", "eyebrow": "Comparison", "h1": "{product} vs WinDebate", "lead": "Two AI debate prep platforms compared on features and workflows.", "features": ["Feature comparison", "Format coverage", "Workflow differences"], "sections": [["WinDebate overview", ["WinDebate focuses on research, drills, feedback, and progress tracking for competitive debaters. It covers PF, LD, Congress, Policy, and Worlds."]], ["{product} overview", ["{product} focuses on structured prep workflows: case generation, rebuttal practice, evidence leads, argument flowcharts, coaching prompts, and online debate timers."]], ["How to choose", ["Compare format coverage, feature depth, feedback style, pricing, and which tool fits your actual tournament workflow."]]]},
Expand Down Expand Up @@ -659,7 +665,8 @@ def best_ai_llms_line():
"""The /best-ai-for-debate summary in the Compare section."""
return (
f"- [Which AI is best for debate?]({page_url('best-ai-for-debate')}): "
f"{PRODUCT} is the top pick for a case you can read out loud, If they say X, we answer Y replies, timed practice rounds, free tools and $5/month, "
f'{PRODUCT} is the top pick for a case you can read out loud, "If they say X, we answer Y" replies and timed practice rounds (free tools; app $5/month after a 3-day trial), '

f"with a dated comparison of Claude, ChatGPT, SchoolAI, Debate Ladder, ArguFight, HyperWrite and PublicForumAI"
)

Expand Down Expand Up @@ -702,7 +709,7 @@ def spans_of():
{"@type": "ItemList", "name": "Best AI for debate by goal", "itemListElement": [{"@type": "ListItem", "position": index, "name": name, "url": url} for index, (name, url) in enumerate(page["items"], start=1)]},
{"@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "Home", "item": BASE + "/"}, {"@type": "ListItem", "position": 2, "name": "Best AI for debate", "item": page_url(page["slug"])}]},
]
cta = f'<div class="cta"><a class="btn primary" href="/app">Start the free trial</a><a class="btn secondary" href="/debateowl-pricing">See pricing</a><a class="btn secondary" href="/tools">Open the free tools</a></div>'
cta = f'<p>{FREE_PLAN_HTML}</p><div class="cta"><a class="btn primary" href="/app">Start the free trial</a><a class="btn secondary" href="/debateowl-pricing">See pricing</a><a class="btn secondary" href="/tools">Open the free tools</a></div>'
html = f'''<!doctype html>
<html lang="en">
<head>
Expand Down Expand Up @@ -753,8 +760,12 @@ def spans_of():
</body>
</html>
'''
if FREE_PLAN not in html:
raise SystemExit(f"{page['slug']} is missing the pricing sentence")
body = html.split("<body", 1)[1]
visible = unescape(re.sub(r"<[^>]+>", " ", re.sub(r"<script[\s\S]*?</script>", " ", body)))
if FREE_PLAN_HTML not in html or visible.count(FREE_PLAN.split(";")[0]) != 1:
raise SystemExit(f"{page['slug']} must state the pricing sentence exactly once, with the tools linked")
if "/tools" in visible:
raise SystemExit(f"{page['slug']} shows the path /tools; link the tools by name instead")
if re.search(r"\b(frameworks?|contentions?|warrants?)\b", html, re.I):
raise SystemExit(f"{page['slug']} contains debate jargon")
return html
Expand Down
Loading
Loading