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/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,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/<slug>/` 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). 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"; 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.

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/<key>.txt`. It is not wired into CI.

Expand Down
Binary file modified public/favicon.ico
Binary file not shown.
3 changes: 2 additions & 1 deletion scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ function checkStructuredData(where, data, body) {
if (!Array.isArray(byType('WebSite').alternateName)) report(where, 'WebSite.alternateName is not a list');
const organization = byType('Organization');
const sameAs = organization.sameAs;
if (!Array.isArray(sameAs) || !sameAs.length || sameAs.some((url) => !url.startsWith('https://'))) report(where, 'Organization.sameAs is not a list of https URLs');
// sameAs is optional (site.json lists only profiles that spell the name "DebateOwl"), but when present it is a non-empty list of https URLs.
if (sameAs !== undefined && (!Array.isArray(sameAs) || !sameAs.length || sameAs.some((url) => !url.startsWith('https://')))) report(where, 'Organization.sameAs is not a list of https URLs');
// One name for one entity: the Organization on this domain is the product, and the company
// that builds it is its parent, never a sameAs.
if (organization.name !== SITE.product) report(where, `Organization.name is "${organization.name}", not the product name "${SITE.product}"`);
Expand Down
2 changes: 1 addition & 1 deletion src/assets/owl/owl-favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions src/config/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@
"height": 512
},
"description": "DebateOwl (formerly DebateLab at debate.pi-stack.org) is an AI debate prep and practice app for students and competitive debaters.",
"sameAs": [
"https://www.saashub.com/debatelab",
"https://www.uneed.best/tool/debatelab"
],
"sameAs": [],
"parent": {
"name": "Pi-Stack",
"url": "https://pi-stack.org/"
Expand Down
5 changes: 3 additions & 2 deletions src/lib/seo/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export const ORGANIZATION_ID = `${SITE_ORIGIN}/#organization`;
/**
* The Organization behind the site, named for the product so search engines
* see one name for one entity. Pi-Stack, which builds it, is its parent, not a
* sameAs: a sameAs profile is the same entity elsewhere.
* sameAs: a sameAs profile is the same entity elsewhere. site.json lists only
* profiles that spell the name "DebateOwl"; with none, the node has no sameAs.
*/
export function organizationNode() {
const { name, logo, description, sameAs, parent } = site.organization;
Expand All @@ -27,7 +28,7 @@ export function organizationNode() {
email: site.contactEmail,
contactPoint: { '@type': 'ContactPoint', contactType: 'customer support', email: site.contactEmail, availableLanguage: ['en', 'hu', 'es'] },
parentOrganization: { '@type': 'Organization', '@id': `${parent.url}#organization`, name: parent.name, url: parent.url },
sameAs,
...(sameAs.length ? { sameAs } : {}),
};
}
export const PRICING = site.pricing;
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@
},
"hero": {
"headline": "{{product}}: AI debate prep and practice",
"subheadline": "Prepare cases and practice rounds with AI.",
"subheadline": "{{product}} helps you prepare cases and practice rounds with AI.",
"trialBadge": "{{trialDays}}-day free trial",
"cta": "Start free trial",
"trustBullet1": "No credit card required",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@
},
"hero": {
"headline": "{{product}}: preparación y práctica de debate con IA",
"subheadline": "Prepara casos y practica rondas con IA.",
"subheadline": "{{product}} te ayuda a preparar casos y practicar rondas con IA.",
"trialBadge": "Prueba gratis de {{trialDays}} días",
"cta": "Comenzar prueba gratis",
"trustBullet1": "Sin tarjeta de crédito",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@
},
"hero": {
"headline": "{{product}}: MI-alapú vitafelkészítés és gyakorlás",
"subheadline": "Készíts eseteket és gyakorolj fordulókat MI-vel.",
"subheadline": "A {{product}} segít eseteket készíteni és fordulókat gyakorolni MI-vel.",
"trialBadge": "{{trialDays}} napos ingyenes próba",
"cta": "Ingyenes próba indítása",
"trustBullet1": "Bankkártya nem szükséges",
Expand Down
6 changes: 4 additions & 2 deletions src/test/seo/brand-entity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ describe("brand entity", () => {
logo: { "@type": "ImageObject", url: `${site.origin}/icon-512.png`, width: 512, height: 512 },
parentOrganization: { "@type": "Organization", name: "Pi-Stack", url: "https://pi-stack.org/" },
});
expect(node.sameAs.length).toBeGreaterThan(0);
expect(node.sameAs).not.toContain("https://pi-stack.org/");
// Only profiles that spell the name "DebateOwl" belong here, so the list may be empty; then the key is left out.
const sameAs: string[] = site.organization.sameAs;
expect(node).toEqual(sameAs.length ? expect.objectContaining({ sameAs }) : expect.not.objectContaining({ sameAs: expect.anything() }));
expect(sameAs).not.toContain("https://pi-stack.org/");
});

it("finds the guides", () => {
Expand Down
Loading