Back to projects
MD2PDF logo

MD2PDF

Markdown editing application with direct PDF export through the browser's rendering engine.

Markdown PDF PWA

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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).
  5. 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

  1. Astro as Shell Host: I use the Astro layer (src/pages/index.astro and 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.
  2. Internal React Micro-Frontend: The root React container (src/components/App.tsx) acts as the core of interactivity.
  3. 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

  • Document Entity: Serves as a repository for raw text in Markdown format.
  • Image Entity: 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 (MarkdownImage in src/components/Preview.tsx) intercepts image requests generated by the Markdown parser, and if the prefix matches local-image://, it asynchronously requests the blob from IndexedDB and creates a temporary URL using URL.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.

TechnologyCategorySpecific Role
AstroFramework Shell / PWABuilds the static environment of the base website and orchestrates local PWA support through Vite.
React 19Interface & UI LayerManages editor and preview panel reactivity, including ref-hooks for virtualization and DOM control.
TypeScriptPrimary LanguageInterface contracts for the database model and global state.
ZustandState ManagementAllows subscribing components to specific state slices to minimize re-renders, and hydrates configuration using LocalStorage as an initial fallback.
Dexie.jsData PersistenceProvides a promise-based interface for IndexedDB with relational schema control.
Monaco EditorEditing EngineText engine extracted from VS Code supporting syntax, minimap, and deep events.
React-MarkdownAST CompilerTransforms raw text, supporting the remark-gfm (tables) and remark-math (KaTeX support) plugins.
Mermaid.jsDiagram-as-CodeParses and renders blocks of specific syntax into SVG graphics in the preview container, coupled with the theme.
Tailwind CSS v4Styling EngineManages the layout and applies .prose from @tailwindcss/typography, conditionally manipulated in the print view with print:!w-full rules.
BiomeJSToolchain LinterGuarantees 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.