From 611d9da6588d6796059526572a911da4b2bb03f0 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sat, 15 Aug 2026 04:54:40 +0530 Subject: [PATCH] test(seo): add sitemap/canonical parity regression check Closes #47 Issue #47: the live sitemap listed /faq and /login with the homepage canonical. The runtime fixes (self-canonical for /faq, /login removed from the sitemap, noindex on /login) shipped in #48. This adds the missing regression guard so the parity cannot silently regress: - For every URL in public/sitemap.xml, resolve its landing-astro source page and assert it declares a self-canonical (canonicalPath == its own pathname). The homepage keeps the Layout default '/'. - Assert /login never re-enters the sitemap. - Reuses the existing sitemapUrls() helper and AGENT_SURFACE catalog. --- src/agent-surfaces.test.ts | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/agent-surfaces.test.ts b/src/agent-surfaces.test.ts index 59487a4..3b73e43 100644 --- a/src/agent-surfaces.test.ts +++ b/src/agent-surfaces.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { AGENT_SURFACE } from './agent-edge.mjs'; const readPublic = (path: string) => readFileSync(resolve(process.cwd(), 'public', path), 'utf8'); +const readRepo = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8'); function sitemapUrls(): string[] { return [...readPublic('sitemap.xml').matchAll(/([^<]+)<\/loc>/g)].map( @@ -11,6 +12,25 @@ function sitemapUrls(): string[] { ); } +/** + * Map a sitemap pathname to the landing-astro source page that renders it. + * Astro uses `index.astro` for directory routes and `.astro` for leaf + * routes; the homepage is `pages/index.astro`. + */ +function landingSourceForPath(pathname: string): string { + if (pathname === '/') return 'landing-astro/src/pages/index.astro'; + const base = pathname.replace(/^\//, ''); + // Prefer a leaf route (e.g. /faq -> pages/faq.astro); fall back to a + // directory route (e.g. /changelog -> pages/changelog/index.astro). + const leaf = `landing-astro/src/pages/${base}.astro`; + try { + readFileSync(resolve(process.cwd(), leaf)); + return leaf; + } catch { + return `landing-astro/src/pages/${base}/index.astro`; + } +} + describe('public agent surface parity', () => { it('keeps the sitemap limited to canonical HTML surfaces', () => { const catalogUrls = AGENT_SURFACE.catalog.surfaces @@ -80,4 +100,31 @@ describe('public agent surface parity', () => { expect(AGENT_SURFACE.llmsFullTxt).toBe(readPublic('llms-full.txt')); expect(AGENT_SURFACE.catalog.surfaces).toEqual(JSON.parse(readPublic('api-ai.json')).surfaces); }); + + // Regression for #47: every sitemap URL must render a self-canonical + // (canonical == its own URL). Account routes like /login must stay out of + // the sitemap and must not be assigned the homepage canonical. + it('every sitemap URL declares a self-canonical matching its own path', () => { + const urls = sitemapUrls(); + expect(urls).not.toContain('https://read.significanthobbies.com/login'); + + for (const url of urls) { + const pathname = new URL(url).pathname; + const sourcePath = landingSourceForPath(pathname); + const source = readRepo(sourcePath); + + // The Layout canonical defaults to '/', so the homepage needs no + // explicit declaration. Every other sitemap route must declare an + // explicit canonicalPath equal to its own pathname. + if (pathname === '/') { + // Home must not override the default '/' canonical with a different path. + const override = source.match(/canonicalPath\s*=\s*"([^"]+)"/); + expect(override ? override[1] : '/').toBe('/'); + } else { + const match = source.match(/canonicalPath\s*=\s*"([^"]+)"/); + expect(match, `${sourcePath} must declare a self-canonical for ${pathname}`).not.toBeNull(); + expect(match?.[1]).toBe(pathname); + } + } + }); });