diff --git a/docs/operations.md b/docs/operations.md index e79cc166b..f8d17317b 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -36,7 +36,7 @@ If Actions jobs fail within 2 to 5 seconds with no runner and no logs, the accou ## Frontend hosting on Vercel -Vercel builds and serves the frontend from the Git repository. A push to `main` deploys both projects: `debate-navigator` to `https://debateowl.com` (the previous domain, `https://debate.pi-stack.org`, 301s there with the path and query kept; see `vercel.json`), and `debatelab-staging` to `https://debatelab-staging.vercel.app`. Any other branch waits for an explicit preview (see [Opt-in previews](#opt-in-previews)). For a staging-pinned preview on the tailnet, use `npm run dev:preview` (see [development.md](development.md#preview-on-the-tailnet)). The production canary runs after each successful Vercel `Production` deployment of `debate-navigator`. +Vercel builds and serves the frontend from the Git repository. A push to `main` deploys both projects: `debate-navigator` to `https://debateowl.com` (the previous domain, `https://debate.pi-stack.org`, 301s there with the path and query kept; see `vercel.json`; `https://www.debateowl.com` redirects to the apex through the project's Domains setting, not `vercel.json`, also keeping the path and query), and `debatelab-staging` to `https://debatelab-staging.vercel.app`. Any other branch waits for an explicit preview (see [Opt-in previews](#opt-in-previews)). For a staging-pinned preview on the tailnet, use `npm run dev:preview` (see [development.md](development.md#preview-on-the-tailnet)). The production canary runs after each successful Vercel `Production` deployment of `debate-navigator`. [`vercel.json`](../vercel.json) owns: diff --git a/e2e/loading-performance.spec.ts b/e2e/loading-performance.spec.ts index 101d46f38..74b12e432 100644 --- a/e2e/loading-performance.spec.ts +++ b/e2e/loading-performance.spec.ts @@ -87,7 +87,7 @@ test('landing CTA enters the app without replacing the document', async ({ page await page.goto('/'); await page.evaluate(() => { document.documentElement.dataset.e2eDocument = 'original'; }); const initialDocumentRequests = documentRequests; - await page.getByRole('link', { name: 'Start trial' }).first().click(); + await page.getByRole('link', { name: 'Start free trial' }).first().click(); await expect(page).toHaveURL(/\/app$/); expect(documentRequests).toBe(initialDocumentRequests); diff --git a/index.html b/index.html index 36584c3ad..45399ba7b 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@ DebateOwl - + + + + + + + + + diff --git a/src/components/free-tools/ToolPageLayout.tsx b/src/components/free-tools/ToolPageLayout.tsx index 11e85f787..f26c55595 100644 --- a/src/components/free-tools/ToolPageLayout.tsx +++ b/src/components/free-tools/ToolPageLayout.tsx @@ -115,7 +115,7 @@ export function ToolPageLayout({ route, title, intro, shortAnswer, tool, childre
- + {site.product} diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx index dcbbf6494..88bb049f9 100644 --- a/src/components/landing/Footer.tsx +++ b/src/components/landing/Footer.tsx @@ -14,7 +14,7 @@ const Footer = () => {
{/* In-app links: no full reload through the startup shell. */} - + {site.product} diff --git a/src/components/landing/Navbar.tsx b/src/components/landing/Navbar.tsx index 222ec7a89..f9ed8d538 100644 --- a/src/components/landing/Navbar.tsx +++ b/src/components/landing/Navbar.tsx @@ -6,6 +6,7 @@ import { Menu, X, ArrowRight } from "lucide-react"; import { Link } from "react-router-dom"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { useRoutePrefetch } from "@/hooks/use-route-prefetch"; +import { localizedPath, useRouteLanguage } from "@/lib/seo/localized-routes"; import { cn } from "@/lib/utils"; const Navbar = () => { @@ -13,6 +14,7 @@ const Navbar = () => { const [isOpen, setIsOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const appPrefetch = useRoutePrefetch("/app"); + const home = localizedPath("/", useRouteLanguage()); // The nav is static on load; its edge appears once content scrolls under it. useEffect(() => { @@ -38,7 +40,7 @@ const Navbar = () => { >
- + {site.product} diff --git a/src/components/layout/PublicPageShell.tsx b/src/components/layout/PublicPageShell.tsx index 24bb07c0e..5b51bd1a3 100644 --- a/src/components/layout/PublicPageShell.tsx +++ b/src/components/layout/PublicPageShell.tsx @@ -27,7 +27,7 @@ export function PublicPageShell({ children, className }: { children: ReactNode;
- + {site.product} diff --git a/src/locales/en.json b/src/locales/en.json index 5990080fd..b06a60c40 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -15,7 +15,7 @@ "features": "Features", "pricing": "Pricing", "howItWorks": "How it works", - "startTrial": "Start trial", + "startTrial": "Start free trial", "settings": "Settings", "openMenu": "Open menu", "closeMenu": "Close menu", @@ -234,6 +234,7 @@ "loading": "Loading", "error": "Error", "logoAlt": "DebateOwl logo", + "homeLink": "DebateOwl home", "skipToContent": "Skip to main content", "or": "or", "back": "Back", @@ -895,7 +896,7 @@ "headline": "{{product}}: AI debate prep and practice", "subheadline": "Prepare cases and practice rounds with AI.", "trialBadge": "{{trialDays}}-day free trial", - "cta": "Start trial", + "cta": "Start free trial", "trustBullet1": "No credit card required", "trustBullet2": "Cancel anytime", "preview": { @@ -1037,7 +1038,7 @@ }, "finalCta": { "title": "Ready to build your next case?", - "cta": "Launch DebateOwl" + "cta": "Start free trial" }, "rateLimit": { "title": "Too many requests" @@ -2587,44 +2588,44 @@ }, "pageHead": { "home": { - "title": "{{brand}} | AI Debate Prep, Cases & Rebuttals", - "description": "{{product}} is an AI debate prep and practice app for students and debaters: cases, rebuttals, evidence, coaching and timed rounds against an AI opponent." + "title": "{{product}}: AI debate prep and practice", + "description": "{{product}} helps students and debaters prepare cases, rebuttals and evidence with AI, then practice timed rounds against an AI opponent." }, "terms": { "title": "Terms of Service | {{brand}}", - "description": "The terms for using {{brand}}: accounts, subscriptions and billing, acceptable use, AI-generated content, intellectual property, and liability." + "description": "The terms for using {{brand}}, covering accounts, subscriptions and billing, acceptable use, AI-generated content, intellectual property and liability." }, "privacy": { "title": "Privacy Policy | {{brand}}", - "description": "How {{brand}} collects, uses, stores, and shares your information, how AI requests are processed, and the rights you have over your data." + "description": "How {{brand}} collects, uses, stores and shares your information, how it processes AI requests, and which rights you have over your data." }, "sources": { "title": "Sources | {{brand}}", - "description": "The sources consulted during the research and development of {{brand}}, listed alphabetically by author surname or institutional name." + "description": "The books, articles and institutions consulted while researching and building {{brand}}, listed alphabetically by author or institution name." }, "notFound": { "title": "Page not found | {{brand}}", "description": "This page does not exist on {{brand}}." }, "tools": { - "title": "Free Debate Tools: Timer, Motions, Flow Sheets | {{brand}}", - "description": "Free debate tools that need no account: a format-aware round timer, a practice motion generator, printable flow sheets and a speech time calculator." + "title": "Free Debate Tools | {{brand}}", + "description": "Free debate tools that need no account: a round timer for every format, a practice motion generator, printable flow sheets and a speech time calculator." }, "debateTimer": { - "title": "Free Debate Timer for PF, LD, Policy, BP & World Schools | {{brand}}", - "description": "A free debate timer with presets for Public Forum, LD, Policy, BP, World Schools and Asian Parliamentary: protected time, POI knocks and prep banks." + "title": "Free Debate Timer | {{brand}}", + "description": "Free debate timer with presets for Public Forum, LD, Policy, BP, World Schools and Asian Parliamentary, including protected time and POI knocks." }, "motionGenerator": { - "title": "Debate Motion Generator: 300 Practice Motions | {{brand}}", - "description": "Draw a random debate motion, or three for a veto drill, from 300 original practice motions sorted by topic, difficulty and type. THW and Resolved wording." + "title": "Debate Motion Generator | {{brand}}", + "description": "Free debate motion generator that draws one random motion, or three for a veto drill, from 300 original practice motions sorted by topic and type." }, "flowSheet": { - "title": "Printable Debate Flow Sheet Generator | {{brand}}", - "description": "Print a one-page debate flow sheet with a column for every speech in PF, LD, Policy, BP, World Schools or Asian Parliamentary, on A4 or Letter." + "title": "Printable Debate Flow Sheets | {{brand}}", + "description": "Free printable debate flow sheets with a column for every speech in PF, LD, Policy, BP, World Schools or Asian Parliamentary, on A4 or Letter." }, "speechTimeCalculator": { - "title": "Speech Time Calculator for Debate Speeches | {{brand}}", - "description": "Paste a speech to see its word count, how long it takes to deliver at slow, average and fast pace, and which debate speech slots it fits. Runs in your browser." + "title": "Speech Time Calculator | {{brand}}", + "description": "Free speech time calculator that counts the words in a debate speech and shows how long it takes at a slow, average or fast pace." } }, "validation": { diff --git a/src/locales/es.json b/src/locales/es.json index 52580c016..570d75bc9 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -15,7 +15,7 @@ "features": "Funciones", "pricing": "Precios", "howItWorks": "Cómo funciona", - "startTrial": "Prueba gratis", + "startTrial": "Comenzar prueba gratis", "settings": "Ajustes", "openMenu": "Abrir menú", "closeMenu": "Cerrar menú", @@ -234,6 +234,7 @@ "loading": "Cargando", "error": "Error", "logoAlt": "Logotipo de DebateOwl", + "homeLink": "Inicio de DebateOwl", "skipToContent": "Saltar al contenido principal", "or": "o", "back": "Atrás", @@ -895,7 +896,7 @@ "headline": "{{product}}: preparación y práctica de debate con IA", "subheadline": "Prepara casos y practica rondas con IA.", "trialBadge": "Prueba gratis de {{trialDays}} días", - "cta": "Prueba gratis", + "cta": "Comenzar prueba gratis", "trustBullet1": "Sin tarjeta de crédito", "trustBullet2": "Cancela cuando quieras", "preview": { @@ -1037,7 +1038,7 @@ }, "finalCta": { "title": "¿Listo para construir tu próximo caso?", - "cta": "Lanzar DebateOwl" + "cta": "Comenzar prueba gratis" }, "rateLimit": { "title": "Demasiadas solicitudes" @@ -2587,8 +2588,8 @@ }, "pageHead": { "home": { - "title": "{{product}} – Preparación y práctica de debate con IA para estudiantes", - "description": "{{product}} es una app con IA para preparar y practicar debates: casos, refutaciones, evidencia, coaching y rondas cronometradas contra un oponente de IA." + "title": "{{product}}: preparación y práctica de debate con IA", + "description": "{{product}} ayuda a estudiantes y debatientes a preparar casos, refutaciones y evidencia con IA, y a practicar rondas cronometradas contra un oponente de IA." }, "terms": { "title": "Términos del servicio | {{brand}}", @@ -2607,24 +2608,24 @@ "description": "Esta página no existe en {{brand}}." }, "tools": { - "title": "Herramientas de debate gratis: cronómetro, mociones, hojas de flujo | {{brand}}", - "description": "Herramientas de debate gratis y sin cuenta: cronómetro por formato, generador de mociones, hojas de flujo imprimibles y calculadora de tiempo de discurso." + "title": "Herramientas de debate gratis | {{brand}}", + "description": "Gratis y sin cuenta: un cronómetro para cada formato, un generador de mociones, hojas de flujo imprimibles y una calculadora de tiempo de discurso." }, "debateTimer": { - "title": "Cronómetro de debate gratis para PF, LD, Policy, BP y World Schools | {{brand}}", - "description": "Cronómetro de debate gratis con ajustes para PF, LD, Policy, BP, World Schools y Asian Parliamentary: tiempo protegido, avisos de POI y preparación." + "title": "Cronómetro de debate gratis | {{brand}}", + "description": "Cronómetro de debate gratis con ajustes para PF, LD, Policy, BP, World Schools y Asian Parliamentary, con tiempo protegido y avisos de POI." }, "motionGenerator": { - "title": "Generador de mociones de debate: 300 mociones de práctica | {{brand}}", - "description": "Saca una moción de debate al azar, o tres para practicar el veto, de entre 300 mociones originales ordenadas por tema, dificultad y tipo." + "title": "Generador de mociones de debate | {{brand}}", + "description": "Generador gratis de mociones de debate que saca una moción al azar, o tres para practicar el veto, de 300 mociones originales por tema y tipo." }, "flowSheet": { - "title": "Generador de hojas de flujo de debate imprimibles | {{brand}}", - "description": "Imprime una hoja de flujo de una página con una columna por discurso en PF, LD, Policy, BP, World Schools o Asian Parliamentary, en A4 o Carta." + "title": "Hojas de flujo de debate imprimibles | {{brand}}", + "description": "Hojas de flujo de debate gratis e imprimibles, con una columna por discurso en PF, LD, Policy, BP, World Schools o Asian Parliamentary, en A4 o Carta." }, "speechTimeCalculator": { - "title": "Calculadora de tiempo para discursos de debate | {{brand}}", - "description": "Pega un discurso para ver cuántas palabras tiene, cuánto dura a ritmo lento, medio o rápido y en qué turnos de debate cabe. Funciona en tu navegador." + "title": "Calculadora de tiempo de discurso | {{brand}}", + "description": "Calculadora gratis que cuenta las palabras de un discurso de debate y muestra cuánto dura a ritmo lento, medio o rápido y en qué turnos cabe." } }, "validation": { diff --git a/src/locales/hu.json b/src/locales/hu.json index a8d74a64f..dd30fbab1 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -15,7 +15,7 @@ "features": "Funkciók", "pricing": "Árak", "howItWorks": "Hogyan működik", - "startTrial": "Próba indítása", + "startTrial": "Ingyenes próba indítása", "settings": "Beállítások", "openMenu": "Menü megnyitása", "closeMenu": "Menü bezárása", @@ -234,6 +234,7 @@ "loading": "Betöltés", "error": "Hiba", "logoAlt": "DebateOwl logó", + "homeLink": "DebateOwl főoldal", "skipToContent": "Ugrás a fő tartalomra", "or": "vagy", "back": "Vissza", @@ -895,7 +896,7 @@ "headline": "{{product}}: MI-alapú vitafelkészítés és gyakorlás", "subheadline": "Készíts eseteket és gyakorolj fordulókat MI-vel.", "trialBadge": "{{trialDays}} napos ingyenes próba", - "cta": "Próba indítása", + "cta": "Ingyenes próba indítása", "trustBullet1": "Bankkártya nem szükséges", "trustBullet2": "Bármikor lemondható", "preview": { @@ -1037,7 +1038,7 @@ }, "finalCta": { "title": "Készen állsz a következő eseted felépítésére?", - "cta": "DebateOwl indítása" + "cta": "Ingyenes próba indítása" }, "rateLimit": { "title": "Túl sok kérés" @@ -2587,8 +2588,8 @@ }, "pageHead": { "home": { - "title": "{{product}} – MI-alapú vitafelkészülés és gyakorlás diákoknak", - "description": "A {{product}} MI-alapú app vitára készülő diákoknak: esetépítés, cáfolatok, bizonyítékok, coaching és időzített gyakorlókörök egy MI-ellenféllel szemben." + "title": "{{product}}: MI-alapú vitafelkészülés és gyakorlás", + "description": "A {{product}} MI segítségével készít fel diákokat és vitázókat: esetek, cáfolatok és bizonyítékok, majd időzített gyakorlókörök egy MI-ellenféllel." }, "terms": { "title": "Felhasználási feltételek | {{brand}}", @@ -2607,24 +2608,24 @@ "description": "Ez az oldal nem létezik a {{brand}} webhelyén." }, "tools": { - "title": "Ingyenes vitaeszközök: időmérő, vitatételek, jegyzetlapok | {{brand}}", + "title": "Ingyenes vitaeszközök | {{brand}}", "description": "Ingyenes, fiók nélkül használható vitaeszközök: formátumhoz igazodó időmérő, vitatétel-generátor, nyomtatható jegyzetlapok és beszédidő-kalkulátor." }, "debateTimer": { - "title": "Ingyenes vitaidőmérő PF, LD, Policy, BP és World Schools formátumhoz | {{brand}}", - "description": "Ingyenes vitaidőmérő PF, LD, Policy, BP, World Schools és Asian Parliamentary beállításokkal: védett idő, POI-jelzés és felkészülési idő." + "title": "Ingyenes vitaidőmérő | {{brand}}", + "description": "Ingyenes vitaidőmérő PF, LD, Policy, BP, World Schools és Asian Parliamentary beállításokkal, védett idővel, POI-jelzéssel és felkészülési idővel." }, "motionGenerator": { - "title": "Vitatétel-generátor: 300 gyakorló vitatétel | {{brand}}", - "description": "Húzz egy véletlen vitatételt, vagy hármat a vétógyakorláshoz, 300 eredeti gyakorló tételből, téma, nehézség és típus szerint rendezve." + "title": "Vitatétel-generátor | {{brand}}", + "description": "Ingyenes vitatétel-generátor, amely egy véletlen tételt, vagy vétógyakorláshoz hármat húz 300 eredeti, téma és típus szerint rendezett tételből." }, "flowSheet": { - "title": "Nyomtatható vitajegyzetlap-generátor | {{brand}}", - "description": "Egyoldalas, nyomtatható vitajegyzetlap minden beszédhez külön oszloppal PF, LD, Policy, BP, World Schools vagy Asian Parliamentary formátumhoz." + "title": "Nyomtatható vitajegyzetlapok | {{brand}}", + "description": "Ingyenes, nyomtatható vitajegyzetlap minden beszédhez külön oszloppal PF, LD, Policy, BP, World Schools vagy Asian Parliamentary formátumhoz." }, "speechTimeCalculator": { - "title": "Beszédidő-kalkulátor vitabeszédekhez | {{brand}}", - "description": "Illessz be egy beszédet, és megmutatjuk a szószámát, hogy lassú, átlagos és gyors tempóban mennyi ideig tart, és mely vitabeszéd-idősávokba fér bele." + "title": "Beszédidő-kalkulátor | {{brand}}", + "description": "Ingyenes beszédidő-kalkulátor, amely megszámolja egy vitabeszéd szavait, és megmutatja, mennyi ideig tart lassú, átlagos és gyors tempóban." } }, "validation": { diff --git a/src/pages/tools/ToolsHubPage.tsx b/src/pages/tools/ToolsHubPage.tsx index 1e46cb47c..d6d82a1ff 100644 --- a/src/pages/tools/ToolsHubPage.tsx +++ b/src/pages/tools/ToolsHubPage.tsx @@ -39,7 +39,7 @@ export default function ToolsHubPage() { -

DebateOwl is a paid practice app for debaters: it drafts cases for either side of a motion, suggests rebuttals, and runs sparring rounds against an AI opponent.

+

DebateOwl is an AI debate prep and practice app: it drafts cases for either side of a motion, suggests rebuttals, and runs sparring rounds against an AI opponent.

); diff --git a/src/pages/tools/localized/copy-es.ts b/src/pages/tools/localized/copy-es.ts index dd4cb04f5..fbf288ff7 100644 --- a/src/pages/tools/localized/copy-es.ts +++ b/src/pages/tools/localized/copy-es.ts @@ -38,7 +38,7 @@ export const COPY_ES: LocalizedToolsCopy = { faq: [], cta: { title: 'Cuando necesitas algo más que un reloj', - body: '{product} es una app de práctica de pago para debatientes: redacta casos para cualquiera de los dos lados de una moción, sugiere refutaciones y organiza rondas de Sparring contra un oponente de IA.', + body: '{product} es una app de preparación y práctica de debate con IA: redacta casos para cualquiera de los dos lados de una moción, sugiere refutaciones y organiza rondas de Sparring contra un oponente de IA.', }, }, diff --git a/src/pages/tools/localized/copy-hu.ts b/src/pages/tools/localized/copy-hu.ts index ba4a00178..02d13c327 100644 --- a/src/pages/tools/localized/copy-hu.ts +++ b/src/pages/tools/localized/copy-hu.ts @@ -38,7 +38,7 @@ export const COPY_HU: LocalizedToolsCopy = { faq: [], cta: { title: 'Ha már nem csak időmérőre van szükséged', - body: 'A {product} fizetős gyakorlóalkalmazás vitázóknak: egy tétel bármelyik oldalára megírja az eset vázlatát, cáfolatokat javasol, és Vitapárbaj-fordulókat futtat egy MI-ellenféllel.', + body: 'A {product} MI-alapú vitafelkészítő és gyakorlóalkalmazás: egy tétel bármelyik oldalára megírja az eset vázlatát, cáfolatokat javasol, és Vitapárbaj-fordulókat futtat egy MI-ellenféllel.', }, }, diff --git a/src/prerender/entry.tsx b/src/prerender/entry.tsx index 0b58c1320..55189c435 100644 --- a/src/prerender/entry.tsx +++ b/src/prerender/entry.tsx @@ -170,10 +170,24 @@ function localizedLandingStructuredData(head: PageHead, language: PageLanguage, }; } -function legalStructuredData(route: '/terms' | '/privacy', dateKey: string) { +/** Home, then the page: the trail the guides and tool pages already carry. */ +function breadcrumbList(route: '/terms' | '/privacy' | '/expocit/sources', name: string) { + return { + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: PRODUCT, item: `${SITE_ORIGIN}/` }, + { '@type': 'ListItem', position: 2, name, item: `${SITE_ORIGIN}${route}` }, + ], + }; +} + +function legalStructuredData(route: '/terms' | '/privacy', page: 'termsPage' | 'privacyPage') { return { '@context': 'https://schema.org', - '@graph': [webPage(route, isoDate(i18n.t(dateKey, { lng: 'en' })))], + '@graph': [ + webPage(route, isoDate(i18n.t(`${page}.date`, { lng: 'en' }))), + breadcrumbList(route, i18n.t(`${page}.title`, { lng: 'en' })), + ], }; } @@ -245,7 +259,7 @@ export async function prerenderPages({ landingModified }: { landingModified?: st module: 'src/pages/TermsPage.tsx', head: PAGE_HEADS['/terms'], body: render('/terms', TermsPage), - structuredData: legalStructuredData('/terms', 'termsPage.date'), + structuredData: legalStructuredData('/terms', 'termsPage'), }, { route: '/privacy', @@ -253,7 +267,7 @@ export async function prerenderPages({ landingModified }: { landingModified?: st module: 'src/pages/PrivacyPage.tsx', head: PAGE_HEADS['/privacy'], body: render('/privacy', PrivacyPage), - structuredData: legalStructuredData('/privacy', 'privacyPage.date'), + structuredData: legalStructuredData('/privacy', 'privacyPage'), }, // The tool pages render their own JSON-LD in the body. ...TOOL_PAGES.map(({ route, module, Page }) => ({ @@ -270,6 +284,10 @@ export async function prerenderPages({ landingModified }: { landingModified?: st module: 'src/pages/SourcesPage.tsx', head: PAGE_HEADS['/expocit/sources'], body: render('/expocit/sources', SourcesPage), + structuredData: { + '@context': 'https://schema.org', + '@graph': [webPage('/expocit/sources', undefined), breadcrumbList('/expocit/sources', i18n.t('sourcesPage.title'))], + }, }, { route: '*',