A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1# 0018 — The article render pipeline lives in one render-article module
2
3- **Status:** Accepted
4- **Date:** 2026-06-10
5- **Scope:** the article render pipeline (`src/lib/reader/render-article.ts`, the
6 article page, `src/lib/feed/publication-feed.ts`)
7
8## Context
9
10Turning a stored block tree into safe HTML takes three calls in one load-bearing
11order — `resolveBlobImageUrls` → `renderBlocks` → `sanitizeArticleHtml` — plus the
12plain-text fallback (`doc.textContent || blocksToText( blocks )`). The ordering is the
13AGENTS.md #6b invariant (sanitise is the LAST step before injection), but no module
14owned it: the article page and the feed builder each assembled it by hand. A third
15read surface (AMP, email, embeds) would have had to re-learn the order, and getting it
16wrong ships unsanitised PDS HTML. Decision 0016 sealed route *resolution* behind
17`read-context.ts` and explicitly deferred this orthogonal deepening.
18
19## Decision
20
21One deep module, `src/lib/reader/render-article.ts`:
22
23```ts
24renderArticle( doc, { pdsUrl, did } ) → { html, text }
25```
26
27- It accepts the document-value slice (`textContent` + `content.blocks`) rather than
28 raw blocks, so the text fallback concentrates here too. `read-context.ts`'s
29 `SkyDocumentValue` and the feed's `FeedDocumentValue` both satisfy the
30 `RenderableDocument` parameter structurally — no adapter layer.
31- The pipeline-order invariant gains locality: it lives once, behaviourally tested in
32 `render-article.test.ts` (a `<script>` in block content never survives; blob-backed
33 image URLs are rebuilt against the author's current PDS before render).
34- Like `render.ts` underneath, the module is dependency-free — it must never import
35 `@wordpress/*` (Decision 0003). It wraps `render.ts`, sitting beside it; render
36 fidelity stays locked by `render.test.ts`, untouched.
37- The article page and `buildPublicationFeedXml` are now callers;
38 `publication-feed.ts` stays a pure transform. New read surfaces get sanitisation for
39 free by construction — AGENTS.md rule 6(b) now points here.
40
41## Consequences
42
43- `resolveBlobImageUrls`, `renderBlocks`, and `sanitizeArticleHtml` keep their own
44 unit suites; no page or transform should compose them by hand again
45 (`_[rkey].meta.test.ts` pins the article page's delegation).
46- A surface needing different rendering rules (e.g. email's stricter tag set) extends
47 this module's interface rather than re-assembling the pipeline at the call site.