MD2PDF
Markdown editing application with direct PDF export through the browser's rendering engine.
I designed this system to act as a single-page application (SPA) embedded within the static Astro environment, providing persistent local storage, scroll synchronization, progressive web application (PWA) support, and a seamless direct PDF export workflow utilizing the browser’s native rendering engine.
Operational Context and System Utility
My primary goal when building this application was to resolve the common friction between technical writing and document publication without relying on cloud infrastructures or third-party databases. I provide a private-by-default environment, ensuring that sensitive data—such as documentation drafts or internal company diagrams—remains isolated on the user’s local machine.
The core workflow operates as follows:
- Initialization: Upon loading, I retrieve the document list and application state from IndexedDB using Dexie.js. If no documents exist, I initialize the data store with a structured example.
- Data Input and Manipulation: The user interacts with the Monaco Editor interface, which natively intercepts keyboard events, formatting structure, and even images pasted from the clipboard.
- In-Memory Optimization: I intercept pasted images before event propagation, resize them using an internal canvas to limit the maximum width to 800 pixels, re-encode them for efficiency, and save them asynchronously to the browser storage system (IndexedDB) using appropriate MIME types (such as WebP or JPEG).
- Real-Time Rendering: As the text model changes, I use Zustand (the state manager) to trigger updates for the document preview. React-Markdown processes the Abstract Syntax Tree (AST) to render mathematical formulas (KaTeX), syntax highlighting (highlight.js), and vector diagrams (Mermaid.js).
- Native Export: Instead of relying on heavy server-side PDF rendering engines, I explicitly adjust the document’s DOM style tree for the print view, inject CSS configurations that hide the user interface, and invoke the
window.print()command, leveraging the high fidelity of the browser’s native PDF engine.
Overall Architecture Analysis
I structured the project following a highly asymmetric hybrid architecture, where the server or build layer is simply responsible for providing the shell, delegating 100% of the business processing to the client.
architecture-beta
group client(cloud)["Client / Web Browser"]
service astro(server)["Astro Shell"] in client
service react(server)["React SPA"] in client
service storage(database)["IndexedDB / Dexie"] in client
astro:L -- R:react
react:B -- T:storage
Structural Patterns
- Astro as Shell Host: I use the Astro layer (
src/pages/index.astroand layouts) to provide static entry points, global PWA configuration (vite-plugin-pwa), and Vercel Analytics SDK integration for metrics, thereby avoiding hydration of heavy frameworks at static startup. - Internal React Micro-Frontend: The root React container (
src/components/App.tsx) acts as the core of interactivity. - Unidirectional State Flow: Following patterns analogous to Clean Architecture applied to the frontend, I manage state with a single source of truth repository (Zustand), where the view (React) simply reflects state changes, and user events dispatch mutations (actions) that I consolidate and insert into the persistence layer (Dexie.js).
Data Modeling and Event Logic
I achieve persistence using the IndexedDB API, orchestrated through the Dexie.js wrapper to get reliable asynchronous promises.
erDiagram
DOCUMENT {
number id PK
string title
string content
date updatedAt
}
IMAGE {
number id PK
blob blobData
string mimeType
date createdAt
}
DOCUMENT ||--o{ IMAGE : "references via markdown local-image://{id}"
Entities and Relationship Flow
DocumentEntity: Serves as a repository for raw text in Markdown format.ImageEntity: Acts as a binary local Blob Store. Instead of embedding base64 directly into the markdown (which would critically slow down the editor’s AST), I intercept binary data pasting in Monaco, reduce its dimensions via the Canvas API, inject it into IndexedDB, and generate a custom internal URI (e.g.,local-image://12).- Visual Lifecycle Interception: A wrapper component (
MarkdownImageinsrc/components/Preview.tsx) intercepts image requests generated by the Markdown parser, and if the prefix matcheslocal-image://, it asynchronously requests the blob from IndexedDB and creates a temporary URL usingURL.createObjectURL(). The object is revoked once the component unmounts, thus protecting the browser’s memory management.
Component Synchronization
sequenceDiagram
participant User
participant Editor as Monaco Editor
participant Zustand as Store
participant Dexie as IndexedDB
participant Preview as Preview Panel
User->>Editor: Types Markdown
Editor->>Zustand: updateCurrentDocument(content)
Zustand->>Dexie: put(updatedDoc)
Zustand-->>Preview: React State Change
Preview->>Preview: Parses Markdown AST
Preview-->>User: Renders Interface
Technological Stack and Tooling Role
My selection of the technology stack prioritizes pure main-thread performance, client isolation, and typographic standardization.
| Technology | Category | Specific Role |
|---|---|---|
| Astro | Framework Shell / PWA | Builds the static environment of the base website and orchestrates local PWA support through Vite. |
| React 19 | Interface & UI Layer | Manages editor and preview panel reactivity, including ref-hooks for virtualization and DOM control. |
| TypeScript | Primary Language | Interface contracts for the database model and global state. |
| Zustand | State Management | Allows subscribing components to specific state slices to minimize re-renders, and hydrates configuration using LocalStorage as an initial fallback. |
| Dexie.js | Data Persistence | Provides a promise-based interface for IndexedDB with relational schema control. |
| Monaco Editor | Editing Engine | Text engine extracted from VS Code supporting syntax, minimap, and deep events. |
| React-Markdown | AST Compiler | Transforms raw text, supporting the remark-gfm (tables) and remark-math (KaTeX support) plugins. |
| Mermaid.js | Diagram-as-Code | Parses and renders blocks of specific syntax into SVG graphics in the preview container, coupled with the theme. |
| Tailwind CSS v4 | Styling Engine | Manages the layout and applies .prose from @tailwindcss/typography, conditionally manipulated in the print view with print:!w-full rules. |
| BiomeJS | Toolchain Linter | Guarantees syntactic consistency, replacing Prettier/ESLint in the code analysis phase. |
Architectural Impact and Printability Design
The key success of this architecture lies in the design of its print mode (Print Mode Override). Instead of relying on the industry-standard dependency (react-to-print), I hijack the React lifecycle for a brief period:
Upon invoking the export, I inject a synchronous state of "light" into the Store, forcing React and Mermaid.js to render the entire DOM tree in pure contrast without dark backgrounds. A setTimeout calculates the exact DOM repaint cycle (approx. 1500ms), I invoke the window.print() dialog box (which temporarily pauses the JS engine), and upon receiving control back, I immediately restore the dark theme if it was enabled. This, combined with resetting margins via @media print style sheets, allows for clean, precise, native PDF exports without duplicate borders.