From 6ee9a1751abee0763a91e87f723100b9323c20c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20M=C3=BCller?= Date: Tue, 28 Jul 2026 20:44:12 +0200 Subject: [PATCH 1/6] feat(astro-docs): embed inline where possible, else link out (#440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 9.3 — fill the 9.2 ResourceCard 9.3 seam with the AD-12/FR-10 embed-vs-link-out contract. One pure resolver (resolveResourceEmbed) maps a resource to an inline youtube-nocookie player (with an always-present static link-out as the AC3 fail-safe) or a safe external link-out; a new ResourceEmbed.astro renders it, mounted in ResourceCard mirroring EventCard->EventLinks. Extract the isHost host-boundary helper into a shared hostMatch.ts (single source). No self-hosted media, no client JS. Stacks on 9.1 (#438) + 9.2 (#439). --- astro-docs/src/components/events/eventMeta.ts | 11 +- astro-docs/src/components/hostMatch.ts | 11 ++ .../components/resources/ResourceCard.astro | 11 +- .../components/resources/ResourceEmbed.astro | 127 ++++++++++++++ .../src/components/resources/resourceMeta.ts | 130 ++++++++++++++ astro-docs/test/host-match.test.ts | 32 ++++ astro-docs/test/resources-embed.test.ts | 162 ++++++++++++++++++ 7 files changed, 471 insertions(+), 13 deletions(-) create mode 100644 astro-docs/src/components/hostMatch.ts create mode 100644 astro-docs/src/components/resources/ResourceEmbed.astro create mode 100644 astro-docs/test/host-match.test.ts create mode 100644 astro-docs/test/resources-embed.test.ts diff --git a/astro-docs/src/components/events/eventMeta.ts b/astro-docs/src/components/events/eventMeta.ts index f7a9f78a..ddb487a3 100644 --- a/astro-docs/src/components/events/eventMeta.ts +++ b/astro-docs/src/components/events/eventMeta.ts @@ -9,6 +9,7 @@ import { type ParticipationMode, type ParticipationType, } from '../../schema/events'; +import { isHost } from '../hostMatch'; /** Display labels for each participation type (also the filter-pill labels). */ export const PARTICIPATION_TYPE_LABELS = { @@ -109,16 +110,6 @@ export function resolveEventLinks(input: { return []; } -/** - * True when `host` is exactly `domain` or a real subdomain of it (`sub.domain`). - * The leading-dot check is deliberate: a bare `host.endsWith(domain)` would also - * match an attacker-controlled `evildomain.com` (it "ends with" the domain - * without the dot boundary) — the incomplete-URL-sanitization pitfall. - */ -function isHost(host: string, domain: string): boolean { - return host === domain || host.endsWith(`.${domain}`); -} - /** * A human, action-shaped label for a past event's resource link, derived from * the destination host (e.g. "Watch on YouTube") rather than the generic diff --git a/astro-docs/src/components/hostMatch.ts b/astro-docs/src/components/hostMatch.ts new file mode 100644 index 00000000..f9097fdc --- /dev/null +++ b/astro-docs/src/components/hostMatch.ts @@ -0,0 +1,11 @@ +/** + * True when `host` is exactly `domain` or a real subdomain of it (`sub.domain`). + * The leading-dot check is deliberate: a bare `host.endsWith(domain)` would also + * match an attacker-controlled `evildomain.com` (it "ends with" the domain + * without the dot boundary) — the incomplete-URL-sanitization pitfall. Shared by + * `events/eventMeta.ts` and `resources/resourceMeta.ts` so both derive their host + * decisions from ONE secure implementation. + */ +export function isHost(host: string, domain: string): boolean { + return host === domain || host.endsWith(`.${domain}`); +} diff --git a/astro-docs/src/components/resources/ResourceCard.astro b/astro-docs/src/components/resources/ResourceCard.astro index 11e2cd56..51c63c5f 100644 --- a/astro-docs/src/components/resources/ResourceCard.astro +++ b/astro-docs/src/components/resources/ResourceCard.astro @@ -2,6 +2,7 @@ import type { CollectionEntry } from 'astro:content'; import ResourceAttribution from './ResourceAttribution.astro'; +import ResourceEmbed from './ResourceEmbed.astro'; import { RESOURCE_TYPE_LABELS } from './resourceMeta'; interface Props { @@ -16,9 +17,11 @@ const { title, resourceType, attribution } = resource.data; {/* - 9.3 seam: embed-vs-linkout player renders here (when `embeddable`, an - inline player from `url`; otherwise a link-out affordance). Not implemented - in 9.2 — no player/link surfaced yet. + 9.3 media surface (embed-vs-link-out) mounts at the BOTTOM of + `.resource-card-body` as — mirroring how EventCard composes + EventLinks. It is deliberately NOT placed here: this spot is a direct child + of the `[edge][body]` flex row, where a full-width player would become a + third flex column and break the band layout. */}
@@ -39,6 +42,8 @@ const { title, resourceType, attribution } = resource.data; + + {/* 9.4 seam: when `originEvent` is set, a cross-link back to the origin event's card/page renders here. 9.2 does NOT resolve `originEvent`. diff --git a/astro-docs/src/components/resources/ResourceEmbed.astro b/astro-docs/src/components/resources/ResourceEmbed.astro new file mode 100644 index 00000000..b6e5593d --- /dev/null +++ b/astro-docs/src/components/resources/ResourceEmbed.astro @@ -0,0 +1,127 @@ +--- +import type { CollectionEntry } from 'astro:content'; + +import { resolveResourceEmbed } from './resourceMeta'; + +interface Props { + resource: CollectionEntry<'resources'>; +} + +const { resource } = Astro.props; +const { title, resourceType, url, embeddable } = resource.data; +const media = resolveResourceEmbed({ resourceType, url, embeddable, title }); + +// One CTA markup for both branches. For an embed it is the AC3 fail-safe link +// (always in static HTML, so a player blocked by X-Frame-Options/CSP — which +// fails silently, no `onerror` — still leaves a working outward link); for a +// link resource it is the primary affordance. No client JS (AD-1). +const linkOut = + media.kind === 'embed' + ? media.fallback + : { url: media.url, label: media.label }; +--- + +
+ { + media.kind === 'embed' && ( +
+