Block renderer + note/project/tag/repo browsing #9
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-docs-app!9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "8-block-renderer-jinja2-sunset"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
Test Plan
npm run check-- 0 errors, 0 warningsnpm run lint-- 0 errorsnpm run build-- clean production buildReview Checklist
Related
plan-pal-e-docsPhase 4 (Block Renderer + Jinja2 Sunset)PR #9 Review
Reviewed branch
8-block-renderer-jinja2-sunsetagainstmain. 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 riskAll 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:32setscontainerEl.innerHTML = svgfrom 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 inBlockRenderer.sveltestating 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.tsline 11 callslistNoteSlugs(), which in turn callslistNotes()(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/slugsAPI 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_COLORSin board[slug]/+page.svelteThe new
src/lib/colors.tsdefines a canonicalTYPE_COLORSmap with 16 note types. The existingsrc/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 likesop,convention,template, etc.Follow-up: refactor board page to import from
$lib/colors.N2.
childNotesfetch could be parallelizedIn
notes/[slug]/+page.server.ts, thelistNotes({ parent_slug: params.slug })call on line 15 happens sequentially after thePromise.allon line 7, but it does not depend on any of those results. It could be included in the initialPromise.allto save one round-trip.N3. Error handling maps all errors to 404 in note route
notes/[slug]/+page.server.tsline 41 catches all errors and throwserror(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.sveltehandles 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.
autolinkregex could match false positivesThe autolink regex
/<code>([^<]+)<\/code>/ginBlockRenderer.svelteline 24 matches any<code>content and checks againstknownSlugs. If a code block contains text that coincidentally matches a slug (e.g., someone writesdocin inline code and there is a note with slugdoc), it will be autolinked. This is by-design per the issue spec but worth calling out.N6. Board
[slug]/+page.sveltestill usesno-underlineclass on linksThe new components consistently use
no-underlinefor card-style links, which matches the existing board pattern. Good consistency.N7. ESLint config scoping is well done
The
svelte/no-at-html-tagsandsvelte/no-dom-manipulatingrules are disabled only forsrc/lib/components/blocks/**/*.svelte, not globally. This is the right approach.SOP COMPLIANCE
8-block-renderer-jinja2-sunsetreferences issue #8plan-pal-e-docsPhase 4".envfiles, credentials, or tokens in diff..env.exampleis appropriate/boards,/boards/[slug]), server loaders, and API proxy (/api/boards/[slug]/items/[id]) are unchanged$props(),$derived(),$state(),$effect()correctly throughout#0a0a14,#0e0e18,#1a1a2e,#e94560,#141420) as existing board UIsm:grid-cols-2 lg:grid-cols-3,overflow-x-autoon tables,flex-wrapon tag cloudsACCEPTANCE CRITERIA (Issue #8)
<code>slug-ref</code>to note pages viaknownSlugssetoverflow-x-auto)html_content-- thoughhtml_contentis still in the Note type, it is never rendered)VERDICT: NOT APPROVED
Two blockers require resolution before merge:
{@html}injection points, or at minimum add explicit trust-model documentation as code comments and file a follow-up issue for sanitization.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.
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>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-- wrapsisomorphic-dompurifywith asanitize()function. Config allowstarget/relattributes andcode/pretags. Clean and minimal.Coverage verified -- all
{@html}injection points are protected:{@html}locationhtmlprop comes fromautolink()which callssanitize()at line 34 of BlockRendererhtmlprop comes fromautolink()-- same pathheadersandrowsindividually mapped throughsanitize()at lines 67-68 of BlockRendereritemspassed throughsanitizeListItems()which recursively sanitizesitem.htmlat line 47 of BlockRendererNon-
{@html}DOM writes also verified:MermaidBlockline 32:containerEl.innerHTML = svg-- output frommermaid.render()which generates SVG from a text DSL, not user HTML. Standard mermaid integration pattern. Acceptable.CodeBlockline 16: uses{content}(auto-escaped by Svelte). Safe.Dependency chain correct:
isomorphic-dompurify(^3.3.0) +dompurify(^3.3.3) independencies,@types/dompurify(^3.0.5) indevDependencies.package-lock.jsonupdated.node_modulesinstalled.isomorphic-dompurifyhandles 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:
cachedSlugsif within TTL. No API call.listNotesfrom$lib/apiwhich uses$env/dynamic/private. Only imported in+page.server.ts. No client-side leakage.Caller updated:
notes/[slug]/+page.server.tsline 12 now callsgetCachedSlugs()instead oflistNoteSlugs(). Import oflistNoteSlugsremoved.NEW NITS (non-blocking)
N1. Dead code:
listNoteSlugs()inapi.tsline 226-229The 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_COLORSin board page,childNotesparallelization, all-errors-as-404, heading fallback). These are pre-existing nits, not introduced by this PR's fixes.SOP COMPLIANCE
8-block-renderer-jinja2-sunset-> issue #8)plan-pal-e-docsPhase 4)Closes #8presentpackage.jsonandpackage-lock.jsonVERDICT: 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
listNoteSlugsexport, duplicateTYPE_COLORS, child notes parallelization) should be tracked as follow-up work after merge.