diff --git a/docs/dependency-upgrade-plan.md b/docs/dependency-upgrade-plan.md
index 7f34eb48..9809eb88 100644
--- a/docs/dependency-upgrade-plan.md
+++ b/docs/dependency-upgrade-plan.md
@@ -655,9 +655,9 @@ detail needed to act on them; kept here as the record of what this phase chose n
- [ ] Burn down the 263 `vue-tsc` errors. Now two distinct groups: ~208 pre-existing (149 in
`composables/useDirectus.ts`) and **55 newly surfaced `noUncheckedIndexedAccess` violations**,
which are the more interesting set — each is a real unchecked index.
-- [ ] Stop the prerender crawler walking image URLs, so a full local build works:
- `nitro: { prerender: { ignore: ['/_ipx'] } }`. Pre-existing on `main`; affects only local
- full builds, since CI skips discovery and Vercel does not prerender.
+- [x] Stop the prerender crawler walking image URLs, so a full local build works — done 2026-08-04
+ (#240): 193s and intermittently failing became 29s. Scoped to non-static builds; see the
+ follow-up backlog for why that guard matters.
- [ ] `tailwind.config.js` `content` globs are `'./pages/**/*.{html,js}'` and
`'./components/**/*.{html,js}'` — **no `.vue`**. The site is styled only because
`@nuxtjs/tailwindcss` injects its own defaults over the top. Worth fixing before Tailwind 4
@@ -1340,11 +1340,26 @@ So the swap needed real text, not tag-stripped HTML:
| regex + `{{ }}` (the naive fix) | ❌ shows `für` | ❌ | ✅ inert |
| `getPlainText` + `{{ }}` (shipped) | ✅ `für` | ✅ `&` | ✅ inert |
-`helpers/getPlainText.ts` sanitises with `ALLOWED_TAGS: []` and `RETURN_DOM_FRAGMENT`, then reads
-`textContent`. That returns genuine text with every entity decoded, which is safe for `{{ }}` and must
-never be handed to `v-html`. It parses instead of pattern-matching, which is the whole point: a regex
-cannot match a tag containing `<` or `>`, so ` src=x onerror=alert(1)>` survived
-`/<[^<>]+>/g` as a working tag.
+`getPlainText` sanitises with `ALLOWED_TAGS: []` and `RETURN_DOM_FRAGMENT`, then reads `textContent`.
+That returns genuine text with every entity decoded, which is safe for `{{ }}` and must never be handed
+to `v-html`. It parses instead of pattern-matching, which is the whole point: a regex cannot match a tag
+containing `<` or `>`, so `
src=x onerror=alert(1)>` survived `/<[^<>]+>/g` as a working tag.
+
+**`helpers/sanitize.ts` is the only module that touches DOMPurify**, after a review comment on #237
+pointed out that the two ProfileCreation components had each grown their own
+`computed(() => DOMPurify.sanitize(...))`, duplicating `InnerHtml.vue`. It cited this repo's own rule
+in `AGENTS.md` — never duplicate logic across modules — and was right. Three exports:
+
+| export | for |
+| --- | --- |
+| `sanitizeHtml` | default policy, anything bound to `v-html` |
+| `sanitizeInlineHtml` | as above but forbids `
`, for the news ticker's single scrolling line |
+| `getPlainText` | real plain text, for `{{ }}` |
+
+`InnerHtml.vue` and `NewsTicker.vue` were routed through it too, rather than fixing only the two new
+duplicates — otherwise two of four policies would still have been inline, which is the appearance of
+the rule rather than the rule. `NewsTicker`'s `{ FORBID_TAGS: ['p'] }` became a named export rather
+than a config parameter, so DOMPurify options do not leak back out to callers.
It is deliberately **not** re-exported from `helpers/index.ts`. That barrel is imported by server
routes, and pulling `isomorphic-dompurify` through it would instantiate jsdom for consumers that only
@@ -1363,9 +1378,11 @@ classes. So these two were inconsistent outliers rather than a deliberate choice
### Verification
-Unit tests cover the bypass payloads, so the regex cannot come back unnoticed: `test/getPlainText.test.ts`,
-5 cases, including ` src=x onerror=alert(1)>`, `