A static site generator built on Rolldown and LightningCSS.
Published as @lukeed/bongo, but projects should install and reference it through the bongo alias:
bun add bongo@npm:@lukeed/bongo
All examples below use bongo.
bongo processes content files through a loader pipeline, applies layouts and partials with a tempura-based template engine, and bundles client assets with Rolldown for JavaScript and LightningCSS for CSS.
bongo build
bongo dev
CLI options:
bongo build -C site --minify
bongo dev -C site --host --port 3000
When you run bongo build:
bongo.config.{ts,mjs,cjs,js} by walking upward from cwd. Bun and Deno import TypeScript config files natively. Node bundles the config through Rolldown to a temp .mjs and dynamic-imports it.layouts/ and partials/. Handlebars files (.hbs) are compiled with tempura. JavaScript and TypeScript partials are loaded as modules.<bongo:style>, <bongo:script>, and <bongo:client> elements. Assets are registered and replaced with placeholder tokens.public/.import { markdown } from '@bongo/markdown';
import { shiki } from '@bongo/markdown/highlight';
export default {
$site: {
title: 'My Site',
url: 'https://example.com/',
lang: 'en',
},
$utils: {
excerpt(text: string, len: number) {
return text.slice(0, len);
},
},
plugins: [
markdown({
highlighter: shiki({
theme: 'github-dark',
langs: ['js', 'ts', 'css', 'bash'],
}),
}),
],
};
Config fields:
$site — site metadata; available in templates as {{$site.title}}. The url field also drives publicPath.$assets — loader map keyed by file extension.$utils — helper functions available in templates.$partials — programmatic partial functions.plugins — array of partial config objects, merged in order.Loaders transform content files by file extension. Register them with $assets:
export const $assets = {
md(filename, content, utils) {
return {
filename: filename + '.html',
content: renderedHtml,
metadata: { title: '...' },
};
},
};
Note: You can use
@bongo/markdownfor a ready-made solution.
Loaders are chained by extension. A .md file produces .md.html, the .html extension stops the chain and enters the template pipeline.
Loader utilities:
utils.decode(uint8array) — convert a buffer to a string.utils.render(partialName, args) — invoke a registered partial.utils.frontmatter(content) — parse YAML-like frontmatter delimited by ---.The publicPath prefix applied to external asset URLs is derived from $site.url:
https://example.com) -> publicPath = /https://example.com/blog/) -> publicPath = /blog/No manual configuration is required.
Templates use tempura syntax. Asset entrypoints are declared with <bongo:style>, <bongo:script>, and <bongo:client> custom elements.
{{! layouts/html.hbs }}
<!doctype html>
<html lang="{{$site.lang}}">
<head>
<meta charset="utf-8"/>
<title>{{ $page.title }}</title>
{{! External CSS: emits <link rel="stylesheet" href="/style.hash.css"/> }}
<bongo:style src="app.css"></bongo:style>
{{! Inline CSS: splices built file contents into a <style> block }}
<bongo:style src="print.css" inline></bongo:style>
</head>
<body>
{{{ $page.content }}}
{{! External JS: emits <script type="module" src="/main.hash.js"></script> }}
<bongo:script src="main.ts"></bongo:script>
{{! Async external JS }}
<bongo:script src="analytics.ts" async></bongo:script>
{{! Inline JS: file contents spliced into the <script> element }}
<bongo:script src="boot.ts" inline></bongo:script>
</body>
</html>
<bongo:style> attributes:
src="..." — path relative to assets/.inline — splice built CSS directly into a <style> block. Omit for an external stylesheet link.global — disable CSS modules auto-detection on *.module.css sources.<bongo:script> attributes:
src="..." — path relative to assets/.inline — splice built JavaScript directly into a <script> block.async — add the async attribute to an external script.defer — add the defer attribute for CJS output. ESM modules are deferred by the browser.format="esm"|"cjs" — output module format. Defaults to esm.target="..." — browserslist target string.<bongo:client> bundles bongo's browser runtime and emits a module script that calls client(...), plus the hidden aria-live region used to announce page-title changes:
<bongo:client />
<bongo:client transitions />
<bongo:client transitions="slide" />
Multiple <bongo:client> tags on a page share one bundled runtime.
Add transition:name and transition:animate to HTML elements in a layout or partial. bongo rewrites them at build time.
<header transition:name="site-header">...</header>
<nav transition:name="nav" transition:animate="slide">...</nav>
transition:name="NAME" — removed from the element; view-transition-name: NAME is merged into the element's style attribute.transition:animate="KEYFRAME" — requires a named @keyframes KEYFRAME in your CSS. bongo injects a <style data-bongo-transitions> block in <head> that wires the keyframe to matching view-transition pseudo-elements.If transition:animate is present without transition:name, an automatic name is generated.
{{#partial src="header" }}
{{#partial src="card" title="Hello" }}
$site — global site config.$page — current page. Includes path, url, title, summary, params, section, tags, and content.$pages — every page in the build, sorted by path.$section — pages scoped to the current page's section directory.$tags — array of { tag, pages } entries.$assets — configured loader map.$utils — configured helper functions.Content files ending in .hbs are tempura-compiled with the same context as layouts: $site, $page, $pages, $section, $tags, $assets, $utils, plus params as an alias for $page.params.
The current page's own $page.content is empty while its body is compiling. It is the value produced by that compile.
All CSS is handled by LightningCSS: @import bundling, autoprefixing, nesting, and optional minification.
.module.css files automatically enable CSS modules. Non-module files can opt in explicitly when registering the entry.
bongo dev starts the HTTP server and runs the initial build behind it. File changes in assets/, layouts/, partials/, or content/ trigger a rebuild.
Rebuilds push one of these SSE messages to the browser:
| Message | Meaning |
|---|---|
{ type: 'reload' } | Successful rebuild; browser reloads the page. |
{ type: 'error', errors } | Build failed; full-viewport error overlay mounts. |
{ type: 'error-clear' } | Next successful build; overlay unmounts, then the browser reloads. |
The hmr message type exists in the type union for forward compatibility. Today, file changes trigger a full reload.
bongo/client turns a statically generated site into a client-navigated SPA.
import { client, navigate, on, off } from 'bongo/client';
The simplest setup is the <bongo:client /> element:
{{! layouts/html.hbs }}
<bongo:client transitions />
That expands at build time to:
<div id="bongo:live" role="status" aria-live="polite" style="...visually-hidden..."></div>
<script type="module">
import { client } from "/client.D59fAaym.js";
client({ transitions: true });
</script>
The runtime updates #bongo:live with the new page title on navigation. If the element is missing, announcements are skipped silently.
If you also need custom event handlers or boot code, keep a separate <bongo:script>:
<bongo:client transitions />
<bongo:script src="main.ts"></bongo:script>
import { on } from 'bongo/client';
on('bongo:navigated', ({ to }) => {
console.log('navigated to', to.pathname);
});
Or call client() yourself:
import { client } from 'bongo/client';
client({ transitions: true });
| Option | Type | Default | Description |
|---|---|---|---|
transitions | boolean | 'fade' | 'initial' | 'slide' | string | false | View-transition preset. true / 'fade' injects a cross-fade keyframe. 'slide' injects a slide keyframe. 'initial' wraps the swap without injecting CSS. Custom string wraps only; user CSS provides the rules. Any falsy value disables transitions. Opt a link out with transition:disable. |
prefetch | boolean | 'hover' | 'tap' | false | Prefetch same-origin links into the in-memory HTML cache. true / 'hover' fires after hover settles, or immediately on touch. 'tap' fires on mousedown / touchstart. Opt a link out with prefetch:disable. |
Subscribe with on(event, handler). It returns an unsubscribe function. Remove a specific handler with off(event, handler).
on('bongo:navigated', ({ to }) => {
console.log('navigated to', to.pathname);
});
| Event | Detail shape | Fires when |
|---|---|---|
bongo:ready | { url: URL } | Once, immediately after client() is called. |
bongo:navigate | { to: URL; from: URL } | Navigation intercepted, before the fetch. |
bongo:replace | { to: URL; from: URL; doc: Document } | HTML fetched and parsed; swap has not happened yet. |
bongo:replaced | { to: URL; from: URL } | DOM swapped. |
bongo:navigated | { to: URL; from: URL } | Navigation settled. |
bongo:error | { to: URL; error: Error } | Navigation fetch or swap threw. |
navigate('/about');
navigate('/old-url', { replace: true });
navigate() returns Promise<void> and resolves when navigation and any transition complete.
Force a full-page load:
<a href="/legacy" transition:disable>Legacy dashboard</a>
Disable prefetch without forcing a full-page load:
<a href="/logout" prefetch:disable>Log out</a>
client() sets history.scrollRestoration = 'manual'.
{x, y} position for that URL.(0, 0).The Navigation API is available in Chrome and Edge, and is behind a flag or unsupported in Firefox and Safari at time of writing. If the API is absent, client() emits bongo:ready and returns without installing navigation interception.
bongo ships ambient JSX types for its custom elements and attributes. Extend the bundled config:
{
"extends": "bongo/tsconfig",
"compilerOptions": {
// your own settings
}
}
The augmentation adds:
<bongo:client>, <bongo:script>, and <bongo:style> intrinsic elements.transition:name and transition:animate on every HTML element.transition:disable and prefetch:disable on anchors.Or reference the types directly:
/// <reference types="bongo/jsx" />