stnd.buildSTANDARD MANUAL2026-07-15

@stnd/press

@stnd/press

Markdown → Beautiful HTML with classical typography.

The typography engine for Standard Garden inspired by 500 years of printing tradition.

Tip

Built on markdown-it for Cloudflare Workers and edge environments.


Markdown in, typeset HTML out — but “typeset” is doing real work here: smart quotes, real em dashes, orphan prevention, locale-aware spacing rules (French gets a thin space before :/;/!/?, because that’s the actual rule). This is what makes a Standard Garden note look like a printed book instead of a GitHub README.

Use it:

import { Press } from "@stnd/press";
const press = new Press("# Hello\n\nThis is **beautiful** text.");
await press.parse();
console.log(press.html);

Install

pnpm add @stnd/press

Quick Start

import { Press } from "@stnd/press";

const press = new Press("# Hello\n\nThis is **beautiful** text.");
await press.parse();

console.log(press.html); // Rendered HTML with proper typography

With Astro Content Collections

When using Astro’s content collections, frontmatter is already parsed. Pass it directly to skip redundant parsing:

const { service } = Astro.props;
const press = new Press(service.body, { frontmatter: service.data });
await press.parse();

What it does

  1. Pre-processing — ::syntax patterns, Obsidian comment stripping
  2. Module plugins — Custom ::patterns from your index.module.js (passed explicitly)
  3. Markdown rendering — CommonMark via markdown-it with callouts, footnotes, heading anchors, and Prism syntax highlighting
  4. Post-processing — Smart quotes, em dashes, ellipses, fractions, orphan prevention, locale-aware spacing
  5. Sanitization — DOMPurify (opt-in, off by default)

Options

const press = new Press(content, {
  locale: "fr", // Typography locale (en, fr, de, es, it)
  sanitize: false, // DOMPurify sanitization (default: false)
  renderHtmlAsRaw: false, // Render ```html blocks as raw HTML
  frontmatter: entry.data, // Pre-parsed frontmatter (skip re-parsing)
  plugins: [], // Module press plugins from index.module.js
  checkSyntax: true, // Warn about unprocessed ::patterns
  html: true, // Allow HTML in markdown
  breaks: true, // Convert \n to <br>
  linkify: true, // Auto-link URLs
});

Images

Image handling is one pipeline, driven entirely by config.press.images (read from the app’s module config). It is deliberately source-agnostic: Press processes the URLs it’s given and never assumes where one app’s bytes live — that assumption is the app’s to declare. (See the framework’s No Assumption Bleed rule and ADR-0001.)

Two orthogonal layers, each switched on by the presence of its data:

// app/modules/core/index.module.js
config: {
  press: {
    images: {
      // Layer A · Resolution — where do the bytes live?
      //   from + to set  → "local-assets": copy loose files referenced by
      //                     basename into `to`, rewrite their <img> src.
      //   neither set     → "passthrough": src is already final (/cdn/…, remote).
      //   exactly one set → fatal config error (stnd.build/manual/press-images-incomplete).
      from: "vault",
      to: "/assets/vault",

      // Layer B · Delivery — wrap the resolved URL for Cloudflare resizing.
      cfImage: { widths: [640, 960, 1280, 1920], quality: 85, format: "auto" },
    },
  },
}
App shape from/to cfImage Mode
File-vault site (loose image files) both optional local-assets + CF resize
Edge / database site (/cdn/…, remote URLs) omit optional passthrough + CF resize

Layer A’s local-assets strategy is Node-only (it copies files at build time); it is dynamically imported and SSR-gated, so it never reaches client or edge bundles. Passthrough apps load none of it.

API

Press Class

const press = new Press(content, options);
await press.parse();

press.html; // Rendered HTML (headings include id attributes)
press.css; // Injected CSS from plugins
press.head; // Injected <head> content from plugins
press.frontmatter; // Parsed (or pre-provided) frontmatter
press.title; // Extracted title
press.readingTime; // "3 mins"
press.wordCount; // 450
press.preview; // Plain-text excerpt
press.isProcessed; // true after parse()

Lightweight Helpers

Import from @stnd/press/helpers to avoid pulling in the rendering engine:

import {
  parseFrontmatter, // Parse YAML frontmatter from markdown
  extractTitleFromContent, // Extract title from headings or first line
  excerpt, // Generate plain-text excerpt (alias) — prefer `press.excerpt`
  getReadingTime, // Estimate reading time
  applyFrontmatterEnrichment, // Add computed metadata to frontmatter
  extractCssVariables, // Convert stnd_* keys to CSS custom properties
  generateOgImageUrl, // Build OG image URL from metadata
  shouldShowIndex, // Check if content needs a table of contents
  press, // small bridge object — use `press.excerpt(...)` as the preferred API
} from "@stnd/press/helpers";

Typography (direct access)

import {
  runPreProcessing,
  runPostProcessing,
  getRulesForLocale,
} from "@stnd/press";

// Or access configuration directly
import {
  typographyRules,
  orphanWords,
  fractionMap,
} from "@stnd/press/typography/config.js";

Heading Anchors

All headings in rendered HTML automatically receive id attributes via markdown-it-anchor. Fragment links like #my-heading work out of the box — no post-processing needed.

<!-- Input -->
## My Heading

<!-- Output -->
<h2 id="my-heading">My Heading</h2>

Typography

Press applies locale-aware typographic rules automatically:

Rule Example Result
Em dash --
Ellipsis
Smart quotes "hello" “hello”
Fractions 1/2 ½
Arrows ->
Orphan prevention a cat a + non-breaking space + cat
French spacing Bonjour ! Thin space before !

Supported Locales

  • English (en) — Curly quotes ""'', comma thousands 1,000
  • French (fr) — Guillemets *«»*, thin-space thousands 1 000
  • German (de) — Low quotes „", dot thousands 1.000
  • Spanish (es) — Guillemets *«»*, dot thousands 1.000
  • Italian (it) — Guillemets *«»*, dot thousands 1.000

::syntax Patterns

Press includes a built-in pattern system for layout directives in markdown:

<div class="hero">

This text becomes a hero block.

</div>

<div class="callout" data-callout="tip">
<div class="callout-title">
<span class="callout-icon">💡</span>
<span class="callout-title-inner">Tip</span>
</div>
<div class="callout-content">

This is a tip callout with an icon.

</div>
</div>


<div class="columns-2">
<div class="column">

Left column content.

</div>
<div class="column">

Right column content.

</div>
</div>


<aside class="note">This is an inline note.</aside>

See typography/SYNTAX_GUIDE.md for the full list of built-in patterns.

Syntax Highlighting

Prism.js with these languages out of the box:

bashyamljsonmarkdownjavascripttypescriptcss

Need more? Import them before creating a Press instance:

import "prismjs/components/prism-python.js";
import "prismjs/components/prism-ruby.js";
import { Press } from "@stnd/press";

Sanitization

Sanitization is off by default. Astro and most frameworks handle this already.

Enable it for user-generated content (e.g., Standard Garden’s paste feature):

const press = new Press(untrustedContent, { sanitize: true });

When enabled, DOMPurify strips dangerous tags/attributes while preserving semantic HTML, SVGs, <video><details>, and style attributes.

Relationship to other packages

Raw file (DOCX, PDF, etc.)
       ↓
  @stnd/ingest    →  "Here's clean Markdown"
       ↓
  @stnd/press     →  "Here's beautiful HTML"

@stnd/ingest handles messy input. @stnd/press handles beautiful output.

Notes / Observations

  • The ::pattern scanner also fires inside fenced code blocks — building a
    page that documents the syntax (the Standard::Syntax book) logs ⚠ Unprocessed pattern for the example in its code fence. Harmless, but the scanner should probably skip fenced/inline code.

Todo

  • [ ] Nothing tracked yet.

Changelog

0.7.0

  • Breaking: Renamed folios option → plugins (aligns with module architecture rename)
  • Breaking: Renamed all internal typography “folio” terminology → “plugin”
  • Renamed initializeTypographyFolios → initializeTypographyPlugins
  • Renamed registerTypographyFolio → registerTypographyPlugin
  • Renamed getMarkdownItFolios → getMarkdownItPlugins
  • Renamed getAllTypographyFolios → getAllTypographyPlugins
  • Renamed clearTypographyFolios → clearTypographyPlugins
  • Renamed markdownFolio export → markdownPlugin (in tags.js, wikilinks.js)
  • Updated all references from index.folio.js → index.module.js

0.6.0

  • Breaking: Removed document-converter.js → moved to @stnd/ingest
  • Breaking: Removed rehype-markdown/ plugins (use markdown-it pipeline instead)
  • Breaking: Sanitization now opt-in (sanitize: false by default)
  • Added frontmatter option to skip re-parsing
  • Added locale option (replaces lang)
  • Lazy-initialize typography plugins (no side effects on import)
  • Removed dead dependencies: cheeriomammothpdfjs-disthtml-to-md
  • Removed @stnd/utils dependency
  • Added <video><section><aside><nav>style to sanitization allowlist
  • Cleaner, more focused API