# marknative: paginated Markdown output without a browser

> marknative is a TypeScript Markdown rendering engine that parses CommonMark and GFM into its own document model, lays it out, paginates it into fixed pages and paints each one with a native 2D canvas, so it can produce PNG and SVG documents on a server with no Chromium and no DOM. The trade is that you own the layout engine rather than borrowing a browser's.

**liyown/marknative** — A Markdown rendering engine that generates paginated PNG and SVG output — no browser, no Chromium, no DOM.

- Repository: https://github.com/liyown/marknative
- Website: https://liyown.github.io/marknative/
- Stars: 657 · Forks: 33
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/liyown-marknative

## The path that skips HTML, DOM and the browser

The README opens by naming the pipeline that most Markdown renderers follow, and then says marknative takes a different path. The usual route is Markdown to HTML to DOM and CSS to browser layout to screenshot. What marknative does instead is parse Markdown directly into a typed document model, run its own block and inline layout engine, paginate the result into fixed-size pages, and paint each page with a native 2D canvas API. The claimed consequence is that the output is deterministic, server-renderable and completely headless. That determinism is the interesting part for a production pipeline. Browser layout is a moving target across versions and platforms, so a screenshot taken today and one taken after a browser upgrade are not guaranteed to match, whereas a page break computed by your own layout engine is. The project is MIT licensed, the homepage is liyown.github.io/marknative/, and the last push to the repository was on 2026-04-04.

## What the engine supports, and where the rendering happens

The feature set is stated in one dense line: CommonMark, GFM with tables, task lists and strikethrough, syntax-highlighted code blocks through Shiki, and LaTeX math formulas through MathJax, all rendered server-side. That is a substantial scope for a from-scratch engine, and it is where the difficulty lies. A Markdown parser is a solved problem. Implementing text layout, inline box positioning, line breaking, table column sizing, code block styling with a syntax highlighter, and math glyph placement is not. The reason to attempt it is the elimination of the browser, and the reason to hesitate is that the layout rules in your own engine are the ones you now have to match to the output you want. The documentation site is split into a guide, a gallery and an API reference, and the gallery in the repository shows a full syntax fixture rendered across ten pages, which is a more honest preview of the engine's range than a single well-chosen example.

## Installing, and the native paint backend you inherit

The package installs with either package manager.

```bash
bun add marknative
# or
npm install marknative
```

The important detail is the peer dependency, which the README flags: marknative uses skia-canvas as its paint backend, and it ships prebuilt native binaries for macOS, Linux and Windows, so no additional setup is needed in most environments. Most environments is the operative phrase. The engine is written for Bun, the repository uses a bun.lock and bunfig.toml, and the root workspace scripts all call bun, including the build, typecheck, test and bench commands. The quick start writes its output with Bun.write, which is a Bun API rather than a Node one, so the Node path exists for the package itself but the example is written for the Bun runtime.

```ts
import { renderMarkdown } from 'marknative'

const pages = await renderMarkdown(`
# Hello, marknative

A native Markdown rendering engine that produces **paginated PNG pages**
without a browser.

- CommonMark + GFM support
- Deterministic layout and pagination
- PNG and SVG output
`)
```

Each page carries its output bytes and the resolved layout, with page.format equal to png and page.data a Buffer, and the example writes them out one file per page.

## One entry point, and the options that change the output shape

The API is one function, renderMarkdown(markdown, options?), which parses, lays out, paginates and paints, returning one entry per page.

```ts
function renderMarkdown(
  markdown: string,
  options?: {
    format?: 'png' | 'svg'                      // default: 'png'
    singlePage?: boolean                         // render into one image instead of paginating
    theme?: BuiltInThemeName | ThemeOverrides    // default: defaultTheme
    scale?: number                               // PNG pixel density multiplier — default: 2 (retina)
    painter?: Painter                            // override the paint backend
    codeHighlighting?: {
      theme?: string                             // Shiki theme — auto-detected from page background
    }
  },
): Promise<RenderPage[]>
```

The options are where most decisions live. singlePage skips pagination and renders one long image instead, which is what you want for a banner rather than a document. scale is a PNG pixel density multiplier with a default of 2 for retina, so a page sized in CSS-like pixels produces a denser bitmap. The codeHighlighting option sets the Shiki theme, and the comment says it is auto-detected from the page background, which is the kind of detail that only matters once you have implemented the theme system yourself. There is also a painter option to override the paint backend, which is the seam to use if you need a different canvas implementation.

The return type is a union, and the distinction between data and page matters.

```ts
type RenderPage =
  | { format: 'png'; data: Buffer;  page: PaintPage }
  | { format: 'svg'; data: string;  page: PaintPage }
```

Each entry carries the raw output and the fully resolved page layout, so you can inspect fragment positions without re-rendering, which is what makes the engine useful as a building block rather than only as an end-to-end renderer.

## Ten built-in themes and a merge-based override model

The theme system is the part of the API that decides what the output looks like, and there are ten built-in themes you pass by name.

```ts
// 'default' | 'github' | 'solarized' | 'sepia' | 'rose'
// 'dark' | 'nord' | 'dracula' | 'ocean' | 'forest'
const pages = await renderMarkdown(markdown, { theme: 'dark' })
```

Beyond names there are partial overrides, which are merged onto defaultTheme.

```ts
const pages = await renderMarkdown(markdown, {
  theme: {
    colors: { background: '#1e1e2e', text: '#cdd6f4' },
    page: { width: 800 },
  },
})
```

And there is mergeTheme for full control, which takes a built-in theme and applies changes to it.

```ts
import { mergeTheme, getBuiltInTheme } from 'marknative'

const myTheme = mergeTheme(getBuiltInTheme('nord'), {
  colors: { link: '#ff6b6b' },
})
```

The default theme also sets the page geometry, and the README prints it directly, a width of 1080 and a height of 1440 for a portrait card ratio, with margins of 80 top and bottom and 72 left and right. Font sizes, line heights, margins and block spacing are all defined in the same place. The gradient background support extends the same override model, with a backgroundGradient object carrying a type, an angle and a list of colour stops.

## A parse-only entry point for custom renderers

Not every consumer wants images. parseMarkdown(markdown) parses the source into marknative's internal document model without running layout or paint, and the README describes it as useful for inspecting document structure or building custom renderers.

```ts
function parseMarkdown(markdown: string): MarkdownDocument
```

That is a meaningful escape hatch. If you want a PDF, an EPUB, a different page size or a completely different visual treatment, you can take the parsed document and write your own layout and painting on top of it, reusing the parser and the syntax highlighting while replacing everything downstream. The cost is that the document model is internal, so it is not a stability guarantee of the same kind as the renderMarkdown signature, and an upgrade can change its shape. For a tool that renders to images only, this entry point is unnecessary.

## Where a hand-written layout engine is the wrong answer

Two cases point elsewhere. The first is a document that contains real HTML and CSS, an inline style block, a web font you load from a CDN, a chart drawn in a canvas tag, or a layout that depends on flexbox. marknative implements Markdown, not a browser, so anything that needs CSS semantics is out of scope, and the difference between a document model and the DOM is exactly the difference the project is built to avoid. The second is a case where the output has to be editable or searchable. A PNG page is a picture of a document, and the moment a user wants to select text, copy a code block or have a search index reach the content, the image pipeline is the wrong abstraction and a structured format such as HTML is the right one.

There is also the general cost of owning a layout engine. When a browser improves its line breaking or its handling of a particular script, you do not get it. The engine's output is stable, which is the point, but stability also means every typographic change is yours to make, and the README's own fixture spread across ten pages suggests the range of constructs the engine has to handle has been growing rather than shrinking. For a team that only needs to render a few simple documents, the browser path is less work.

## A monorepo with a rapid release sequence

The repository is a Bun workspace. The root package.json is a private workspace named marknative-workspace, with workspaces of packages/*, and every script delegates into a package: the build filters on marknative, the test script runs in packages/marknative, and there are separate commands for the docs package. The dependencies at the root are dev-only, @types/bun, bun-types and typescript at version 5. There is a separate packages/ directory alongside the workspace configuration, and the tree also carries a .codex/ directory, a CLAUDE.md, bunfig.toml, tsconfig.json and a LICENSE.

The release history is unusually compressed. All three recent releases land on the same day, 2026-04-04: v0.3.1, then v0.4.0 which brought the CLI, then v0.5.0 which brought the monorepo, a CLI skill and a documentation overhaul. The last push to the repository was on 2026-04-04 as well, so the project has been at v0.5.0 since. A version 0.x with three releases in a day and no activity since tells you the project is young and its API may still move, so pinning the version rather than tracking the latest is the safer choice for a project that puts rendered images into production.

## Conclusion

marknative fits a pipeline that has to turn Markdown into fixed-size images on a server, where installing Chromium is not an option or where reproducible page breaks matter, such as a documentation build, a slide generator or a social card service. It does not fit a project that needs arbitrary HTML and CSS in the source, since the engine implements Markdown rather than a browser's layout model, and it does not fit a team unwilling to depend on a native paint backend, because skia-canvas is a peer dependency. Before you adopt it, check the peer dependency situation on your target platform, and read the theme system, since the built-in themes and the override API are where the visual identity of the output is decided.

## FAQ

### What is marknative and how is it different from a browser-based renderer?

It is a native Markdown rendering engine that produces paginated PNG and SVG documents with no browser, no Chromium and no DOM. Instead of Markdown to HTML to DOM to browser layout to screenshot, it parses Markdown into a typed document model, runs its own block and inline layout engine, paginates into fixed-size pages and paints each page with a native 2D canvas API, so the output is deterministic and server-renderable.

### How do I install marknative and what does it need?

Install it with bun add marknative or npm install marknative. It uses skia-canvas as its paint backend, listed as a peer dependency, and skia-canvas ships prebuilt native binaries for macOS, Linux and Windows so no additional setup is needed in most environments.

### Which Markdown features does marknative support?

CommonMark, GFM with tables, task lists and strikethrough, syntax-highlighted code blocks through Shiki, and LaTeX math formulas through MathJax, all rendered server-side. The theme for the code highlighting can be set through the codeHighlighting option and is auto-detected from the page background by default.

### Can I change the page size and the output format in marknative?

Yes. The options include format for png or svg with png as the default, singlePage to render one image instead of paginating, and scale as a PNG pixel density multiplier that defaults to 2. The default theme sets the page to 1080 by 1440 with margins of 80 top and bottom and 72 left and right, and a partial theme override can change the page width.

### Can I use marknative without producing images?

Yes. parseMarkdown(markdown) parses the source into the internal document model without running layout or paint, which the README describes as useful for inspecting document structure or building custom renderers. The document model is internal, so a custom renderer built on it is coupled to that shape.

## Sources

- [License: MIT](https://github.com/liyown/marknative/blob/main/LICENSE)
- [liyown/marknative on GitHub](https://github.com/liyown/marknative)
- [Project website](https://liyown.github.io/marknative/)
- [README](https://github.com/liyown/marknative/blob/main/README.md)
- [Releases](https://github.com/liyown/marknative/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/liyown-marknative
