diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md
index 0fbd7e6..0f57f72 100644
--- a/PROJECT_STATUS.md
+++ b/PROJECT_STATUS.md
@@ -18,6 +18,8 @@ maintenance-first support.
## Timeline
+- **2026-07-31:** Kept the public sitemap HTML-only and added source-level
+ sitemap/catalog/Markdown parity coverage.
- **2026-07-31:** Added locally verified Open Graph/Twitter image metadata and
SoftwareApplication structured data to the public landing layout; production
deployment remains separate.
@@ -36,6 +38,7 @@ maintenance-first support.
AI-assisted reading.
- Account-backed and device-local capture paths.
- Owned editorial product changelog at `/changelog`.
+- HTML-only public sitemap with cataloged Markdown mirrors for agent discovery.
## Work queue
diff --git a/docs/product/surfaces.md b/docs/product/surfaces.md
index 94cbc6c..c6b4d3c 100644
--- a/docs/product/surfaces.md
+++ b/docs/product/surfaces.md
@@ -32,7 +32,7 @@ Served from `public/` and the `agent-edge.mjs` handler in `src/worker.ts`
| `index.md` | `/index.md` | Product brief in Markdown (no JS) |
| `api/ai` | `/api/ai` | JSON catalog of public surfaces |
| `robots.txt` | `/robots.txt` | Allows all + lists agent surfaces |
-| `sitemap.xml` | `/sitemap.xml` | Public URL inventory |
+| `sitemap.xml` | `/sitemap.xml` | Canonical public HTML page inventory |
| IndexNow key | `/fa7259e2e0d942f1a1267b344a75a143.txt` | Bing/Yandex URL submission key |
The agent-edge payload is generated into `src/agent-edge.mjs` by the fleet
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 89838c7..393f8b2 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -1,7 +1,5 @@
https://read.significanthobbies.com/
- https://read.significanthobbies.com/index.md
- https://read.significanthobbies.com/llms.txt
https://read.significanthobbies.com/login
diff --git a/src/agent-surfaces.test.ts b/src/agent-surfaces.test.ts
new file mode 100644
index 0000000..b55ecfe
--- /dev/null
+++ b/src/agent-surfaces.test.ts
@@ -0,0 +1,58 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+const publicPath = (name: string) => resolve(process.cwd(), 'public', name);
+const readPublicFile = (name: string) => readFileSync(publicPath(name), 'utf8');
+
+const sitemap = readPublicFile('sitemap.xml');
+const catalog = JSON.parse(readPublicFile('api-ai.json')) as {
+ url: string;
+ llms: string;
+ llmsFull: string;
+ sitemap: string;
+ robots: string;
+ surfaces: Array<{ id: string; url: string; md: string }>;
+};
+
+const sitemapUrls = [...sitemap.matchAll(/([^<]+)<\/loc>/g)].map((match) => match[1]);
+
+describe('public agent surface parity', () => {
+ it('keeps the sitemap limited to cataloged HTML pages', () => {
+ expect(sitemapUrls.sort()).toEqual(catalog.surfaces.map((surface) => surface.url).sort());
+
+ for (const url of sitemapUrls) {
+ const pathname = new URL(url).pathname;
+ expect(pathname).not.toMatch(/\.(?:md|txt|json|xml)$/);
+ expect(pathname).not.toBe('/api/ai');
+ }
+ });
+
+ it('keeps one checked-in Markdown mirror for every cataloged HTML page', () => {
+ for (const surface of catalog.surfaces) {
+ const markdownUrl = new URL(surface.md);
+ expect(markdownUrl.origin).toBe(catalog.url);
+ expect(markdownUrl.pathname).toMatch(/\.md$/);
+ expect(existsSync(publicPath(markdownUrl.pathname.slice(1)))).toBe(true);
+ }
+ });
+
+ it('advertises machine resources outside the HTML sitemap', () => {
+ const discoverySources = [
+ readPublicFile('robots.txt'),
+ readPublicFile('llms.txt'),
+ readPublicFile('api-ai.json'),
+ ].join('\n');
+
+ for (const resource of [
+ '/llms.txt',
+ '/llms-full.txt',
+ '/index.md',
+ '/api/ai',
+ '/robots.txt',
+ '/sitemap.xml',
+ ]) {
+ expect(discoverySources).toContain(resource);
+ }
+ });
+});