Block renderer + note/project/tag/repo browsing #9

Merged
forgejo_admin merged 3 commits from 8-block-renderer-jinja2-sunset into main 2026-03-14 12:54:34 +00:00

Summary

Implements the block-based note renderer and adds browsing routes for notes, projects, tags, and repos. All note content renders exclusively from the blocks API (GET /notes/{slug}/blocks), not html_content. Supports all 6 block types (heading, paragraph, table, list, code, mermaid) with anchor links, autolinked slug references, table of contents sidebar, child note composition, and breadcrumb navigation.

Changes

  • src/lib/api.ts: Added Note, Block, TocEntry, NoteLink, Project, Tag, Repo types and API functions (getNote, getNoteBlocks, getNoteToc, listNotes, listNoteSlugs, listProjects, listTags, listRepos)
  • src/lib/components/blocks/: 7 new components -- HeadingBlock, ParagraphBlock, TableBlock, ListBlock, CodeBlock, MermaidBlock, BlockRenderer (dispatcher with autolink logic)
  • src/lib/components/NoteLayout.svelte: Full note layout with breadcrumbs, metadata badges, block content, TOC sidebar, and child notes sidebar
  • src/routes/notes/[slug]/: Note detail page, loads blocks/toc/children/parent/known slugs server-side
  • src/routes/notes/: Note listing with search, type-grouped display, and filter support (tags, project, note_type)
  • src/routes/projects/: Project list and project detail (notes grouped by type)
  • src/routes/tags/: Tag cloud and tag detail (notes filtered by tag)
  • src/routes/repos/: Repo listing grouped by project
  • src/routes/+page.svelte and +page.server.ts: Landing page updated with project/board/tag overview dashboard
  • src/routes/+layout.svelte: Nav updated with Notes, Projects, Tags, Repos links
  • eslint.config.js: Disabled no-at-html-tags and no-dom-manipulating for block components (they render trusted API HTML by design)
  • package.json: Added mermaid dependency for diagram rendering

Test Plan

  • npm run check -- 0 errors, 0 warnings
  • npm run lint -- 0 errors
  • npm run build -- clean production build
  • Verify note detail pages render all 6 block types correctly
  • Verify autolinked slugs navigate to /notes/{slug}
  • Verify breadcrumb navigation and TOC anchor links work
  • Verify project/tag/repo listing pages load and display data
  • Verify landing page shows project, board, and tag overview
  • Verify empty notes show graceful "no content" message
  • Verify mobile responsiveness on all pages

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • Closes #8
  • Plan: plan-pal-e-docs Phase 4 (Block Renderer + Jinja2 Sunset)
## Summary Implements the block-based note renderer and adds browsing routes for notes, projects, tags, and repos. All note content renders exclusively from the blocks API (GET /notes/{slug}/blocks), not html_content. Supports all 6 block types (heading, paragraph, table, list, code, mermaid) with anchor links, autolinked slug references, table of contents sidebar, child note composition, and breadcrumb navigation. ## Changes - **src/lib/api.ts**: Added Note, Block, TocEntry, NoteLink, Project, Tag, Repo types and API functions (getNote, getNoteBlocks, getNoteToc, listNotes, listNoteSlugs, listProjects, listTags, listRepos) - **src/lib/components/blocks/**: 7 new components -- HeadingBlock, ParagraphBlock, TableBlock, ListBlock, CodeBlock, MermaidBlock, BlockRenderer (dispatcher with autolink logic) - **src/lib/components/NoteLayout.svelte**: Full note layout with breadcrumbs, metadata badges, block content, TOC sidebar, and child notes sidebar - **src/routes/notes/[slug]/**: Note detail page, loads blocks/toc/children/parent/known slugs server-side - **src/routes/notes/**: Note listing with search, type-grouped display, and filter support (tags, project, note_type) - **src/routes/projects/**: Project list and project detail (notes grouped by type) - **src/routes/tags/**: Tag cloud and tag detail (notes filtered by tag) - **src/routes/repos/**: Repo listing grouped by project - **src/routes/+page.svelte** and **+page.server.ts**: Landing page updated with project/board/tag overview dashboard - **src/routes/+layout.svelte**: Nav updated with Notes, Projects, Tags, Repos links - **eslint.config.js**: Disabled no-at-html-tags and no-dom-manipulating for block components (they render trusted API HTML by design) - **package.json**: Added mermaid dependency for diagram rendering ## Test Plan - [x] `npm run check` -- 0 errors, 0 warnings - [x] `npm run lint` -- 0 errors - [x] `npm run build` -- clean production build - [ ] Verify note detail pages render all 6 block types correctly - [ ] Verify autolinked slugs navigate to /notes/{slug} - [ ] Verify breadcrumb navigation and TOC anchor links work - [ ] Verify project/tag/repo listing pages load and display data - [ ] Verify landing page shows project, board, and tag overview - [ ] Verify empty notes show graceful "no content" message - [ ] Verify mobile responsiveness on all pages ## Review Checklist - [x] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related - Closes #8 - Plan: `plan-pal-e-docs` Phase 4 (Block Renderer + Jinja2 Sunset)
Add block renderer and note/project/tag/repo browsing routes
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
7b348cd240
Renders notes from the blocks API (all 6 block types: heading, paragraph,
table, list, code, mermaid) with anchor links, autolinked slugs, TOC sidebar,
child note composition, and breadcrumb navigation. Adds listing pages for
notes, projects, tags, and repos. Updates landing page with overview dashboard
and navigation with links to all sections.

Closes #8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract TYPE_COLORS to shared $lib/colors module
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
a0a5e77b71
Eliminates duplication of the 16-type color map across 4 files by
centralizing it in src/lib/colors.ts with a reusable typeColor() helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

PR #9 Review

Reviewed branch 8-block-renderer-jinja2-sunset against main. Read all source files in full: 6 block components, BlockRenderer dispatcher, NoteLayout, 8 new route pages (svelte + server loaders), api.ts extensions, colors.ts, layout changes, eslint config, package.json, Dockerfile.


BLOCKERS

1. No HTML sanitization on {@html} -- XSS risk

All 6 block components render API-supplied HTML via {@html} without any sanitization: ParagraphBlock.svelte:10, HeadingBlock.svelte:15,26,37,48,59, TableBlock.svelte:17,28, ListBlock.svelte:21,32. Additionally, MermaidBlock.svelte:32 sets containerEl.innerHTML = svg from mermaid render output.

The data comes from the pal-e-docs API which is trusted (internal, no user-facing write path today). However, this is a frontend that renders arbitrary HTML from a backend. If the API ever serves compromised or user-supplied content, every {@html} call is an XSS vector.

Recommendation: Add DOMPurify (or equivalent) as a dependency and sanitize all HTML before {@html} injection. At minimum, document the trust model explicitly (e.g., a comment in BlockRenderer.svelte stating the API is the trusted source and sanitization is intentionally omitted). This is a blocker because the code silently accepts any HTML from the API with zero defense-in-depth.

2. listNoteSlugs() fetches ALL notes on every note page load

/home/ldraney/pal-e-app/src/routes/notes/[slug]/+page.server.ts line 11 calls listNoteSlugs(), which in turn calls listNotes() (fetching all 262+ notes) just to extract slugs for the autolink feature. This happens on every single note detail page load.

At 262 notes this is tolerable. At 1000+ notes this becomes a real latency problem. The function also fetches full note objects (title, tags, status, etc.) when only slugs are needed.

Recommendation: Either add a lightweight /notes/slugs API endpoint to pal-e-docs, or cache the slug set server-side with a short TTL. At minimum, file a follow-up issue documenting this as known technical debt. Blocking because this is a performance regression that scales linearly with note count on the most-visited route.


NITS

N1. Duplicate TYPE_COLORS in board [slug]/+page.svelte

The new src/lib/colors.ts defines a canonical TYPE_COLORS map with 16 note types. The existing src/routes/boards/[slug]/+page.svelte (lines 13-20) still has its own local copy with only 6 types. This is not a regression (board code is untouched), but it is technical debt that will cause color inconsistencies between boards and notes for types like sop, convention, template, etc.

Follow-up: refactor board page to import from $lib/colors.

N2. childNotes fetch could be parallelized

In notes/[slug]/+page.server.ts, the listNotes({ parent_slug: params.slug }) call on line 15 happens sequentially after the Promise.all on line 7, but it does not depend on any of those results. It could be included in the initial Promise.all to save one round-trip.

N3. Error handling maps all errors to 404 in note route

notes/[slug]/+page.server.ts line 41 catches all errors and throws error(404, message). If the API returns a 500, the user sees a 404. Consider differentiating: use 404 for "note not found" and 502 for upstream API failures, matching the pattern used in other server loaders (boards, projects, etc.).

N4. Heading level fallback renders <h5> for levels 5+

HeadingBlock.svelte handles levels 1-4 explicitly, then falls back to <h5> for everything else (levels 5, 6, and any invalid values). This is fine functionally but worth noting. No action needed unless the API ever sends level 0 or negative values.

N5. autolink regex could match false positives

The autolink regex /<code>([^<]+)<\/code>/g in BlockRenderer.svelte line 24 matches any <code> content and checks against knownSlugs. If a code block contains text that coincidentally matches a slug (e.g., someone writes doc in inline code and there is a note with slug doc), it will be autolinked. This is by-design per the issue spec but worth calling out.

N6. Board [slug]/+page.svelte still uses no-underline class on links

The new components consistently use no-underline for card-style links, which matches the existing board pattern. Good consistency.

N7. ESLint config scoping is well done

The svelte/no-at-html-tags and svelte/no-dom-manipulating rules are disabled only for src/lib/components/blocks/**/*.svelte, not globally. This is the right approach.


SOP COMPLIANCE

  • Branch named after issue -- 8-block-renderer-jinja2-sunset references issue #8
  • PR body follows template -- Has ## Summary, ## Changes, ## Test Plan, ## Related sections
  • Related references plan slug -- "Plan: plan-pal-e-docs Phase 4"
  • Closes line present -- "Closes #8"
  • No secrets committed -- No .env files, credentials, or tokens in diff. .env.example is appropriate
  • No scope creep -- All changes serve the block renderer + browsing pages. Board routes untouched. API proxy untouched
  • Commit messages -- PR title is descriptive
  • No regressions to boards -- Board routes (/boards, /boards/[slug]), server loaders, and API proxy (/api/boards/[slug]/items/[id]) are unchanged
  • SvelteKit 5 patterns -- Uses $props(), $derived(), $state(), $effect() correctly throughout
  • Dark theme consistency -- Uses same color palette (#0a0a14, #0e0e18, #1a1a2e, #e94560, #141420) as existing board UI
  • Mobile responsive -- Grid layouts with sm:grid-cols-2 lg:grid-cols-3, overflow-x-auto on tables, flex-wrap on tag clouds

ACCEPTANCE CRITERIA (Issue #8)

  • Renders all 6 block types (heading, paragraph, table, list, code, mermaid)
  • Autolinks <code>slug-ref</code> to note pages via knownSlugs set
  • Responsive table rendering with horizontal scroll (overflow-x-auto)
  • Mermaid diagrams rendered client-side with dark theme (dynamic import)
  • Child note composition displayed in sidebar
  • Navigation breadcrumbs (parent > current)
  • Project/tag filtering on notes list page
  • Consistent dark theme matching existing board UI
  • Source from blocks API only (never html_content -- though html_content is still in the Note type, it is never rendered)
  • Board functionality preserved (no regressions)
  • Landing page with projects, boards, and tags overview

VERDICT: NOT APPROVED

Two blockers require resolution before merge:

  1. XSS risk: Add HTML sanitization (DOMPurify or equivalent) on all {@html} injection points, or at minimum add explicit trust-model documentation as code comments and file a follow-up issue for sanitization.
  2. Performance: The listNoteSlugs() pattern fetches all notes on every note page load. File a follow-up issue and add a code comment documenting this as known debt, or implement a lightweight endpoint/caching strategy.

If blockers 1 and 2 are addressed (even with documented-debt + follow-up issues rather than full fixes), this PR is ready to merge. The code quality, SvelteKit patterns, and dark theme consistency are all solid.

## PR #9 Review Reviewed branch `8-block-renderer-jinja2-sunset` against `main`. Read all source files in full: 6 block components, BlockRenderer dispatcher, NoteLayout, 8 new route pages (svelte + server loaders), api.ts extensions, colors.ts, layout changes, eslint config, package.json, Dockerfile. --- ### BLOCKERS **1. No HTML sanitization on `{@html}` -- XSS risk** All 6 block components render API-supplied HTML via `{@html}` without any sanitization: `ParagraphBlock.svelte:10`, `HeadingBlock.svelte:15,26,37,48,59`, `TableBlock.svelte:17,28`, `ListBlock.svelte:21,32`. Additionally, `MermaidBlock.svelte:32` sets `containerEl.innerHTML = svg` from mermaid render output. The data comes from the pal-e-docs API which is trusted (internal, no user-facing write path today). However, this is a frontend that renders arbitrary HTML from a backend. If the API ever serves compromised or user-supplied content, every `{@html}` call is an XSS vector. **Recommendation:** Add `DOMPurify` (or equivalent) as a dependency and sanitize all HTML before `{@html}` injection. At minimum, document the trust model explicitly (e.g., a comment in `BlockRenderer.svelte` stating the API is the trusted source and sanitization is intentionally omitted). This is a blocker because the code silently accepts any HTML from the API with zero defense-in-depth. **2. `listNoteSlugs()` fetches ALL notes on every note page load** `/home/ldraney/pal-e-app/src/routes/notes/[slug]/+page.server.ts` line 11 calls `listNoteSlugs()`, which in turn calls `listNotes()` (fetching all 262+ notes) just to extract slugs for the autolink feature. This happens on every single note detail page load. At 262 notes this is tolerable. At 1000+ notes this becomes a real latency problem. The function also fetches full note objects (title, tags, status, etc.) when only slugs are needed. **Recommendation:** Either add a lightweight `/notes/slugs` API endpoint to pal-e-docs, or cache the slug set server-side with a short TTL. At minimum, file a follow-up issue documenting this as known technical debt. Blocking because this is a performance regression that scales linearly with note count on the most-visited route. --- ### NITS **N1. Duplicate `TYPE_COLORS` in board `[slug]/+page.svelte`** The new `src/lib/colors.ts` defines a canonical `TYPE_COLORS` map with 16 note types. The existing `src/routes/boards/[slug]/+page.svelte` (lines 13-20) still has its own local copy with only 6 types. This is not a regression (board code is untouched), but it is technical debt that will cause color inconsistencies between boards and notes for types like `sop`, `convention`, `template`, etc. Follow-up: refactor board page to import from `$lib/colors`. **N2. `childNotes` fetch could be parallelized** In `notes/[slug]/+page.server.ts`, the `listNotes({ parent_slug: params.slug })` call on line 15 happens sequentially after the `Promise.all` on line 7, but it does not depend on any of those results. It could be included in the initial `Promise.all` to save one round-trip. **N3. Error handling maps all errors to 404 in note route** `notes/[slug]/+page.server.ts` line 41 catches all errors and throws `error(404, message)`. If the API returns a 500, the user sees a 404. Consider differentiating: use 404 for "note not found" and 502 for upstream API failures, matching the pattern used in other server loaders (boards, projects, etc.). **N4. Heading level fallback renders `<h5>` for levels 5+** `HeadingBlock.svelte` handles levels 1-4 explicitly, then falls back to `<h5>` for everything else (levels 5, 6, and any invalid values). This is fine functionally but worth noting. No action needed unless the API ever sends level 0 or negative values. **N5. `autolink` regex could match false positives** The autolink regex `/<code>([^<]+)<\/code>/g` in `BlockRenderer.svelte` line 24 matches any `<code>` content and checks against `knownSlugs`. If a code block contains text that coincidentally matches a slug (e.g., someone writes `doc` in inline code and there is a note with slug `doc`), it will be autolinked. This is by-design per the issue spec but worth calling out. **N6. Board `[slug]/+page.svelte` still uses `no-underline` class on links** The new components consistently use `no-underline` for card-style links, which matches the existing board pattern. Good consistency. **N7. ESLint config scoping is well done** The `svelte/no-at-html-tags` and `svelte/no-dom-manipulating` rules are disabled only for `src/lib/components/blocks/**/*.svelte`, not globally. This is the right approach. --- ### SOP COMPLIANCE - [x] **Branch named after issue** -- `8-block-renderer-jinja2-sunset` references issue #8 - [x] **PR body follows template** -- Has ## Summary, ## Changes, ## Test Plan, ## Related sections - [x] **Related references plan slug** -- "Plan: `plan-pal-e-docs` Phase 4" - [x] **Closes line present** -- "Closes #8" - [x] **No secrets committed** -- No `.env` files, credentials, or tokens in diff. `.env.example` is appropriate - [x] **No scope creep** -- All changes serve the block renderer + browsing pages. Board routes untouched. API proxy untouched - [x] **Commit messages** -- PR title is descriptive - [x] **No regressions to boards** -- Board routes (`/boards`, `/boards/[slug]`), server loaders, and API proxy (`/api/boards/[slug]/items/[id]`) are unchanged - [x] **SvelteKit 5 patterns** -- Uses `$props()`, `$derived()`, `$state()`, `$effect()` correctly throughout - [x] **Dark theme consistency** -- Uses same color palette (`#0a0a14`, `#0e0e18`, `#1a1a2e`, `#e94560`, `#141420`) as existing board UI - [x] **Mobile responsive** -- Grid layouts with `sm:grid-cols-2 lg:grid-cols-3`, `overflow-x-auto` on tables, `flex-wrap` on tag clouds --- ### ACCEPTANCE CRITERIA (Issue #8) - [x] Renders all 6 block types (heading, paragraph, table, list, code, mermaid) - [x] Autolinks `<code>slug-ref</code>` to note pages via `knownSlugs` set - [x] Responsive table rendering with horizontal scroll (`overflow-x-auto`) - [x] Mermaid diagrams rendered client-side with dark theme (dynamic import) - [x] Child note composition displayed in sidebar - [x] Navigation breadcrumbs (parent > current) - [x] Project/tag filtering on notes list page - [x] Consistent dark theme matching existing board UI - [x] Source from blocks API only (never `html_content` -- though `html_content` is still in the Note type, it is never rendered) - [x] Board functionality preserved (no regressions) - [x] Landing page with projects, boards, and tags overview --- ### VERDICT: NOT APPROVED Two blockers require resolution before merge: 1. **XSS risk**: Add HTML sanitization (DOMPurify or equivalent) on all `{@html}` injection points, or at minimum add explicit trust-model documentation as code comments and file a follow-up issue for sanitization. 2. **Performance**: The `listNoteSlugs()` pattern fetches all notes on every note page load. File a follow-up issue and add a code comment documenting this as known debt, or implement a lightweight endpoint/caching strategy. If blockers 1 and 2 are addressed (even with documented-debt + follow-up issues rather than full fixes), this PR is ready to merge. The code quality, SvelteKit patterns, and dark theme consistency are all solid.
Fix XSS via DOMPurify and cache slugs for autolink performance
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
d15b0a1e88
Blocker 1 (XSS): Add isomorphic-dompurify and a shared sanitize()
helper in src/lib/sanitize.ts. All {@html} inputs in block components
are now sanitized through BlockRenderer before reaching the template:
paragraph and heading HTML via autolink(), table headers/rows via
per-cell sanitize(), and list items via recursive sanitizeListItems().

Blocker 2 (Performance): Replace listNoteSlugs() with a server-side
slug cache (src/lib/slugCache.ts) that has a 60-second TTL and
deduplicates concurrent in-flight requests. The note detail page
loader now calls getCachedSlugs() instead of fetching all notes on
every page load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

PR #9 Re-Review (Post-Fix)

Re-reviewed after dev pushed fixes for the two blockers flagged in the initial review. Read all modified and new files in full.


BLOCKER #1 RESOLUTION: XSS Sanitization -- FIXED

New file: src/lib/sanitize.ts -- wraps isomorphic-dompurify with a sanitize() function. Config allows target/rel attributes and code/pre tags. Clean and minimal.

Coverage verified -- all {@html} injection points are protected:

Component {@html} location Sanitization path
HeadingBlock lines 15, 26, 37, 48, 59 html prop comes from autolink() which calls sanitize() at line 34 of BlockRenderer
ParagraphBlock line 10 html prop comes from autolink() -- same path
TableBlock lines 17, 28 headers and rows individually mapped through sanitize() at lines 67-68 of BlockRenderer
ListBlock lines 21, 32 items passed through sanitizeListItems() which recursively sanitizes item.html at line 47 of BlockRenderer

Non-{@html} DOM writes also verified:

  • MermaidBlock line 32: containerEl.innerHTML = svg -- output from mermaid.render() which generates SVG from a text DSL, not user HTML. Standard mermaid integration pattern. Acceptable.
  • CodeBlock line 16: uses {content} (auto-escaped by Svelte). Safe.

Dependency chain correct: isomorphic-dompurify (^3.3.0) + dompurify (^3.3.3) in dependencies, @types/dompurify (^3.0.5) in devDependencies. package-lock.json updated. node_modules installed. isomorphic-dompurify handles SSR+hydration dual-environment correctly.


BLOCKER #2 RESOLUTION: Performance (slug cache) -- FIXED

New file: src/lib/slugCache.ts -- module-level cache with 60-second TTL and concurrent request deduplication.

Verified correct behavior:

  • Cache hit path (line 19): returns cachedSlugs if within TTL. No API call.
  • Concurrent deduplication (line 25): if a fetch is already in-flight, subsequent callers await the same promise instead of firing a second request.
  • Error resilience (line 39): on API failure, returns stale cache if available; only throws if no cache exists.
  • TTL reset (line 32): expiry set from completion time, not request time. Correct.
  • Server-only (line 2): imports listNotes from $lib/api which uses $env/dynamic/private. Only imported in +page.server.ts. No client-side leakage.

Caller updated: notes/[slug]/+page.server.ts line 12 now calls getCachedSlugs() instead of listNoteSlugs(). Import of listNoteSlugs removed.


NEW NITS (non-blocking)

N1. Dead code: listNoteSlugs() in api.ts line 226-229

The function is still exported but has zero imports anywhere in the codebase. It should be removed to keep the module clean.

N2-N5 from initial review still apply (duplicate TYPE_COLORS in board page, childNotes parallelization, all-errors-as-404, heading fallback). These are pre-existing nits, not introduced by this PR's fixes.


SOP COMPLIANCE

  • Branch named after issue (8-block-renderer-jinja2-sunset -> issue #8)
  • PR body follows template (Summary, Changes, Test Plan, Related)
  • Related references plan slug (plan-pal-e-docs Phase 4)
  • Closes #8 present
  • No secrets committed
  • No scope creep -- fixes are surgical (2 new files, 2 modified files)
  • Dependencies properly declared in both package.json and package-lock.json

VERDICT: APPROVED

Both blockers are resolved cleanly. The sanitization coverage is complete across all {@html} paths with recursive handling for nested list items. The slug cache is well-implemented with TTL, deduplication, and stale-cache fallback. No new issues introduced by the fixes.

Nits (dead listNoteSlugs export, duplicate TYPE_COLORS, child notes parallelization) should be tracked as follow-up work after merge.

## PR #9 Re-Review (Post-Fix) Re-reviewed after dev pushed fixes for the two blockers flagged in the initial review. Read all modified and new files in full. --- ### BLOCKER #1 RESOLUTION: XSS Sanitization -- FIXED **New file:** `src/lib/sanitize.ts` -- wraps `isomorphic-dompurify` with a `sanitize()` function. Config allows `target`/`rel` attributes and `code`/`pre` tags. Clean and minimal. **Coverage verified -- all `{@html}` injection points are protected:** | Component | `{@html}` location | Sanitization path | |-----------|-------------------|-------------------| | HeadingBlock | lines 15, 26, 37, 48, 59 | `html` prop comes from `autolink()` which calls `sanitize()` at line 34 of BlockRenderer | | ParagraphBlock | line 10 | `html` prop comes from `autolink()` -- same path | | TableBlock | lines 17, 28 | `headers` and `rows` individually mapped through `sanitize()` at lines 67-68 of BlockRenderer | | ListBlock | lines 21, 32 | `items` passed through `sanitizeListItems()` which recursively sanitizes `item.html` at line 47 of BlockRenderer | **Non-`{@html}` DOM writes also verified:** - `MermaidBlock` line 32: `containerEl.innerHTML = svg` -- output from `mermaid.render()` which generates SVG from a text DSL, not user HTML. Standard mermaid integration pattern. Acceptable. - `CodeBlock` line 16: uses `{content}` (auto-escaped by Svelte). Safe. **Dependency chain correct:** `isomorphic-dompurify` (^3.3.0) + `dompurify` (^3.3.3) in `dependencies`, `@types/dompurify` (^3.0.5) in `devDependencies`. `package-lock.json` updated. `node_modules` installed. `isomorphic-dompurify` handles SSR+hydration dual-environment correctly. --- ### BLOCKER #2 RESOLUTION: Performance (slug cache) -- FIXED **New file:** `src/lib/slugCache.ts` -- module-level cache with 60-second TTL and concurrent request deduplication. **Verified correct behavior:** - Cache hit path (line 19): returns `cachedSlugs` if within TTL. No API call. - Concurrent deduplication (line 25): if a fetch is already in-flight, subsequent callers await the same promise instead of firing a second request. - Error resilience (line 39): on API failure, returns stale cache if available; only throws if no cache exists. - TTL reset (line 32): expiry set from completion time, not request time. Correct. - Server-only (line 2): imports `listNotes` from `$lib/api` which uses `$env/dynamic/private`. Only imported in `+page.server.ts`. No client-side leakage. **Caller updated:** `notes/[slug]/+page.server.ts` line 12 now calls `getCachedSlugs()` instead of `listNoteSlugs()`. Import of `listNoteSlugs` removed. --- ### NEW NITS (non-blocking) **N1. Dead code: `listNoteSlugs()` in `api.ts` line 226-229** The function is still exported but has zero imports anywhere in the codebase. It should be removed to keep the module clean. **N2-N5 from initial review still apply** (duplicate `TYPE_COLORS` in board page, `childNotes` parallelization, all-errors-as-404, heading fallback). These are pre-existing nits, not introduced by this PR's fixes. --- ### SOP COMPLIANCE - [x] Branch named after issue (`8-block-renderer-jinja2-sunset` -> issue #8) - [x] PR body follows template (Summary, Changes, Test Plan, Related) - [x] Related references plan slug (`plan-pal-e-docs` Phase 4) - [x] `Closes #8` present - [x] No secrets committed - [x] No scope creep -- fixes are surgical (2 new files, 2 modified files) - [x] Dependencies properly declared in both `package.json` and `package-lock.json` --- ### VERDICT: APPROVED Both blockers are resolved cleanly. The sanitization coverage is complete across all `{@html}` paths with recursive handling for nested list items. The slug cache is well-implemented with TTL, deduplication, and stale-cache fallback. No new issues introduced by the fixes. Nits (dead `listNoteSlugs` export, duplicate `TYPE_COLORS`, child notes parallelization) should be tracked as follow-up work after merge.
forgejo_admin deleted branch 8-block-renderer-jinja2-sunset 2026-03-14 12:54:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/pal-e-docs-app!9
No description provided.