diff --git a/docs/operations.md b/docs/operations.md index 27cf810a..07da68cf 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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//` 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/.txt`. It is not wired into CI. diff --git a/public/favicon.ico b/public/favicon.ico index 9d6d5e20..2e17b59a 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index d62ab12b..2ec67406 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -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}"`); diff --git a/src/assets/owl/owl-favicon.svg b/src/assets/owl/owl-favicon.svg index 4a5491d2..cfaf1f93 100644 --- a/src/assets/owl/owl-favicon.svg +++ b/src/assets/owl/owl-favicon.svg @@ -1,5 +1,5 @@ - diff --git a/src/config/site.json b/src/config/site.json index 3225ce02..7d1967f7 100644 --- a/src/config/site.json +++ b/src/config/site.json @@ -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/" diff --git a/src/lib/seo/pages.ts b/src/lib/seo/pages.ts index a88b87bd..2b1faeb8 100644 --- a/src/lib/seo/pages.ts +++ b/src/lib/seo/pages.ts @@ -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; @@ -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; diff --git a/src/locales/en.json b/src/locales/en.json index 3d6ad63a..d440def6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/es.json b/src/locales/es.json index 98bc9b1f..ec4f891e 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -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", diff --git a/src/locales/hu.json b/src/locales/hu.json index 28e64c4d..7b8811d0 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -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", diff --git a/src/test/seo/brand-entity.test.ts b/src/test/seo/brand-entity.test.ts index 7fb4b483..d0dc62b0 100644 --- a/src/test/seo/brand-entity.test.ts +++ b/src/test/seo/brand-entity.test.ts @@ -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", () => {