Markdown Pipeline
Markdown rendering goes through the React processor below. src/lib/markdown.ts is a separate utility module — not a processor.
-
Markdown processor (
src/components/markdown/markdown-renderer.client.tsx, with amarkdown-renderer.server.tsxSSR variant)- Used by: Public pages, dashboard, interactive preview
- Output: React components via
rehype-react - When to modify: For all remark/rehype plugin additions
-
Markdown utilities (
src/lib/markdown.ts)- Not a processor — small helpers used by API routes/services:
generateSlug,generateExcerpt,isReservedSlug,validateMarkdown
- Not a processor — small helpers used by API routes/services:
Primary Processor Architecture
File: src/components/markdown/markdown-renderer.tsx
Processing Flow:
- Entry point: React component renders markdown via
unified()pipeline - Transforms: Markdown String → MDAST → HAST → React JSX
- Uses
rehype-reactto convert HTML AST to React components - Renders directly as React JSX (no HTML string intermediate)
- Custom components:
<CodeMirrorCodeBlock>,<ImageWithResize>,<Heading>, etc.
Used By:
src/app/[domain]/[collectionSlug]/[skriptSlug]/[pageSlug]/page.tsx- Public pagessrc/components/public/annotatable-content.tsx- Annotatable contentsrc/components/dashboard/interactive-preview.tsx- Dashboard preview
Markdown utilities (src/lib/markdown.ts)
Not a markdown processor — a small utility module. Exports:
generateSlug(title)/isReservedSlug(slug)— URL slug generation + reserved-word guardgenerateExcerpt(content)— plain-text excerpt for previews/SEOvalidateMarkdown(content)— lightweight content validation
Used by API routes and src/lib/services/{skripts,pages}.ts.
Plugin Execution Order
The markdown transformation follows this exact plugin order (critical for proper rendering):
Remark Plugins (Operate on Markdown AST)
-
remarkParse- Parse markdown string into MDAST (Markdown Abstract Syntax Tree) -
remarkImageResolver(src/lib/remark-plugins/image-resolver.ts)- Hybrid plugin: Queries DB on server (skriptId), uses fileList on client
- Resolves relative image paths to
/api/files/{id}URLs - Skips absolute URLs, .excalidraw files, and video files (handled by other plugins)
- Sets
data-original-srcattribute for reference
-
remarkExcalidraw(src/lib/remark-plugins/excalidraw.ts)- Hybrid plugin: Queries DB on server (skriptId), uses fileList on client
- Handles
.excalidrawfiles by finding light/dark SVG variants - Sets
data-light-src,data-dark-src,data-excalidrawattributes - Falls back to
/missing-file/URL with?missing=query param if variants not found
-
remarkMuxVideo(src/lib/remark-plugins/mux-video.ts)- Hybrid plugin: Queries DB on server (skriptId), uses fileList on client
- Transforms
.mp4/.movreferences to Mux video components - Looks up
{video}.jsonmetadata file for playback ID, poster, blur data - Creates custom
<muxvideo>element with Mux-specific data attributes
-
remarkCodeEditor(src/lib/remark-plugins/code-editor.ts)- Converts code blocks with
editorkeyword to interactive editors - Syntax:
```python editor```or```sql editor db="database.db"``` - Transforms to custom
<code-editor>element withdata-*attributes - Supports multi-file syntax, IDs, and database references
- Converts code blocks with
-
remarkCallouts(src/lib/remark-plugins/callouts.ts)- Transforms Obsidian-style callouts:
> [!type]syntax - 41 callout types with aliases:
- Base types: note, tip, warning, abstract, info, todo, success, question, failure, danger, bug, example, quote, solution, discuss
- Aliases:
lernziele→success,hint→tip,exercise→abstract, etc.
- Foldable syntax:
> [!note]-(folded) or> [!note]+(open) - Generates structure:
<blockquote class="callout callout-{type} [callout-foldable] [callout-folded]"> <div class="callout-title {type}"></div> <div class="callout-content">...</div> </blockquote>
- Transforms Obsidian-style callouts:
-
remarkMath- Parse LaTeX math ($...$or$$...$$) -
remarkGfm- GitHub-Flavored Markdown (tables, strikethrough, task lists) -
remarkServerImageOptimizer(Server-only, dynamically added)- Downloads remote images and caches in
/public/cache/images/[domain]/[skriptId]/ - Only runs in Node.js environment
- Downloads remote images and caches in
Rehype Plugins (Operate on HTML AST)
-
remarkRehype- Convert MDAST → HAST (HTML AST)allowDangerousHtml: truepreserves custom elements
-
rehypeSlug- Add IDs to headings# My Heading→<h1 id="my-heading">My Heading</h1>
-
rehypeHeadingSectionIds(src/lib/rehype-plugins/heading-section-ids.ts)- Adds
data-section-id(e.g., "h1-my-heading") - Adds
data-heading-text(extracted text content) - Used by annotation system for precise targeting
- Adds
-
rehypeAutolinkHeadings- Add anchor links to headings- Creates
<a class="heading-link" href="#...">inside headings behavior: 'wrap'wraps entire heading content
- Creates
-
rehypeExcalidrawDualImage(src/lib/rehype-plugins/excalidraw-dual-image.ts)- Handles theme-aware Excalidraw drawings
- Wraps in
<figure>with both light/dark SVG variants - CSS shows appropriate variant based on theme class
-
rehypeImageOptimizer(src/lib/rehype-plugins/image-optimizer.ts)- Adds
loading="lazy"anddecoding="async"to all images
- Adds
-
rehypeKatex- Process LaTeX math to HTML -
rehypeHighlight- Syntax highlighting (non-editor code blocks only) -
rehypeStringify- Convert HAST → HTML stringallowDangerousHtml: truepreserves custom elements
Client-Side Hydration
After server-side processing, the client performs selective hydration:
-
Code Editors: Finds
<code-editor>custom elements, extractsdata-*attributes, looks up DB file URL via fileList, mounts React<CodeEditor>in place. -
Callout Interactivity: Finds
blockquote.callout-foldable, attaches click handlers that toggle.callout-folded. -
Theme Updates: Re-renders all code editors when theme changes, preserving user state.
Markdown Context
The MarkdownContext object flows through the pipeline:
interface MarkdownContext {
pageId?: string // For user data persistence
domain?: string // Username for file resolution
skriptId?: string // For file API lookups
fileList?: Array<{ // Pre-fetched files for this skript
id: string
name: string
url?: string
isDirectory?: boolean
}>
theme?: 'light' | 'dark' // For Excalidraw theme selection
}
File List Usage:
- Server: Passed to
remarkImageResolverand other file-resolving plugins - Client: Fetched via
/api/upload?skriptId={id}during hydration - Used to resolve filenames → URLs for images and databases
Key Design Patterns
-
Data Attributes for Hydration — plugins store metadata in
node.data.hProperties(becomes HTML attributes); client reads viagetAttribute()/querySelectorAll(). -
HTML Entity Escaping — code content escaped to prevent XSS; client decodes via textarea trick.
-
Lazy Hydration — full HTML rendered immediately; React components only loaded for interactive elements.
-
Theme-Aware Rendering — Excalidraw light/dark variants both in DOM, CSS controls visibility; code editors re-render on theme change.
-
Plugin Composition — single responsibility per plugin; order matters (file resolution before image processing).
Debugging Tips
Plugin not running: check TypeScript types (especially tree: Root parameter). Add console.log() to verify execution.
Wrong output: plugin order matters. File resolver must run before image processing.
Hydration fails: custom element attributes missing or HTML entities not decoded.
Theme not switching: CSS classes not applied or images not duplicated.