···11+# 0017 — Recognising the app behind a foreign publication
22+33+- **Status:** Accepted
44+- **Date:** 2026-06-10
55+- **Scope:** foreign `site.standard.publication` records on both the authed dashboard
66+ ("From other apps") and the public profile ("Elsewhere") — `src/lib/publish/providers.ts`,
77+ the two `publications.ts` mappers, `ProviderLogo.{tsx,astro}`.
88+99+## Context
1010+1111+`site.standard.publication` is a shared collection: other apps (Leaflet, pckt, Offprint,
1212+…) write their own records into a writer's repo. We list these as "foreign" and want to
1313+show the originating app's logo next to its hostname. The obvious signal — the
1414+publication's hostname — fails for the paid tiers of these services, which serve from a
1515+**custom domain** (`myblog.com`, not `*.leaflet.pub`).
1616+1717+## Decision
1818+1919+`detectProvider( url, value )` recognises a provider in two steps, framework-agnostic
2020+(no JSX) so the React dashboard and the server-rendered Astro reader share one core:
2121+2222+1. **Record discriminator (primary).** App-specific namespaced `$type`s embedded in the
2323+ record survive a custom domain. Observed in real records: pckt writes
2424+ `theme.$type === "blog.pckt.theme"`. This is the only custom-domain-proof signal.
2525+2. **Hostname suffix (fallback).** `*.leaflet.pub`, `*.pckt.blog`, `*.offprint.app`,
2626+ matched at a dot boundary (`host === domain || host.endsWith('.' + domain)`) so
2727+ `evil-leaflet.pub.com` and `notleaflet.pub` do **not** match.
2828+2929+The logo glyphs live as monochrome (`currentColor`) inner-SVG `body` strings in
3030+`KNOWN_PROVIDERS`, rendered inside each framework's own fixed-size `<svg>` wrapper.
3131+3232+## Consequences
3333+3434+- **Leaflet and Offprint on a custom domain are unrecognised** — their records carry no
3535+ app-specific `$type` (verified against a real `*.leaflet.pub` record; Offprint
3636+ unsampled), so they fall back to a bare hostname, exactly as before. Accepted for v1.
3737+ If either later adds a discriminator, extend step 1.
3838+- Recognising a new provider = one `KNOWN_PROVIDERS` entry + (its discriminator and/or)
3939+ one `PROVIDER_DOMAINS` line + a `detectProvider` test case. No renderer changes.
4040+- The glyph data is trusted constant content, so `ProviderLogo` injects it via
4141+ `dangerouslySetInnerHTML` / `set:html` without sanitising — it is never PDS-derived,
4242+ unlike article HTML (which still goes through the reader sanitiser, AGENTS.md rule 6).
4343+- Leaflet's brand asset was a raster PNG, so its glyph is a substitute vector feather
4444+ (Lucide, ISC) rather than the official mark.
···17171818- **Section heading:** "Elsewhere".
1919- **Per-item content:** publication **name** only (plus its logo). No hostname.
2020+ - **Superseded 2026-06-10** (`2026-06-10-provider-logos-design.md`): each row now also
2121+ shows a hostname pill, with the originating service's logo before it when recognised
2222+ (Decision 0017). `ReaderForeignPublication` gained `hostname` + `provider` for this.
2023- Render the section only when there is at least one foreign publication.
21242225## Data layer — `src/lib/reader/publications.ts`
···11+# Provider logos next to foreign-publication hostnames — design
22+33+**Date:** 2026-06-10
44+**Status:** Approved (ready for implementation)
55+66+## Goal
77+88+When a "foreign" publication (a `site.standard.publication` record SkyPress doesn't
99+own — written by Leaflet, pckt, Offprint, …) is shown, display the originating
1010+service's logo next to its hostname **if** we can recognise the service.
1111+1212+Two interfaces, sharing one detection + logo core:
1313+1414+- **Dashboard "From other apps"** (`src/components/Dashboard.tsx`) — logo before the
1515+ hostname in the existing `.dash__pubhost` pill.
1616+- **Public profile "Elsewhere"** (`src/pages/[author]/index.astro`) — currently shows
1717+ the publication name only; now also shows a hostname pill with the logo before it.
1818+ (This reverses the "name only, no hostname" decision in
1919+ `2026-06-09-profile-elsewhere-section-design.md`, which is updated accordingly.)
2020+2121+## Detecting the service
2222+2323+A two-step strategy, because the hostname alone misses services on custom domains:
2424+2525+1. **Record discriminator (primary).** App-specific records carry namespaced `$type`s
2626+ that survive a custom domain. Observed: pckt embeds `theme.$type ===
2727+ "blog.pckt.theme"`. Detect pckt this way regardless of hostname.
2828+2. **Hostname suffix (fallback).** `*.leaflet.pub` → leaflet, `*.pckt.blog` → pckt,
2929+ `*.offprint.app` → offprint. Suffix test is exact-or-dot-boundary
3030+ (`host === domain || host.endsWith('.' + domain)`) to avoid `evil-leaflet.pub.com`.
3131+3232+**Known limitation:** Leaflet records are fully standard (no app-specific `$type` —
3333+verified against a real `*.leaflet.pub` record) and Offprint is unsampled. So Leaflet
3434+and Offprint published on a **custom domain** can't be recognised and fall back to a
3535+bare hostname, exactly as today. Acceptable for v1.
3636+3737+## Shared core — `src/lib/publish/providers.ts` (new)
3838+3939+Framework-agnostic, pure TS (no JSX), the reusable part both interfaces consume:
4040+4141+```ts
4242+export type ProviderId = 'leaflet' | 'pckt' | 'offprint';
4343+export interface KnownProvider { id: ProviderId; label: string; viewBox: string; body: string; }
4444+export const KNOWN_PROVIDERS: Record<ProviderId, KnownProvider>;
4545+export function detectProvider( url: string, value: unknown ): ProviderId | null;
4646+```
4747+4848+- `body` is the inner SVG markup (paths/etc.), self-contained and monochrome
4949+ (`currentColor` — fill-based for pckt/offprint, `fill="none" stroke="currentColor"`
5050+ for the leaflet feather). Rendered inside a fixed-size `<svg viewBox>` wrapper, so the
5151+ source asset's dimensions don't matter, and both fill- and stroke-based glyphs work.
5252+- `label` (e.g. "Leaflet") is the SVG's accessible name (`aria-label`).
5353+5454+### Logo assets
5555+5656+- **pckt, offprint** — extracted from the vector SVGs the user saved in `public/`
5757+ (already single-colour). pckt's hardcoded `fill="black"` becomes `currentColor`.
5858+- **leaflet** — the saved `public/leaflet.svg` is a 147KB raster PNG wrapped in SVG, so
5959+ there is no vector to recolour. Substitute a clean vector **feather** glyph
6060+ (Leaflet's mark is a feather), monochrome, `currentColor`.
6161+- The now-inlined `public/{leaflet,pckt,offprint}.svg` files are removed.
6262+6363+## Data layer
6464+6565+Both mappers call `detectProvider( url, value )` and store the result.
6666+6767+- `ForeignPublication` (`src/lib/publish/publications.ts`) gains
6868+ `provider: ProviderId | null`.
6969+- `ReaderForeignPublication` (`src/lib/reader/publications.ts`) gains
7070+ `provider: ProviderId | null` **and** `hostname: string` (it has neither today; the
7171+ profile page needs the hostname to display it).
7272+7373+## Renderers — share the data, one thin renderer per framework
7474+7575+Astro can't server-render a React component, and the read path must not take a client
7676+island just for a logo. So the *data + detection* is shared; each framework gets a
7777+~5-line SVG renderer reading from `KNOWN_PROVIDERS[ id ]`:
7878+7979+- **`src/components/ProviderLogo.tsx`** (React) — used in `Dashboard.tsx`. Renders
8080+ `null` when `provider` is null. Placed before the hostname text in `.dash__pubhost`.
8181+- **`src/components/ProviderLogo.astro`** — used in `[author]/index.astro` "Elsewhere"
8282+ rows, which gain a hostname pill (logo + hostname) reusing the dashboard's pill look.
8383+8484+## Tests (TDD — failing test first)
8585+8686+- `src/lib/publish/providers.test.ts` (new): `detectProvider` →
8787+ - pckt via `theme.$type === 'blog.pckt.theme'` even on a non-pckt hostname;
8888+ - leaflet/pckt/offprint via hostname suffix (exact + subdomain);
8989+ - rejects look-alike hosts (`evil-leaflet.pub.com`, `notleaflet.pub`);
9090+ - returns null for unknown hosts / unparseable urls;
9191+ - `KNOWN_PROVIDERS` has an entry for every `ProviderId`.
9292+- `src/lib/publish/publications.test.ts`: `listAllPublications` sets `provider` on
9393+ foreign entries (pckt by discriminator, leaflet by host, null when unknown).
9494+- `src/lib/reader/publications.test.ts`: `listAllReaderPublications` sets `provider`
9595+ and `hostname` on foreign entries; owned-only regression still holds.
9696+9797+## Out of scope
9898+9999+No lexicon changes, no curated-block changes, no reader-sanitiser changes. The read
100100+path still never imports `@wordpress/*`. Custom-domain Leaflet/Offprint detection is
101101+explicitly not solved.