From 88626ed414275fbe36e35af80e59f17cb7c1a549 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 3 Nov 2026 16:58:57 +0000 Subject: [PATCH] List DebateOwl's SaaSHub and AITL profiles in sameAs The Organization and the home page's SoftwareApplication now name the GitHub repository, the renamed SaaSHub listing and the AITL listing as sameAs. llms.txt lists the same profiles, generated from site.json. Uneed stays out while it still uses the old DebateLab slug. The landing page's lastmod now also follows src/config/site.json, which it renders; /about and /press move to 2026-11-03. Co-Authored-By: Claude Opus 5.5 --- docs/operations.md | 2 +- public/llms.txt | 1 + scripts/generate_seo_content.py | 1 + scripts/prerender.mjs | 2 +- src/config/site.json | 6 +++++- src/lib/seo/pages.ts | 4 ++-- src/prerender/entry.tsx | 4 +++- 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/operations.md b/docs/operations.md index 7b081685..062c667d 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -80,7 +80,7 @@ Every guide opens with the reader's main question as its first H2 and an answer To retire a guide, delete its `PAGES` entry, its `CLUSTERS` and `LINK_PHRASES` entries, and its `public//` folder (the generator never deletes), add its redirects to `vercel.json`, and repoint any link to it. Two guides that answer the same search should become one page, not two thin ones. -The site origin, product name, brand line, organization name and the organization's `sameAs` profiles live in [`src/config/site.json`](../src/config/site.json). List a profile in `sameAs` only after checking that its live page spells the name "DebateOwl"; on 29 October 2026 the only two listings (SaaSHub and Uneed) still said "DebateLab", so the list is empty and the Organization node leaves `sameAs` out. The app's head tags, the prerender and sitemap scripts, and the guide generator all read it, and guide copy names the product with a `{product}` token and the Pro price with `{pro_price}`. The number of prep modes comes from the tool lists in [`tool-links.tsx`](../src/components/features/tools/tool-links.tsx): the app's copy reads `PREP_MODE_COUNT` and `UTILITY_COUNT`, and guide copy uses `{mode_count}`, `{mode_names}` and `{utility_names}`. The trial length and Pro price come from `pricing` in the same file: i18next passes `{{trialDays}}`, `{{proMonthly}}`, `{{product}}`, `{{brand}}` and `{{host}}` to every catalog string, so the English, Spanish and Hungarian pages state the same facts. Do not hardcode those strings in new code or copy. App strings name the product with `{{product}}`, `{{brand}}` and `{{host}}`, which [`src/i18n.ts`](../src/i18n.ts) fills from `site.json` for every key. +The site origin, product name, brand line, organization name and the organization's `sameAs` profiles live in [`src/config/site.json`](../src/config/site.json). List a profile in `sameAs` only after checking that its live page spells the name "DebateOwl"; the home page's Organization and SoftwareApplication nodes both carry the list. As of 3 November 2026 it holds the GitHub repository, SaaSHub (renamed from its old DebateLab slug, which now 404s) and Artificial Intelligence Tools List (AITL). Uneed still lists the app under its old DebateLab slug, so it stays out until that changes. The app's head tags, the prerender and sitemap scripts, and the guide generator all read it, and guide copy names the product with a `{product}` token and the Pro price with `{pro_price}`. The number of prep modes comes from the tool lists in [`tool-links.tsx`](../src/components/features/tools/tool-links.tsx): the app's copy reads `PREP_MODE_COUNT` and `UTILITY_COUNT`, and guide copy uses `{mode_count}`, `{mode_names}` and `{utility_names}`. The trial length and Pro price come from `pricing` in the same file: i18next passes `{{trialDays}}`, `{{proMonthly}}`, `{{product}}`, `{{brand}}` and `{{host}}` to every catalog string, so the English, Spanish and Hungarian pages state the same facts. Do not hardcode those strings in new code or copy. App strings name the product with `{{product}}`, `{{brand}}` and `{{host}}`, which [`src/i18n.ts`](../src/i18n.ts) fills from `site.json` for every key. After a production deploy, and only with Adam's approval, `npm run seo:indexnow` sends every URL in the sitemap to IndexNow in one request to `api.indexnow.org`, which shares it with Bing, Yandex, Seznam, Naver and the other engines that take part (not Google). It reads `dist/sitemap.xml` when a build is present and the live sitemap otherwise. `-- --only /tools /` sends just those pages, and `-- --dry-run` prints the request without sending it. The key file is `public/.txt`. It is not wired into CI. diff --git a/public/llms.txt b/public/llms.txt index 0849c9dd..03f56c3d 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -17,6 +17,7 @@ DebateOwl runs at https://debateowl.com/. It is a separate product from debatela - Trial and free tools: The DebateOwl app has no free plan (3-day free trial, then $5/month); the separate tools at /tools are free with no account. - Pro adds expanded AI prep access, all 10 models (including Claude Opus and Sonnet, GPT-6.1 Sol and Gemini), Active Grounding research and priority support. - Privacy: cases, sources, practice rounds and history stay behind sign-in and are not indexed. +- Listed elsewhere: https://github.com/Programcode/DebateOwl, https://www.saashub.com/debateowl, https://www.artificialintelligencetoolslist.com/ai-tools/debateowl ## Main pages diff --git a/scripts/generate_seo_content.py b/scripts/generate_seo_content.py index 618f1764..b943eb4a 100644 --- a/scripts/generate_seo_content.py +++ b/scripts/generate_seo_content.py @@ -883,6 +883,7 @@ def llms_txt(): - Trial and free tools: {FREE_PLAN} - Pro adds expanded AI prep access, all 10 models (including Claude Opus and Sonnet, GPT-6.1 Sol and Gemini), Active Grounding research and priority support. - Privacy: cases, sources, practice rounds and history stay behind sign-in and are not indexed. +- Listed elsewhere: {", ".join(ORGANIZATION["sameAs"])} ## Main pages diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs index 375c1d67..8aa4416c 100644 --- a/scripts/prerender.mjs +++ b/scripts/prerender.mjs @@ -11,7 +11,7 @@ import { createServer } from 'vite'; const DIST = 'dist'; const SITE_ORIGIN = JSON.parse(readFileSync('src/config/site.json', 'utf8')).origin; // The landing page's lastmod is the last commit that changed these. -const LANDING_SOURCES = ['src/pages/LandingPage.tsx', 'src/components/landing', 'src/lib/seo/pages.ts']; +const LANDING_SOURCES = ['src/pages/LandingPage.tsx', 'src/components/landing', 'src/lib/seo/pages.ts', 'src/config/site.json']; const escapeAttribute = (value) => value .replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>'); diff --git a/src/config/site.json b/src/config/site.json index bc0590aa..abbdf740 100644 --- a/src/config/site.json +++ b/src/config/site.json @@ -19,7 +19,11 @@ }, "description": "DebateOwl (formerly DebateLab at debate.pi-stack.org) is an AI debate prep and practice app for students and competitive debaters.", "disambiguatingDescription": "DebateOwl is an AI debate prep and practice app for students, not a sports or computer-vision product.", - "sameAs": ["https://github.com/Programcode/DebateOwl"], + "sameAs": [ + "https://github.com/Programcode/DebateOwl", + "https://www.saashub.com/debateowl", + "https://www.artificialintelligencetoolslist.com/ai-tools/debateowl" + ], "operator": { "name": "Ádám Gáborjáni", "country": "Hungary", diff --git a/src/lib/seo/pages.ts b/src/lib/seo/pages.ts index 4fb6d375..2a1754d0 100644 --- a/src/lib/seo/pages.ts +++ b/src/lib/seo/pages.ts @@ -14,8 +14,8 @@ export const ORGANIZATION_ID = `${SITE_ORIGIN}/#organization`; * see one name for one entity. Its founder is the person who runs it; Pi-Stack * is only the name he publishes under, not a company, so it appears as his URL * and never as a parent organization or legal name. A sameAs profile is the same - * entity elsewhere: site.json lists only profiles that spell the name "DebateOwl" - * and that DebateOwl itself runs; with none, the node has no sameAs. /press, the + * entity elsewhere: site.json lists only pages about DebateOwl that spell the name + * "DebateOwl" (its repository and directory listings); with none, the node has no sameAs. /press, the * fact sheet, is its subjectOf. */ export function organizationNode() { diff --git a/src/prerender/entry.tsx b/src/prerender/entry.tsx index 4ec0fec8..13c40a53 100644 --- a/src/prerender/entry.tsx +++ b/src/prerender/entry.tsx @@ -127,6 +127,8 @@ function landingStructuredData(dateModified: string | undefined) { image: `${SITE_ORIGIN}/hero-visual.jpg?v=2`, description: SOFTWARE_DESCRIPTION, disambiguatingDescription: site.organization.disambiguatingDescription, + // The same listings as the Organization: each is a page about the app itself. + ...(site.organization.sameAs.length ? { sameAs: site.organization.sameAs } : {}), inLanguage: ['en', 'es', 'hu'], featureList: [...APP_TOOLS.map((tool) => i18n.t(`features.${tool.key}`)), EXPORT_FEATURE], // One paid offer. Google reads an Offer priced 0 as an app available without @@ -199,7 +201,7 @@ function legalStructuredData(route: '/terms' | '/privacy', page: 'termsPage' | ' } // The day each page's copy last changed; move it when its text does. -const PAGE_MODIFIED = { '/about': '2026-11-02', '/contact': '2026-10-28', '/press': '2026-11-02' }; +const PAGE_MODIFIED = { '/about': '2026-11-03', '/contact': '2026-10-28', '/press': '2026-11-03' }; /** * /about as an AboutPage and /press as a WebPage, each about the Organization,