Files
PersonalBlog/docs/rich-content-and-media.md
T
2026-08-27 08:18:13 +00:00

4.3 KiB
Raw Blame History

Rich content & media

Images and video are first-class; embeds/charts/diagrams are a founding requirement (v1.1 §29-A restores what v1.0 omitted).

Media pipeline (§29.1–29.4, v1.1)

  • Storage port ObjectStorage (put/get/stat/delete); initial adapter stores on /var/lib/eppp/media; future adapters target S3/R2/MinIO without changing content records (content stores media IDs, never absolute paths).
  • Upload validation (§29.1): size checked before reading the body; content type from magic bytes (client header/extension advisory only); SHA-256 on ingest (duplicate checksum returns the existing record); generated content-addressed keys, never the original filename.
  • Image processing (§29.2): every accepted image re-encoded before serving (original bytes never served); metadata stripped, orientation baked in then dropped (GPS/camera serial never reach the public volume); derivative widths 480/800/1200/1600/2400 (never upscaled), AVIF + WebP per width + one universal JPEG fallback; rendered images carry width/height (zero CLS); lazy-load below the fold; immutable one-year cache headers.
  • Video posture (§29.3): no transcoding in v1; accept MP4 (H.264 + AAC) only, reject others with a message naming export settings; probe dimensions/duration, generate a poster frame.

Embeds — three tiers (§29-A.1)

Tier Mechanism Survives provider death Public JS
1 Native chart/diagram blocks rendered server-side to SVG yes none
2 Snapshot embed + locally cached static image degrades to image none
3 Live sandboxed iframe from allowlisted host, click-to-load no one island

The editor steers to tier 1 whenever data belongs to the author ("paste data" beside "paste embed URL").

Provider allowlist (§29-A.2)

eppp_embed_providers: exact hostnames only (no wildcards), admin-editable, every change audited. Seed: Observable, Datawrapper, CodePen, youtube-nocookie.com, player.vimeo.com, archive.org, plus a configurable self-hosted Grafana host. Enforced twice: in the renderer and in a CSP frame-src generated from the same table.

Fetch, cache & privacy (§29-A.3)

On save of content containing an embed: resolve provider by exact host (unknown host = author-visible error); fetch oEmbed through the hardened fetch; sanitise to an iframe with allowlisted src + dimensions; download and re-host the thumbnail (never hotlink); refresh on a 30-day cycle. Live iframes are click-to-load; every iframe carries sandbox, referrerpolicy="no-referrer", loading="lazy", a required title. No reader request reaches a third party before explicit interaction.

Hardened outbound fetch (§29-A.4, core)

One shared core service (oEmbed, thumbnail download, future link checker): resolve DNS first and reject private/loopback/link-local/multicast/metadata ranges; re-check after every redirect (max 3), pinning the connection to the pre-resolved address (closes DNS-rebinding); 10 s timeout, 5 MB cap; worker egress additionally restricted at the network layer. No extension ever writes its own outbound fetch (FIT-015).

Native charts & diagrams (§29-A.5)

core.chart (line, bar, area, scatter; CSV/JSON props) and core.diagram (Mermaid), rendered server-side to a single inline <svg>: every chart carries <title>, <desc> and a visually-hidden data table; all colour comes from theme tokens (a literal colour in chart output is a build failure, FIT-016); renderers are pure functions (no headless browser, no client bundle, no network); limits are configuration (2000 points / 12 series default, exceeded at save time).

Ownership: blocks/UX ship as first-party extensions (org.eppp.embeds, org.eppp.charts, or one org.eppp.rich-content); hardened fetch + CSP generation are core (FIT-015–017).

Metadata & syndication (§34-A, v1.1)

Core (every content type needs it): per-page <title>, meta description, canonical URL, Open Graph tags, JSON-LD BlogPosting (headline, datePublished, dateModified, author) on articles; sitemap.xml regenerated on publish; robots.txt excluding /admin and /api; ETag/Last-Modified with conditional requests. RSS remains a v0.2 extension. Open Graph images (when added) are generated server-side (SVG rasterised at publish, cached as a media object), no headless browser.