The public project SUBJECT gets a route — app/api/public/p/[identifier], the one endpoint the page's own subject has never had
Type: code · Executor: coding_agent · Repo: motir-core · ONE PR.
The page's own subject has no public endpoint. Every LIST a public project page renders has one — app/api/public/p/[identifier]/{tree,items,roadmap,changelog} — and the project itself does not: there is no app/api/public/p/[identifier]/route.ts on origin/main. A second renderer can fetch a project's work items and never learn its name.
This is the single hard gap the 2026-08-29 re-measurement found (MOTIR-3877 carries it in full), and it is one route.
What it returns
The subject of /p/<identifier> as the page renders it today: the display name, the public overview / README markdown, the tags, the owning workspace's public identity, and whatever the hero reads. Read app/(public)/p/[identifier]/page.tsx for the field set rather than inventing one — the contract is "what the page needs", and the page is the specification.
Conventions
- Anonymous, like its four sibling READS. Those take an optional
actorUserId = session?.user.id ?? nullfor viewer-awareness and are explicitly "NOT session-gated on READ"; this route follows them exactly. It is not a new access posture. - A framework boundary calls a service.
publicProjectsService.getOverviewalready exists and is what the page uses; this route is a thin caller, not a second read path (the 4-layer rule). - The project-access gate is the service's, unchanged. A non-public identifier answers exactly as the page does today — do not invent a second notion of "public".
Boundary
- It renders nothing. The consumer is MOTIR-3877, later and elsewhere.
- It does not version anything. Promoting
/api/public/*into a contract a second repository may depend on is the sibling card,blocked_bythis one. - It adds no capability. The data is already public and already served to this repository's own pages; this exposes the same read over HTTP.
Acceptance criteria
GET /api/public/p/<identifier>returns the subject fieldsapp/(public)/p/[identifier]/page.tsxrenders, for a public project, with no session.- A non-public or unknown identifier answers with the same status and shape the existing sibling routes use for that case — asserted, so the two cannot drift.
- The route calls
publicProjectsService; no Prisma client is imported intoapp/api/public/, asserted in the shape the repository already uses for framework-boundary rules. - The response field set is derived from the page's own read — a test fails if the page starts rendering a subject field this route does not return.
- ≥90% coverage on the files this PR touches.
Context refs
motir-core/app/api/public/p/[identifier]/— the four sibling reads whose shape and access posture this followsmotir-core/app/(public)/p/[identifier]/page.tsx— the specification for the field setmotir-core/lib/services/publicProjectsService.ts—getOverview, the service this calls- MOTIR-3877 — the consumer, and the card carrying the measurement that found this gap