🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

MdSvex

Markdown support is opt-in. Install mdsvex and any rehype/remark plugins you want, then inject them through Mochi.serve({ markdown: ... }).

bun add mdsvex@^0.12 rehype-slug@^6

Mochi is tested against mdsvex ^0.12 and rehype-slug ^6.

With markdown configured, .md and .svx files compile through the pipeline and can be used anywhere a .svelte component is accepted, including as a Mochi.page() route target:

// src/index.ts
import { Mochi } from 'mochi-framework';
import { compile as mdsvexCompile } from 'mdsvex';
import rehypeSlug from 'rehype-slug';

await Mochi.serve({
  markdown: {
    compile: mdsvexCompile,
    rehypePlugins: [rehypeSlug],
  },
  routes: {
    '/about': Mochi.page('./src/about.md'),
  },
});

Markdown can embed Svelte syntax — a top-level <script> block, $props, and {expression} interpolation work as in a .svelte file:

<script>
  let { name = 'world' } = $props();
</script>

# Hello, {name}

This page was rendered at {new Date().toISOString()}.

The markdown config accepts a full plugin chain compatible with mdsvex’s rehypePlugins and remarkPlugins.

Syntax highlighting with Shiki or twinkleplop

Fenced code blocks pass through unchanged unless you supply markdown.highlight.highlighter. Install a highlighting engine (twinkleplop, Shiki, highlight.js, Prism) and build a highlighter with the framework’s createHighlighter factory. It adds the code-block wrapper, copy button, Svelte-brace escape, and memoization per (code, lang).

This site uses twinkleplop, which ships one package per language and highlights synchronously:

bun add @twinkleplop/typescript @twinkleplop/bash
// src/lib/highlightCode.ts
import { language as bash } from '@twinkleplop/bash';
import { language as typescript } from '@twinkleplop/typescript';
import { createHighlighter } from 'mochi-framework/highlight';

const grammars: Record<string, (code: string) => string> = {
  bash: bash(),
  typescript: typescript(),
};

export const highlightCode = createHighlighter((code, lang) => (grammars[lang] ?? grammars.typescript)(code));
// src/index.ts
import { highlightCode } from './lib/highlightCode';

markdown: {
  compile: mdsvexCompile,
  highlight: { highlighter: (code, lang) => highlightCode(code, lang) },
}

The cache holds 1000 snippets and evicts in insertion order. Tune it with cacheSize (0 disables memoization).

Any other engine drops in the same way, sync or async:

import hljs from 'highlight.js';
import { createHighlighter } from 'mochi-framework/highlight';

export const highlightCode = createHighlighter((code, lang) => hljs.highlight(code, { language: lang }).value);

Theming

twinkleplop emits token classes rather than inline colours, so the theme is a stylesheet you supply:

<pre class="twinkleplop"><code><span class="l"><span class="tok keyword">const</span> …</span></code></pre>

Import a ready-made one (import '@twinkleplop/theme-github'), or write the rules yourself against the token names the theme packages export from their /tokens subpath:

.twinkleplop .tok.keyword {
  color: #a7c9a8;
}
.twinkleplop .tok.string {
  color: #d5b982;
}

Engines that inline their own colours (Shiki, highlight.js themes) need no stylesheet of yours.

Islands in markdown

mochi:hydrate, mochi:hydrate:visible, mochi:defer, and mochi:defer:visible work on components instantiated inside a .md / .svx file. Import the component as a default import from the markdown’s top-level <script> block, then apply the directive on the tag:

<script>
  import Counter from './Counter.svelte';
</script>

<Counter mochi:hydrate count={3} />

Mapping slugs to markdown components

Since: 0.10.0 (not released yet): *.prerender.ts modules and moduleRef() are not in the published release yet.

A docs or blog section usually needs every .md file keyed by slug. Build that map in a prerendered module instead of generating a barrel file into your source tree:

// src/lib/docs.prerender.ts
import { moduleRef } from 'mochi-framework';
import type { Component } from 'svelte';
import { loadDocs } from './docs';

export const docs: Record<string, Component> = Object.fromEntries((await loadDocs()).map((d) => [d.slug, moduleRef<Component>(`../../docs/${d.filename}`)]));

Each moduleRef() becomes a real import of the .md file, so the markdown still compiles through the pipeline above.

See it in action

Live demos showing key concepts from this page