@bongo/markdown is the Markdown loader plugin for bongo, powered by md4x.
Install
bun add @bongo/markdownUsage
import { markdown } from '@bongo/markdown';
export default {
plugins: [
markdown(),
],
};The plugin registers an .md loader. It converts Markdown files to HTML content files, and exposes the frontmatter as page metadata:
---
title: Hello
tags: docs, markdown
---
# HelloSee Content and Frontmatter.
Elements
A <bongo:name> element in Markdown prose calls the partial with that name. bongo leaves elements inside code blocks and inline code as written.
The element's attributes become the partial's arguments. bongo reads them as data, and never runs them as code:
<bongo:card title="Hello" count=3 draft=false tags=[js, css] meta={ author: 'Luke' } featured />- A quoted value,
"…",'…', or`…`, is a string. Backslash escapes work as in JavaScript.${…}is plain text. - A bare value is a number,
true,false,null, or else text, sosrc=img/a.pngis a string. […]is a list and{…}is a map, written like JSON5: keys need no quotes, and a trailing comma is fine.- A name with no value is
true.
Content between tags
An element can also wrap Markdown. The plugin renders the Markdown between the tags to HTML, and passes it to the partial as $slot:
<bongo:card title="Hello">
Some *Markdown* text.
</bongo:card>A .hbs partial prints it with {{{ $slot }}}. It does not need to declare $slot:
{{#expect title}}
<article class="card"><h3>{{ title }}</h3>{{{ $slot }}}</article>Built-in elements
bongo's own elements, <bongo:asset>, <bongo:image>, <bongo:style>, <bongo:script>, and <bongo:client>, pass through untouched, for bongo to replace. See Special elements.
Images
Images next to a post need no element. bongo gives  and <img src="hero.jpg"> a srcset and their dimensions. Use <bongo:image> for a <picture> with more formats, or for your own sizes. See Images.
<figure>
<bongo:image src="./hero.jpg" alt="…" sizes="(min-width: 800px) 720px, 100vw" />
<figcaption>…</figcaption>
</figure>Headings
The plugin gives each page its headings, in document order, as $page.headings. Each heading has a level, an id, its plain text, and its inner html, which keeps inline code and links. The headings are also in each entry of $pages, and in the $pages of data modules. A table of contents in a layout:
<ol>
{{#each $page.headings.filter(h => h.level === 2) as h}}
<li><a href="#{{ h.id }}">{{{ h.html }}}</a></li>
{{/each}}
</ol>The id is the anchor that headingIds: true gives the heading. Without that option, the page HTML has no anchors, so set headingIds: true when you link to headings. A line in a code block is not a heading. A partial in a heading is in html, but not in text or id.
Options
markdown() accepts md4x HTML options. For example, headingIds: true gives each heading an id that links can target. Syntax highlighting is optional:
import { markdown } from '@bongo/markdown';
import { shiki } from '@bongo/markdown/highlight';
export default {
plugins: [
markdown({
highlighter: shiki({
theme: 'github-dark',
langs: ['js', 'ts', 'css', 'bash'],
}),
}),
],
};Highlighters
Install the highlighter package that you want to use:
bun add shiki
bun add sugar-highThen import from @bongo/markdown/highlight:
import { shiki, sugarhigh } from '@bongo/markdown/highlight';Shiki
shiki(options) loads Shiki with the requested theme and languages. A fence in a language that is not in langs renders as escaped plain text in the same theme. Set unknown: 'throw' to fail the build instead. A fence with no language keeps the plain md4x block.
Sugar High
sugarhigh() loads Sugar High, version 2 or later. It highlights fenced code in a language that Sugar High knows, such as ts, py, or bash. Other code blocks stay plain. Sugar High colors tokens with --sh-* CSS variables, so add a theme to your styles.
Your own highlighter
A highlighter can also be your own function. md4x calls it once for each code block, with the code and the block details, such as lang. It must return synchronously: either the HTML for the whole block, <pre> included, or undefined to keep the plain block.
Twinkleplop
Twinkleplop writes CSS classes, not inline colors, so a theme stylesheet gives the colors. Install its markdown core, one package for each language that you use, and a theme:
bun add @twinkleplop/markdown-core @twinkleplop/typescript @twinkleplop/bash @twinkleplop/theme-githubThen give your languages to twinkleplop() from @bongo/markdown/twinkleplop:
import { markdown } from '@bongo/markdown';
import { twinkleplop } from '@bongo/markdown/twinkleplop';
import { language as bash } from '@twinkleplop/bash';
import { language as typescript } from '@twinkleplop/typescript';
export default {
plugins: [
markdown({
highlighter: twinkleplop({
languages: {
typescript: typescript(),
bash: bash(),
},
}),
}),
],
};Languages
languages maps a fence name to a highlighter, or to the name of another entry, such as js: 'typescript'. Common aliases resolve to the languages that you register: typescript also covers ts, mts, and cts fences, and bash covers sh, shell, and zsh. Your own entries take priority.
A fence in a language that is not in languages renders as escaped plain text. Set on_unknown_language: 'throw' to fail the build instead. A fence with no language keeps the md4x <pre><code> output, unless you set default_language.
Other options
The other options, such as default_language, line_numbers, and render, are the same as in the twinkleplop markdown plugins. Fences can mark lines with {1,3-4}, add a title with [app.ts], and turn on line numbers with ts:line-numbers.
Theme
Import the theme in your CSS by its package name. See Packages in @import.
@import "@twinkleplop/theme-github";
.twinkleplop {
background: var(--twp-background);
}<bongo:style src="app.css"></bongo:style>The theme has light and dark colors. A dark class on an ancestor, such as <html class="dark">, selects the dark colors. To use one set only, import @twinkleplop/theme-github/light or @twinkleplop/theme-github/dark.
Code blocks
fence wraps the HTML of each code block. Use it to add a title bar, a language label, or a copy button. It works with every highlighter, and without one:
import { markdown } from '@bongo/markdown';
import type { Fence } from '@bongo/markdown';
const escape = (text: string) => text.replace(/[&<>"]/g, char => `&#${char.charCodeAt(0)};`);
const fence: Fence = (html, block) => {
let title = escape(block.filename ?? block.lang);
return `<figure class="code"><figcaption>${title}</figcaption>${html}</figure>`;
};
export default {
plugins: [markdown({ fence })],
};htmlis the output of the highlighter. When there is no highlighter, or when the highlighter returnsundefined, such as for an unknown language, it is the md4x<pre><code>block with the code escaped.blockis the block that the highlighter gets, typed asFence.Block:lang(an empty string when the block has no language),filenamefrom[app.ts], andhighlightsfrom{1,3-4}. It also hascode, the text of the block, with its last line break.- Every code block goes through
fence: fences with a language, fences without one, and indented code blocks. - When you set
fence, the highlighter does not getfilename. Thus a highlighter that prints its own title, such as twinkleplop with[app.ts], does not add a second one. Print the title infence. langandfilenameare text from your Markdown, as you wrote it. Escape them before you put them in HTML.fenceruns during the render, so it must return a string, not a promise.