A Markdown editor that produces print-ready PDFs, entirely client-side, in your browser.
You write Markdown in the editor; one click switches to a paginated
preview that matches the PDF exactly — click anywhere in the
preview to jump back to the editor at that spot. When you're done
you save as .pdf, .md, or .tex.
- No installation. Open the page, start writing.
- No account, no subscription. A static web app — host it yourself in five minutes if you want.
- Nothing leaves your machine by default. Documents, images, settings — all in the browser's local storage. No server-side state, no telemetry. The two opt-in exports (OneDrive upload, share-link) only push content out when you explicitly click them.
- Direct to PDF. No round-trip to an external service. The browser's own print engine renders the same paged layout you see in the preview, you click Save as PDF.
A Markdown extended for technical and scientific writing:
- Math via MathJax 4 (
$x^2$inline,$$ … $$displayed, or fencedmathblocks), with selectable math font set - Inference rules (
```inference) — premises / dashes / conclusion, with automatic Gunter / Scott typography (calligraphic semantic functions, bold constructors inside⟦…⟧, sans-serif functions outside, numeric subscripts) - Commutative diagrams (
```category) — declarative syntax (f : A -> B, equations,by (…)for universal morphisms), type-checked compositions, native SVG renderer with Mermaiddagrefallback for tricky topologies - EBNF grammars (
```ebnf) as railroad / syntax diagrams, one diagram per production with aligned=signs - Algebraic data types (
```adt) —LHS ::= Ctor | Ctor(args)definitions typeset with aligned|separators and two-tier highlighting (defined types vs constructors) - Faust block-diagram algebra (
```bda) — left-to-right audio-DSP signal-flow circuits, five binary operators (~ : , <: :>), native SVG renderer with optionalz⁻¹markers - Mermaid diagrams (flowcharts, sequence, class, state, …)
and inline
chartblocks (line, bar) — all rendered as SVG - Unified diffs (
```diff) with per-line green/red/grey colouring - Indented trees (
```tree) → Unicode box-drawing tree or top-down SVG (tree svg) - Algorithmic pseudocode (
```algorithm) with line numbers and bolded keywords, à la LaTeXalgorithm2e - Syntax-highlighted code blocks — ~20 common languages bundled (Python, Rust, JS/TS, Go, C/C++, Haskell, OCaml, SQL, …) plus a custom Faust grammar for audio DSP specs
- Captions and cross-references — any rich fence can carry a
quoted caption (auto-numbered "Figure N", "Listing N",
"Algorithm N", "Table N") and a
\label{key}; reference anywhere with\ref{key} - Side-by-side demos (
```demo) — source markdown and its rendered output displayed in two columns, with auto-zoom in slides mode - Slides mode — 16:9 PDF presentations à la Beamer, every
## h2starts a new slide; activate via Settings → Page → Format = Slides 16:9, or per-document via theslides: truefrontmatter key - Callouts (
::: theorem,::: note,::: warning, …) with optional titles - Footnotes (
[^id]) and Pandoc-lite citations ([@key]+[@key]: …), each with auto-numbered end-of-document sections and back-links - Definition lists and CSV/TSV tables
- On-the-fly input ligatures:
->becomes→,\alphabecomesα,\|Nbecomesℕ; double-backslash escapes (\\alphakeeps the source literal) - Section auto-numbering — write the first heading the way you want, the rest follows the same style
- YAML frontmatter — optional
---block at the top of a doc withtitle:/author:/organization:/date:overrides per-document,slides: trueto switch to slides mode, plusmathjax-preamble:for defining\newcommandmacros once and using them in every formula - Running page header / footer (
```header/```footer) — fill the top / bottom margin of every page with up to three slots (left | center | right); substitutions{page},{pages},{date}resolve per-page
| Format | Use case |
|---|---|
| The final document, paginated with the typography from Settings | |
Markdown (.md) |
Portable source, opens in any editor |
LaTeX (.tex) |
Hand-off to a journal that wants TeX sources (compiles with xelatex --shell-escape) |
| OneDrive | Uploads the .md to your OneDrive Apps/markpage/ folder and copies an anonymous share link to the clipboard |
| Share link | Encodes the doc into a ?import=… URL anyone can open in markpage to load it as a fresh local copy — no account, no server. Capped at ~8 KB of source; bigger docs fall back to OneDrive. |
| Send by email | Same encoded URL, but opens the user's mail client with the link pre-filled in the body |
- Multiple settings profiles — switch in one click, import or export each as a JSON file
- Interface and document languages chosen independently (French / English; the architecture supports adding more)
- Pick fonts from a curated list of Google Fonts, or paste any
fonts.googleapis.comURL for a custom family — plus pre-paired font packs that align the body / headings / code family with the math font in one click (Fira, STIX Two, …) - Fine-grained typography: page format, margins, justification, line height, per-heading size / colour / weight / italic / underline, asymmetric heading and paragraph spacing
- A built-in Help tutorial that opens in a side window without disturbing your document
markpage ships an optional MCP bridge (markpage-mcp) so an AI client
(Claude Desktop / Claude Code) can drive the app in your browser: read and
write the current document, switch views, list render errors, manage the
library, and export — plus two tab-free tools that hand the AI markpage's
authoring guide and fence syntax, so it writes idiomatic markpage Markdown
even if it has never seen the app.
Open the MCP pill (bottom-right of the app) to download the prebuilt
bridge for your platform and copy the claude mcp add markpage … command,
then restart your AI client. Architecture and the full tool list:
MCP-SPEC.md; build / install / release details:
mcp/README.md.
npm install
npm run dev # development server
npm run build # production build into dist/
npm run typecheck # type-check
npm test # regression test corpusA push to main runs .github/workflows/deploy.yml,
which publishes the static build to GitHub Pages.
To enable Pages on a fresh fork: Settings → Pages → Source: GitHub Actions.
markpage is an npm-workspaces monorepo. Several deliverables share one render core, so a document looks the same wherever it is rendered:
- the web app (
src/) — the editor + paginated preview, built with Vite; - three npm packages (
packages/*) — the reusable rendering stack; - a VS Code extension (
vscode/) — a live preview that reuses that core; - a Go MCP bridge (
mcp/) — lets an AI client drive the web app in-browser; - specs, skill, templates (
docs/,skill/,templates/).
The keystone is @orlarey/markpage-render: the Markdown → HTML pipeline,
consumed both by the web app and the VS Code extension. @orlarey/blocks is
the dependency-free base; @orlarey/marked is a separate, lightweight plugin for
third-party pipelines (see Use the fences in your own pipeline below).
flowchart TB
subgraph pkgs["packages/ — reusable npm stack"]
blocks["@orlarey/blocks<br/>fence renderers (HTML/SVG)"]
marked["@orlarey/marked<br/>marked plugin (standalone)"]
core["@orlarey/markpage-render<br/>full pipeline + MathJax/Mermaid hydrate"]
blocks --> marked
blocks --> core
end
app["Web app — src/ (Vite)<br/>editor + paginated preview, storage, volumes"]
ext["VS Code extension — vscode/<br/>live preview (webview, esbuild)"]
mcp["markpage-mcp — mcp/ (Go)<br/>MCP ↔ WebSocket bridge"]
ai["AI client<br/>(Claude Code / Desktop)"]
core --> app
core --> ext
ai -->|"stdio (MCP)"| mcp
mcp -->|"ws 127.0.0.1:7878"| app
markpage/
├── src/ Web app — editor + preview (Vite, TypeScript)
├── packages/ Reusable npm packages (the rendering stack)
│ ├── blocks/ @orlarey/blocks — framework-agnostic fence renderers
│ ├── marked/ @orlarey/marked — marked plugin wrapping the renderers
│ └── markpage-render/ @orlarey/markpage-render — the full render pipeline
├── vscode/ VS Code extension — live markpage preview (esbuild)
├── mcp/ markpage-mcp — Go MCP bridge (stdio MCP ↔ WebSocket)
├── skill/ "markpage-specs" authoring skill (SKILL.md)
├── docs/ Specs & design docs (SPEC, MCP-SPEC, FRONTMATTER-SPEC, …)
├── templates/ Ready-to-use documents (+ style profiles)
├── tests/ Vitest regression corpus
├── e2e/ Playwright end-to-end tests
├── patches/ patch-package patches (paged.js null-deref fixes)
└── public/ Static assets served as-is
| Package | Path | Role |
|---|---|---|
@orlarey/blocks |
packages/blocks/ |
The base layer — framework-agnostic renderers for the fenced blocks (chart, bda, category, adt, diff, tree) → self-contained HTML/SVG, plus a portable stylesheet. No dependencies. |
@orlarey/marked |
packages/marked/ |
A marked plugin (marked.use(markpageBlocks())) for dropping the fences into any third-party Markdown toolchain. Peers: @orlarey/blocks, marked. |
@orlarey/markpage-render |
packages/markpage-render/ |
The app's full pipeline — fences, callouts, footnotes, refs, frontmatter parsing + the phase-B DOM hydrate (MathJax 4, Mermaid). Registers the blocks via its own marked config. Consumed by both the web app and the VS Code extension. |
- Shared core, two front-ends. The web app and the extension both import
@orlarey/markpage-render, so the editor preview, the PDF, and the VS Code preview agree on rendering. The app adds storage / volumes / settings / pagination; the extension adds a webview and translates a document's frontmatter profile into CSS. - Dev vs publish. In development the app (Vite) and the extension (esbuild)
consume the packages' TypeScript sources through the
developmentexport condition — no build step between edits. Thedist/(tsc) output is only needed for an npm publish.postinstallrunspatch-package(the paged.js fixes inpatches/). - The MCP bridge is a separate Go binary. It speaks MCP over stdio to the AI
client and WebSocket to one markpage browser tab; two tools answer from
embedded docs without a tab. See
mcp/README.mdandMCP-SPEC.md.
npm run build:packages # tsc each package → its dist/
npm run build # build:packages, then tsc --noEmit, then vite build (app)
node vscode/esbuild.mjs # bundle the VS Code extension
make -C mcp build # build the Go MCP bridge → mcp/markpage-mcpSpecs and design docs live in docs/ (with an index). Quick links:
FEATURES.md/FEATURES.fr.md— the full feature list, including every supported fence.- Help — the in-app tutorial (the Help button); source also at
src/HELP.fr.md/src/HELP.en.md. AI-AUTHORING.md— how to write markpage Markdown: every fenced block and convention. (Also what the MCPget_authoring_guidetool serves to an AI.)
SPEC.md— the app architecture reference (storage model, render pipelines, i18n, LaTeX export, regression test harness).MCP-SPEC.md— the MCP bridge: action↔tool audit, protocol, contract. Build / install / release:mcp/README.md.FORMAL-METHOD-SPEC.md— how specifications are written in this project (methodology, not a feature spec).FILESYSTEM-BLUEPRINT.md— a reusable blueprint (model, invariants, browser-sandbox constraints + an AI implementation recipe) for building a desktop-like, serverless file system in a static web app — markpage as the worked example.
The original design documents; every feature below has shipped, so they read as reference + history:
CATEGORY-SPEC.md— the commutative-diagram language (category).MOSAIC-SPEC.md— the justified image gallery (mosaic).TOC-PLUS-SPEC.md— the table of contents + plan (::: toc+).FILE-MANAGEMENT-SPEC.md— document / asset storage and the disk-link feature (the original model, since superseded byVOLUMES-SPEC).VOLUMES-SPEC.md— the unified file system: one Open, one root, mounted volumes (Library / Disk / GitHub repo / OneDrive).GITHUB-SYNC-SPEC.md— shared documents across devices via a GitHub repo (fine-grained PAT, no server); the R1–R4 engine under the GitHub volume.FRONTMATTER-SPEC.md— the frontmatter contract: accepted YAML subset, the full key catalogue (metadata / layout / typography / themarkpage-profileJSON), precedence, the portable-profile model.BACKGROUND-SPEC.md— the::: backgroundblock: a page backdrop layer of positioned markdown minipages (full-page fill, cascading likeheader/footer) for covers and slide templates.STYLE-SPEC.md— the::: stylecompanion block: local, controlled typographic overrides on recursive markdown content (colour, size, font, weight, align…).
The rich block renderers (chart, bda, category, adt, diff, tree)
are published as standalone, framework-agnostic packages — drop them into any
Markdown toolchain, no markpage app required:
@orlarey/marked— a marked plugin.marked.use(markpageBlocks())and the fences render, with optional auto-numbered figure captions.@orlarey/blocks— the renderers + a registry for any pipeline (renderBlock('chart', body, info)→ HTML/SVG). Ships a portable stylesheet (@orlarey/blocks/styles.css, scoped to.markpage).
import { marked } from 'marked';
import { markpageBlocks } from '@orlarey/marked';
import '@orlarey/blocks/styles.css';
marked.use(markpageBlocks());
const html = marked.parse('```chart line "Sales"\nq, rev\nQ1, 12\nQ2, 19\n```');
// wrap the output in <div class="markpage">…</div> so the styles applyFence body + option syntax: AI-AUTHORING.md. See each package's README for the full API.
Ready-to-customise documents under templates/:
- Facture (FR) —
templates/facture.mdis a French invoice scaffold using the dedicated```sender/```recipient/```signatureletterhead blocks (side-by-side flex layout), followed by a pipe-table of items, a totals block, and a::: cautioncallout for the mandatory mentions légales. Pair it with the matching profiletemplates/profil-facture.json: serif body (Source Serif 4), sober colours, hidden auteur / organisation / date metadata block (the invoice carries its own header). Import the profile from Réglages → ▾ → Importer…, switch to it, then Importer the.mdto start a new invoice.
markpage is glue around a lot of open-source work. Each piece below does something specific and does it well.
- CodeMirror 6 — the editor pane. Modular, fast, with the extension points we needed to hook the on-the-fly input ligatures and the click-back-to-source mapping.
- marked — Markdown parsing. We extend it with custom block tokens for admonitions, footnotes, definition lists and inference rules; everything still flows through the standard token API.
- paged.js — pagination engine. Turns the flowing preview into discrete pages with proper margins, page numbers and break-before/avoid behaviour. Both the on-screen paginated preview and the PDF go through it, so what you see is what you get.
- MathJax — LaTeX math rendering
for inline
$…$, displayed$$…$$, and the dedicated```inferenceblocks. - Mermaid — flowcharts, sequence, class, state, gantt, mindmap, pie diagrams. Rendered as SVG so the PDF stays vector-crisp.
- ebnf2railroad
and railroad-diagrams
— W3C EBNF → SVG railroad diagrams for the
```ebnffence, one diagram per production. - Charts (
line,bar) come from a small custom SVG generator inpackages/blocks/src/renderers/chart.ts— light enough that it wasn't worth pulling a full charting library.
- highlight.js — colourising for
fenced code blocks (a curated ~20-language subset bundled, plus
a custom Faust grammar shipped in
src/highlight-faust.ts). Theme: atom-one-light.
- Mammoth.js —
converts Word
.docxfiles to clean HTML on import, which we then run through Turndown to land as Markdown. - Turndown — HTML →
Markdown for the
.htmlimport path (and the second half of the.docxpipeline).
- JSZip — bundles the
.texsource with the referenced images / mermaid / chart SVGs into a single zip when the LaTeX export needs to ship resources.
- Roboto Condensed and Roboto Mono bundled via @fontsource so they work offline; the editor and the default PDF render on them. Plain Roboto is bundled too for the brand mark.
- Noto Sans Math and Noto Sans Symbols ride the fallback cascade for math glyphs and miscellaneous Unicode symbols.
- Every other Google Font in the catalogue is fetched on demand
via the standard
fonts.googleapis.comCSS endpoint when the user picks it in Settings.
- Vite for the dev server, the bundling and the static build that ships to GitHub Pages.
- TypeScript for the type
system — every module is fully typed, no
anyoutside thin boundary shims. - vitest + happy-dom
for the regression corpus: each
.mdtest case has pinned snapshots of its LaTeX and HTML output, so any rendering change is reviewed as a precise diff in pull requests.
A substantial part of the development happened in pair with
Claude Code, Anthropic's
agentic CLI for Claude. The architecture decisions, the regression
test harness, the i18n rework, most of the LaTeX export pipeline
and a large fraction of the diff-by-diff iteration were designed
and implemented through that workflow. The commit history carries
Co-Authored-By: Claude Opus 4.x trailers where appropriate.