The crawl surface's two missing endpoints — the changelog Atom feed, and the public-project index motir.co's sitemap reads
Two things a search engine and a feed reader consume have no home on either host since MOTIR-3951.
1 · The changelog Atom feed is orphaned. lib/publicProjects/atomFeed.ts still exists and tests/publicProjects/atomFeedDocument.test.ts still exercises it, but the route that served it — app/(public)/p/[identifier]/changelog.xml/route.ts — was deleted, and git grep atomFeed -- app lib now returns only the module itself. The builder has no caller. Anyone subscribed to a project's feed is subscribed to a 404.
2 · motir.co's sitemap cannot list public projects. publicProjectsService.listPublicForSitemap() exists and is called only by publicFollowDigestService. motir-core's own app/sitemap.ts is deliberately empty now (MOTIR-3951: "every crawlable public page moved to motir.co, which publishes its own sitemap") — and the sitemap on that host is a static file in a repository with no database. Without a public endpoint, motir.co cannot enumerate the projects it is supposed to be advertising.
Add both to the public contract:
- an Atom document endpoint for a project's changelog, returning
application/atom+xmlbuilt by the existing module — not a JSON re-implementation, and not a copy of the builder intomotir-marketing; - a public-project index endpoint returning the identifiers and
updatedAttimestampslistPublicForSitemapalready produces, in a page shape a crawler-facing sitemap can consume for a growing set (the cardinality question: this is a SET whose size is the customer count, so it pages).
Same posture as every read beside them: capability gate first, anonymous, ProjectNotFoundError → 404 for the feed. Both are entries in lib/api/public/openapi/operations.ts; the Atom one is the surface's first non-JSON response, so declare its media type honestly rather than describing it as JSON.
The URL people already subscribed to is app.motir.co/p/<identifier>/changelog.xml. It is inside proxy.ts's /p/:path* matcher, so it 308s to the same path on motir.co once the cutover configures the public origin — which means the crawl surface on motir.co must answer that exact path. Do not invent a different one, and record the correspondence in this route's comment.
Acceptance criteria
- A public endpoint returns a project's changelog as a valid Atom document with
Content-Type: application/atom+xml, built bylib/publicProjects/atomFeed.ts(the module gains a caller;git grepfor it returns a route). - A public endpoint returns the index of public projects — identifier and last-updated — paged, with a stable cursor, over the same source
listPublicForSitemapreads. - Both call
publicSurfaceUnavailable()before any session read; the feed answers 404 with no existence leak for a non-public or unknown project. - Both are declared in
lib/api/public/openapi/operations.ts, the Atom one with its real media type;contract-coverage,contract-drift,cloud-gate-totalityandanonymous-posturepass with no new exemption. - The feed route's comment names the public URL it is consumed at (
motir.co/p/<identifier>/changelog.xml) and theproxy.tsredirect that sends the old address there. tests/publicProjects/atomFeedDocument.test.tsstill passes unchanged — the builder is reused, not rewritten.- No file outside
motir-coreis touched.
Context refs
motir-core/lib/publicProjects/atomFeed.ts— the orphaned buildermotir-core/tests/publicProjects/atomFeedDocument.test.tsmotir-core/lib/services/publicProjectsService.ts—listPublicForSitemap(:398),getChangelogFeed(:681)motir-core/app/sitemap.ts— empty by decision, and the comment explaining itmotir-core/proxy.ts—PUBLIC_REDIRECT_SEGMENTS, and/p/:path*in the matchermotir-core/docs/decisions/public-surface-hosts.md§6 — each host writes its ownrobots.txtand sitemap, and neither describes the othermotir-core/lib/api/public/openapi/operations.ts