(motir-marketing) One canonical per page — canonical, og:url, JSON-LD @id, the per-host sitemap and robots name the primary address; every other address 301s to it, motir.co/p/* included
Opened by Zhu Yue ·
A project now answers at several addresses; this card makes exactly ONE of them the address search engines and social cards are told about, and turns every other one into a redirect. Without it the same page is indexable at motir.co/p/PROD, acme.<base>/PROD and roadmap.acme.com — three duplicates competing, which is the failure Canny's make primary exists to prevent and the story's third criterion forbids.
What ships — all in motir-marketing, over what the router and the host contract provide
- The redirect, at page level. Every
/p/*page already loads the subject (loadProject); the DTO now carriesaddresses.primary. Compare the request's host (the router'sx-motir-public-hostheader, orSITE_ORIGIN's host when the router stepped aside) with the primary's host: different ⇒permanentRedirect()to the same path on the primary, path and query preserved. This is ONE helper (lib/publicHost.ts→redirectIfNotPrimary(project, request)) called from the shell so every tab and detail page inherits it — includingmotir.co/p/<identifier>once the project's primary lives elsewhere (ADR Q6's stated consequence). - The canonical and its friends name the primary:
generateMetadata'salternates.canonicalandopenGraph.urlon every/p/*page; everyurl/@idinapp/p/[identifier]/_components/JsonLd.tsx; the changelog Atom route's<link rel="alternate">and entry links — all built fromaddresses.primarythrough onepublicUrlFor(project, path)helper, replacing everysiteUrl('/p/…')call underapp/p/**(a grep after this card finds none).metadataBasestaysSITE_ORIGIN— an absolute canonical overrides it. - Per-host crawl surface.
app/sitemap.tsbecomes host-aware: onmotir.coit lists the static entries plus only the projects whoseprimaryHost(the index's new field) ismotir.co; on a workspace host, that workspace's projects whose primary is this host, at their host-relative paths; on a custom domain, that project's tabs at the root.app/robots.tsbecomes dynamic and names THIS host'ssitemap.xmlandhost. A sitemap may only list URLs on its own host, so a project whose canonical moved offmotir.codisappears frommotir.co's sitemap and appears on its own — say so in the file header. - The OpenGraph image route needs no change beyond
og:url; confirm and say so in the PR. - Tests: the redirect helper for every (request host, primary host) pair including the
motir.co→ custom primary case and the primary is this host no-op; canonical /og:url/@idon each host kind (rendered metadata, not source grep); the Atom feed's links; the sitemap on three host kinds over a stubbed index and contract; robots per host; a grep-based guard thatsiteUrl('/p/no longer appears underapp/p/**.
Boundary
No routing (the router's), no new page, no change to what a page RENDERS (the story's ends at the address boundary), nothing in motir-core. The /explore cards keep linking /p/<identifier> on motir.co — the page-level redirect carries the visitor to the primary, so the directory needs no change; state this in the PR and do not touch app/explore/.
Acceptance criteria
- On a host that is not the project's primary —
motir.co, a workspace subdomain, or a non-primary custom domain — every/p/*page and tab answers301to the same path on the primary, query preserved; on the primary host it renders with200. - The rendered
<link rel="canonical">,og:urland every JSON-LD@idname the primary address on all three host kinds, asserted from rendered metadata; the Atom feed'srel="alternate"and entry links do too. motir.co/sitemap.xmlomits a project whose primary is elsewhere and lists one whose primary ismotir.co; a workspace host's sitemap lists only its own projects at host-relative paths; a custom domain's sitemap lists that project's tabs at the root; each host'srobots.txtnames its own sitemap.- A repository grep finds no
siteUrl('/p/underapp/p/**after this card. - No
@prisma/clientor database URL appears in the repository; no file outsidemotir-marketingis touched.
Context refs
motir-marketing/app/p/[identifier]/page.tsx(generateMetadata,alternates.canonical) ·_components/JsonLd.tsx·changelog.xml/route.ts·opengraph-image.tsx·app/sitemap.ts·app/robots.ts·lib/siteOrigin.ts- the host contract —
addresses.primaryon the subject,primaryHoston the index; the router — the request headers this reads - the decision — Q6
- MOTIR-4118 — the crawl surface as shipped for one host
- Canny — make primary marks the canonical source (
help.canny.io, fetched 2026-09-02)
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.