GitHub

Markdown

Pulse includes a built-in markdown parser. Write content in .md files, load them with the md() helper, and render with the prose component. No external dependencies — zero browser bytes.

Basic usage

Import md from @invisibleloop/pulse/md and call it with a file path. It returns an async server fetcher — use it in server and optionally in meta:

import { md }    from '@invisibleloop/pulse/md'
import { prose } from '@invisibleloop/pulse/ui'

const page = md('content/about.md')

export default {
  route:  '/about',
  server: { page },
  view:   (state, { page }) => prose({ content: page.html }),
}

The fetcher returns { html, frontmatter }. The html is ready to pass directly to prose(). The frontmatter object contains the parsed key/value pairs from the --- block at the top of the file.

Frontmatter

Add a --- block at the top of any .md file to define metadata. Use the values in meta for SEO:

---
title: About Us
description: Learn about our company
date: 2024-01-15
---

# About Us

Welcome to our company…
const page = md('content/about.md')

export default {
  route: '/about',
  meta: {
    title:       async (ctx) => (await page(ctx)).frontmatter.title,
    description: async (ctx) => (await page(ctx)).frontmatter.description,
  },
  server: { page },
  view:   (state, { page }) => prose({ content: page.html }),
}
Calling page(ctx) in both meta and server reads the file only once per request — the result is cached on ctx._mdCache automatically.

Dynamic routes

Use :param placeholders in the path — they are resolved from ctx.params at request time:

import { md }    from '@invisibleloop/pulse/md'
import { prose } from '@invisibleloop/pulse/ui'

const post = md('content/blog/:slug.md')

export default {
  route: '/blog/:slug',
  meta: {
    title:       async (ctx) => (await post(ctx)).frontmatter.title,
    description: async (ctx) => (await post(ctx)).frontmatter.description,
  },
  server: { post },
  view:   (state, { post }) => `
    <main id="main-content">
      ${prose({ content: post.html })}
    </main>
  `,
  onViewError: (err) => `
    <main id="main-content">
      <p>Post not found.</p>
    </main>
  `,
}
If the file does not exist, the fetcher throws with { status: 404 }. Define onViewError on dynamic-route pages to render a friendly not-found message instead of a 500.

parseMd()

For cases where you already have a markdown string (from a database, API, or generated content), use parseMd directly instead of the file helper:

import { parseMd } from '@invisibleloop/pulse/md'
import { prose }   from '@invisibleloop/pulse/ui'

export default {
  route: '/post/:id',
  server: {
    post: async (ctx) => {
      const record = await db.posts.find(ctx.params.id)
      const { html, frontmatter } = parseMd(record.body)
      return { html, title: frontmatter.title ?? record.title }
    }
  },
  view: (state, { post }) => prose({ content: post.html }),
}

What the parser renders

MarkdownOutput
# Heading through ###### Heading<h1><h6> with auto id anchors
**bold**, *italic*, ~~strike~~<strong>, <em>, <del>
`inline code`<code>
Fenced code block with language tag<pre><code class="language-X"> with syntax highlighting
[text](url), ![alt](url)<a>, <img>
- item, 1. item<ul>, <ol> (nested lists supported)
> quote<blockquote>
GFM table (| col | col |)<table> with <thead> and <tbody>
--- (three or more)<hr>

Syntax highlighting

Fenced code blocks with a language tag are syntax-highlighted automatically — no client JS required. The highlighting runs on the server and emits <span class="tok-*"> elements. The colour tokens are defined in pulse-ui.css and respect the active theme.

```js
const greeting = 'Hello, world'
console.log(greeting)
```

Supported languages:

Tag(s)Highlighter
js, javascript, ts, typescript, jsx, tsxJavaScript / TypeScript
html, xml, svg, vue, svelteHTML / XML
css, scss, lessCSS
bash, sh, shell, zshBash / Shell
json, jsoncJSON
Any other tagPlain text (escaped, no colour tokens)

The prose component

The prose component is designed specifically for markdown output. It applies typographic styles to all descendant elements — headings, paragraphs, lists, blockquotes, code blocks, tables, images — without requiring any classes on the elements themselves:

import { prose } from '@invisibleloop/pulse/ui'

// Wrap the html from parseMd or md()
prose({ content: page.html })

// Optional size modifier
prose({ content: page.html, size: 'lg' })  // 'sm' | 'base' | 'lg'
The content slot is injected as raw HTML — only pass server-side content that you trust. Never pass user-submitted HTML directly; sanitise it first.

Using URL paths

Pass a URL object to resolve paths relative to the spec file rather than the process working directory:

const page = md(new URL('./content/about.md', import.meta.url))