Cross-platform navigation contract for petrova.{host,codes,blog}
Date: 2026-05-13 Status: open Supersedes: none Superseded-by: none — current
Context
Section titled “Context”petrova-codes ships three public-facing surfaces under one brand: petrova.host (Fleet MCP), petrova.codes (artifact registry), and the planned petrova.blog (field notes). Until now there was no navigation between them — operators landing on one surface had no affordance to discover the others, and traffic flow between platforms was unmeasurable. A user request during the Phase 0–4 impeccable followup asked for unified nav with UTM tagging plus arrival-side metrics.
The decision below describes the contract the codes/ surface implements as the reference; host/ and (when it ships) blog/ adopt the same contract verbatim.
Decision
Section titled “Decision”Every petrova platform ships an identical PlatformNav component reading from a shared src/data/platforms.json registry. Outbound cross-platform links carry UTM params on the query string. Each platform also runs the nav-arrival.ts script that records, beacons, and cleans incoming UTM-tagged URLs.
The contract has four artefacts:
1. src/data/platforms.json
Section titled “1. src/data/platforms.json”{ "$schema": "https://petrova.codes/schemas/platforms.json", "version": "1", "self": "<host|codes|blog>", "platforms": { "host": { "url": "https://petrova.host", "label": "petrova.host", "tagline": "Fleet MCP", "status": "live" }, "codes": { "url": "https://petrova.codes", "label": "petrova.codes", "tagline": "Artifact registry", "status": "live" }, "blog": { "url": "https://petrova.blog", "label": "petrova.blog", "tagline": "Field notes", "status": "planned" } }, "utm": { "medium": "cross_nav", "campaign": "petrova_platforms" }, "beacon": { "endpoint": "/api/nav-arrival", "method": "POST", "fields": ["utm_source","utm_medium","utm_campaign","referrer"] }}self is the only field that varies per platform. utm.medium and utm.campaign are fixed across the brand. status: "planned" renders the entry inert (no href, aria-disabled="true", dimmed); flip to "live" when the platform ships.
2. src/components/PlatformNav.astro
Section titled “2. src/components/PlatformNav.astro”Renders a <nav aria-label="Petrova platforms"> with one <li> per platform. The self entry renders as a non-link with aria-current="page" and a relative / href; planned entries render with no href and aria-disabled="true"; live non-self entries render as full external links with the UTM query string appended:
?utm_source=<self>&utm_medium=cross_nav&utm_campaign=petrova_platforms&utm_content=<target>Each entry shows a mono label (the typed value — domain) and a sans tagline (the human-read description). The component is sliced into the layout footer.
3. src/scripts/nav-arrival.ts
Section titled “3. src/scripts/nav-arrival.ts”Runs on every page load. If utm_source is present in the URL search params, the script:
- Records the arrival to
sessionStorageunder keypetrova:nav-arrivalas JSON{ utm_source, utm_medium, utm_campaign, utm_content?, referrer, arrived_at }. - Fires a best-effort
navigator.sendBeacon('/api/nav-arrival', json). Failure is silent — the beacon is metrics, not a blocking dependency. - Cleans the URL via
history.replaceState, removing allutm_*keys so the canonical URL stays visible to the operator.
If utm_source is absent, the script returns immediately. If sendBeacon is unsupported, the storage record still happens.
4. /api/nav-arrival endpoint (per platform)
Section titled “4. /api/nav-arrival endpoint (per platform)”Each platform that wants metrics implements this endpoint. The MVP shape is a Vercel function returning 204 No Content after enqueueing the JSON payload to wherever that platform sends analytics (Plausible, a Postgres table, a logflare drain — implementation-defined). The endpoint MUST accept POST with Content-Type: application/json and a body matching the arrival schema. Returning anything other than 2xx triggers no client-side retry — the script is single-shot best-effort.
Alternatives considered
Section titled “Alternatives considered”- Inline UTM on hand-coded
<a href>s without a registry — rejected because three platforms × three links each with UTM strings duplicated as raw text invites drift; the first time someone changes the campaign string, only one of nine links gets the update. - Server-side redirect via
petrova.host/go/<target>— rejected because adding a hop trades URL cleanliness and offline-debuggability (you can’t curl the destination URL directly any more) for a small consistency win; the JSON-registry approach gives the same consistency without the redirect hop. - Cookie-based cross-platform attribution — rejected because petrova has no shared parent domain (each platform is a separate apex). Cookie sharing requires either a third-party cookie (deprecated) or a server-side stitching layer; sessionStorage + beacon is simpler and good enough for the operator-facing analytics this brand needs.
- No URL cleanup (leave UTM params visible) — rejected because operator-facing surfaces should not show campaign tracking in the browser bar; UTM is a metric, not part of the canonical URL.
Consequences
Section titled “Consequences”For code:
codes/src/data/platforms.json,codes/src/components/PlatformNav.astro,codes/src/scripts/nav-arrival.tsare the reference implementation; landed in followup commit alongside this decision doc.codes/src/layouts/Distribution.astroslices<PlatformNav />into the footer above the existing meta line.host/adopts the same three artefacts (withself: "host") into its Astro app/dashboard chrome. Tracking issue to follow.blog/, when it ships, adopts the same three artefacts (withself: "blog").
For docs:
- This decision doc is the source of truth for the contract; changes are append-only via a new dated decision doc that supersedes this one (per
MR-7). - The handbook prompt
5.6incodes/IMPECCABLE_HANDBOOK_PHASE5.mdreferences this doc as its deliverable verification.
For ops:
- Each platform must stand up
/api/nav-arrivalto capture metrics. Until that endpoint exists, the client-side script silently no-ops on the network call (storage + URL cleanup still happen). This is intentional: the contract is forward-compatible with platforms that haven’t wired up the endpoint yet.
Open follow-ups
Section titled “Open follow-ups”- Vercel function for
codes/api/nav-arrival.ts— write the receiving endpoint; pipe to a chosen analytics backend. host/adoption —host/is currently an MCP server, not an HTML surface; clarify whether the cross-platform nav lives on the Fleet dashboard (Vercel) or the MCP server’s HTTP welcome page.blog/placeholder — once a domain or stub exists atpetrova.blog, flipstatus: "planned"to"live"in every platform’splatforms.jsonsimultaneously (one PR per platform).