# Architecture
A practical overview for someone (human or agent) about to read or extend the codebase.
## High-level shape
`mdview` is a **local CLI** that boots a **Fastify HTTP server** and opens the browser to a **Preact single-page app**. The server renders markdown to HTML on the fly and pushes file-change events through Server-Sent Events. The SPA fetches rendered files via a JSON API and refreshes content in place when SSE events arrive — preserving scroll/outline state.
```
┌──────────┐ ┌────────────────────────┐
│ CLI │ spawns │ Fastify HTTP server │
│ src/cli │ ───────────────────────▶│ src/server/index.ts │
└──────────┘ └────────┬───────────────┘
│
│ serves
▼
┌────────────────────────┐
│ Browser (Preact SPA) │
│ src/client/main.tsx │
└────────────────────────┘
│
│ fetch /api/file → JSON
│ fetch /api/tree → JSON (incl. project config)
│ fetch /api/search → JSON
│ subscribe /api/watch (SSE)
▼
┌────────────────────────┐
│ 3-pane reading view │
└────────────────────────┘
```
The server stays running for the lifetime of one CLI invocation. Killing the process tears everything down.
## Repo layout (post Phase 2)
```
src/
├── cli.ts ← CLI entry, args, port fallback, graceful shutdown
├── shared/ ← cross-runtime modules (no Node-only or DOM-only deps)
│ ├── types.ts
│ ├── search-pattern.ts ← compilePattern (case/word/regex) used by client + server
│ ├── relative-time.ts ← formatRelativeTime (Updated N ago)
│ └── tree-utils.ts ← MD_EXT, flattenMdRelPaths
├── render/ ← pure server-agnostic markdown render pipeline
│ ├── markdown.ts
│ ├── shiki.ts
│ ├── frontmatter.ts
│ ├── outline.ts
│ ├── links.ts ← tagInternalLinks, rewriteImageSrc
│ └── math.ts ← KaTeX placeholder emission
├── server/
│ ├── index.ts ← createServer factory + .mdview.json watcher
│ ├── config.ts ← loadEffectiveConfig (global + project), mergeConfigs, validateConfig
│ ├── routes/
│ │ ├── api-file.ts ← GET /api/file
│ │ ├── api-tree.ts ← GET /api/tree (incl. project config)
│ │ ├── api-asset.ts ← GET /__asset/*
│ │ ├── api-search.ts ← GET /api/search (folder grep)
│ │ └── sse.ts ← GET /api/watch
│ ├── fs/
│ │ ├── resolve.ts ← resolveSafePath (security boundary!)
│ │ ├── ignore.ts ← DEFAULT_IGNORED_DIRS + isPathIgnored (shared with watcher + walkFolder)
│ │ ├── tree.ts ← walkFolder
│ │ └── grep.ts ← folder-wide search backend
│ └── watcher.ts ← chokidar wrapper + emitSynthetic for config events
└── client/
├── main.tsx ← Preact mount + JetBrains Mono CSS
├── App.tsx ← top-level orchestration (slim — most logic in hooks)
├── shortcuts.ts ← single-source-of-truth keyboard shortcut registry
├── components/ ← UI components (panes + overlays)
├── hooks/ ← signals + effects (use*.ts)
├── lib/ ← DOM augmentation helpers (wire*.ts) + pure utils
└── styles/ ← reset / theme / layout / content / components
tests/
├── server/ ← vitest unit tests (markdown, math, grep, config, …)
└── client/ ← persisted-signal, scroll-spy, outline-nav, search-pattern, …
```
## Server (`src/server/`)
### Entry & boot
- **`src/cli.ts`** — argument parser, port-fallback listener, browser launcher, SIGINT/SIGTERM shutdown with timeout, friendly error messages (port-in-use, ENOENT, EACCES). `MDVIEW_DEBUG=1` enables full stack traces.
- **`src/server/index.ts`** — `createServer(opts)` factory. Composes routes, registers `@fastify/static` for the bundled SPA at `/`, hooks `onClose` to close the chokidar watchers. Boots a **second chokidar watcher** dedicated to `.mdview.json` because the main watcher's `ignored` filter excludes dotfiles.
### Routes (`src/server/routes/`)
| Route | Purpose |
|-------|---------|
| `GET /api/file?path=...` | Read the file, parse frontmatter, render to HTML, extract outline, tag internal links, rewrite image src. Returns a `RenderedFile` (incl. `lastModified` mtime). |
| `GET /api/tree` | Walk the open folder; return a nested `TreeNode[]` with markdown files flagged. Response also carries the validated project `config` (`.mdview.json`). |
| `GET /api/search?q=...&case=...&word=...®ex=...` | Folder-wide grep. Caps per-file (20) and global (200); returns snippets with highlight ranges. |
| `GET /api/watch` | Server-Sent Events stream of `WatchEvent`s as files change. Includes a synthetic `config` event when `.mdview.json` changes. |
| `GET /__asset/*` | Serve user content assets (images, etc.) safely via `resolveSafePath`. |
| `GET /*` | Falls through to the static SPA bundle; SPA fallback for unknown routes (so client routing works on refresh). |
All request paths that touch the filesystem go through `resolveSafePath(rootAbsPath, rel)` — see `src/server/fs/resolve.ts` — which rejects absolute paths and traversal attempts.
### Rendering pipeline (`src/render/`)
The renderer lives in `src/render/`, **outside** `src/server/`, because it has no Node-only or HTTP-specific code. It can run from any context (server route, future editor extension, tests). Each request to `/api/file` runs through this pipeline in order:
1. **`frontmatter.ts`** — `parseFrontmatter(raw)` peels off `---`-delimited YAML using `gray-matter`. Returns `{ data, body }`.
2. **`markdown.ts`** — `renderMarkdown(body)` constructs a singleton `markdown-it` instance with `linkify`, `markdown-it-anchor` (custom slugify), `markdown-it-task-lists`, and the local **`mathPlugin`**. Walks the token list to intercept `fence` tokens: `mermaid` becomes a `
` for client rendering, everything else is highlighted via Shiki.
3. **`shiki.ts`** — `highlightCode(code, lang)` lazy-loads languages on demand; uses dual-theme (`github-light` / `github-dark`) with `defaultColor: false` so the client can swap themes purely via CSS.
4. **`math.ts`** — markdown-it core rule that scans for `$$...$$` paragraphs and `$...$` inline runs (with whitespace heuristics and code-span exclusion), emitting `` / `