Skip to content

Cross-platform navigation contract for petrova.{host,codes,blog}

Date: 2026-05-13 Status: open Supersedes: none Superseded-by: none — current

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.

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:

{
"$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.

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.

Runs on every page load. If utm_source is present in the URL search params, the script:

  1. Records the arrival to sessionStorage under key petrova:nav-arrival as JSON { utm_source, utm_medium, utm_campaign, utm_content?, referrer, arrived_at }.
  2. Fires a best-effort navigator.sendBeacon('/api/nav-arrival', json). Failure is silent — the beacon is metrics, not a blocking dependency.
  3. Cleans the URL via history.replaceState, removing all utm_* 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.

  • 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.

For code:

  • codes/src/data/platforms.json, codes/src/components/PlatformNav.astro, codes/src/scripts/nav-arrival.ts are the reference implementation; landed in followup commit alongside this decision doc.
  • codes/src/layouts/Distribution.astro slices <PlatformNav /> into the footer above the existing meta line.
  • host/ adopts the same three artefacts (with self: "host") into its Astro app/dashboard chrome. Tracking issue to follow.
  • blog/, when it ships, adopts the same three artefacts (with self: "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.6 in codes/IMPECCABLE_HANDBOOK_PHASE5.md references this doc as its deliverable verification.

For ops:

  • Each platform must stand up /api/nav-arrival to 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.
  • Vercel function for codes/api/nav-arrival.ts — write the receiving endpoint; pipe to a chosen analytics backend.
  • host/ adoptionhost/ 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 at petrova.blog, flip status: "planned" to "live" in every platform’s platforms.json simultaneously (one PR per platform).