# PRD: Markdown Viewer for /files Page **Status:** Draft **Date:** 2026-03-13 **Scope:** outline-sync webui — `/files` page enhancement --- ## Problem The `/files` page currently displays a static file tree showing document names and file sizes. Files are not interactive — there is no way to read document content within the web UI. Users who want to verify a document's content must access the vault directory directly (container shell, WebDAV mount, or Obsidian). This breaks the self-contained nature of the web UI and is a missing piece in the read workflow: pull → verify content → push. --- ## Goal Make every `.md` file in the vault tree clickable, opening a clean rendered view of the document without leaving the web UI. --- ## Library Assessment The [awesome-markdown-editors](https://github.com/mundimark/awesome-markdown-editors) list is focused primarily on **editors** (WYSIWYG, Monaco-based, cloud tools), not standalone viewers/renderers. The relevant rendering engines mentioned are `markdown-it`, `marked.js`, `highlight.js`, `KaTeX`, and `Mermaid`. For this project's architecture — a pure FastAPI app with all HTML/CSS/JS inline in `webui.py`, no npm, no build system, no CDN currently in use — the viable options are: | Approach | Library | Pro | Con | |----------|---------|-----|-----| | Server-side (Python) | `mistune` | No CDN, consistent with existing diff rendering, works offline/air-gapped | New pip dependency | | Server-side (Python) | `markdown2` | Same as above, supports fenced code + tables via extras | New pip dependency | | Client-side (CDN) | `marked.js` | Zero Python deps, ~50 KB | CDN call from container; breaks if offline | | Client-side (CDN) | `markdown-it` | Plugin ecosystem | Same CDN concern + more complex | **Recommendation: `mistune` (server-side)** Reason: The existing diff renderer (`difflib.HtmlDiff`) is already server-side. `mistune` is a lightweight pure-Python CommonMark-compatible parser (~25 KB, no transitive deps). It keeps the architecture consistent, avoids CDN latency from inside Docker, and doesn't require any frontend changes to `_SCRIPT`. `markdown2` is an equally valid alternative with a nearly identical API. --- ## User Stories 1. **As a user**, I can click any `.md` file in the vault tree to open a rendered view of its contents. 2. **As a user**, I can see the document's markdown rendered as formatted HTML (headings, lists, code blocks, bold/italic, tables). 3. **As a user**, the YAML frontmatter block (`---` ... `---`) is either hidden or rendered as a collapsible metadata section, not shown as raw YAML in the document body. 4. **As a user**, I can navigate back to the file tree with a single click (back link or breadcrumb). 5. **As a user**, non-markdown files (e.g. `.json`, `.txt`) show a plain text fallback, not an error. 6. **As a user**, attempting to view a file outside the vault directory results in a 403, not a path traversal. --- ## Scope ### In scope - New route `GET /files/view?path=` that serves a rendered HTML view of the file - Frontmatter stripped from rendered output; optionally shown as a collapsible `
` block with the sync metadata fields - Clickable file links in `_build_tree_html()` for `.md` files (and other text files) - Back-navigation link to `/files` - Path traversal protection: resolve the path and assert it is within `VAULT_DIR` - `mistune` added to `Dockerfile` pip install line ### Out of scope - Editing files within the viewer (separate feature) - Syntax highlighting for code blocks (can be added later with `pygments`) - Full-text search across vault files - Non-text binary files (images, PDFs) - Live refresh / watch mode --- ## Technical Design ### New route ```python GET /files/view?path= ``` - Decode and resolve path relative to `VAULT_DIR` - Assert resolved path is inside `VAULT_DIR` (403 if not) - Assert file exists and is a regular file (404 if not) - Read content as UTF-8 - Strip frontmatter using existing `parse_frontmatter()` from `outline_sync.py` - Render body with `mistune.create_markdown()` - Wrap in `_page()` with a back-link header card and the rendered content card ### File tree changes In `_build_tree_html()`, wrap `.md` file names in anchor tags: ```html {item.name} ``` Non-`.md` files remain as plain `` labels (or optionally link to a raw text view). ### Frontmatter handling Use the already-existing `parse_frontmatter()` helper to split metadata from body before rendering. Optionally render frontmatter fields in a collapsed `
` block styled consistently with the existing card/badge UI. ### Path traversal protection ```python resolved = (VAULT_DIR / rel_path).resolve() if not str(resolved).startswith(str(VAULT_DIR.resolve())): raise HTTPException(status_code=403) ``` ### Dependency change `Dockerfile` pip install line gains `mistune`: ``` pip install --no-cache-dir requests fastapi "uvicorn[standard]" pydantic mistune ``` --- ## UI Design **File tree** — `.md` files become blue underlined links. Non-markdown files stay as grey `` labels. **Viewer page layout:** ``` ┌──────────────────────────────────────────────────────┐ │ ← Back to Files Collection/Document.md │ ├──────────────────────────────────────────────────────┤ │ [▶ Sync metadata] (collapsed
) │ ├──────────────────────────────────────────────────────┤ │ │ │ # Document Title │ │ │ │ Rendered markdown body... │ │ │ └──────────────────────────────────────────────────────┘ ``` Styling reuses existing `.card` and `_BASE_CSS` variables — no new CSS classes needed beyond a `.md-body` prose wrapper (max-width, line-height, heading sizes). --- ## Acceptance Criteria - [ ] Clicking a `.md` file in the vault tree navigates to `/files/view?path=...` - [ ] Rendered page shows formatted headings, paragraphs, lists, bold/italic, code blocks, and tables - [ ] YAML frontmatter is not visible in the rendered body - [ ] Requesting `/files/view?path=../../etc/passwd` returns 403 - [ ] Requesting a non-existent path returns 404 - [ ] Back link returns to `/files` and preserves the tree state - [ ] `mistune` is installed in the Docker image (Dockerfile updated) - [ ] No new test files required for MVP, but the new route is covered by a basic integration test asserting 200, 403, 404 --- ## Risks & Mitigations | Risk | Mitigation | |------|-----------| | Path traversal to read secrets outside vault | Strict `resolve()` + prefix check; test explicitly | | `mistune` API changes (v2 vs v3) | Pin version in Dockerfile; use `mistune.create_markdown()` stable API | | Very large files causing slow response | Enforce a max file size read limit (e.g. 2 MB) with a warning | | Malformed/binary `.md` files | Wrap read in try/except, fall back to plain `
` block |