Two-way sync between Markdown and Svelte.
Keep your page copy in Markdown and your design in Svelte. malte injects
the Markdown into your components at build time, so the text you read and edit
lives in one clean, ordered file — while your layout, classes and styles stay
untouched.
Hero.svelte ← design & layout (you keep this)
Hero.md ← the words (edit these)
Svelte pages with rich layouts usually source their copy from one of:
- Inline, hardcoded in
.svelte— great until you want editorial control, and the prose ends up tangled in markup. - A CMS (Sanity, Contentful, …) — powerful, but now your copy lives in a different system entirely.
- i18n JSON files — every paragraph becomes a key, scattered among hundreds of unrelated entries (button labels, menus, errors…).
The last two share a problem: fragmentation. The paragraphs of a single page are spread across many places, in no particular order. You can't read the page as prose, which is exactly what you need to do to write good prose.
It's also painful for an LLM to copyedit. To understand the flow of a page it has to reassemble it from a JSON file and the markup — burning tokens and context just to find the words.
With malte, each component has a Markdown companion that reads top-to-bottom
like the page itself. A human or an agent edits the Markdown; the design never
moves.
You mark the elements whose content comes from Markdown, and the Markdown is
a sequence of blocks separated by ---. The Nth block fills the Nth marker — no
selectors, no ids, no special syntax.
Hero.svelte — your design, with placeholder text for previewing:
<section class="hero">
<h1 data-malte>Design-time heading</h1>
<p data-malte>Design-time tagline.</p>
</section>Hero.md — the real copy, in order:
Build content-driven Svelte sites
---
Edit the **Markdown**. Watch the component update.At build time malte renders each block and injects it into the matching
element. The <h1> and <p> get the real copy; everything else — classes,
structure, styles — is yours.
Two equivalent markers, matched positionally in document order:
<!-- A boolean attribute on any HTML element -->
<h1 data-malte>…</h1>
<!-- A comment before any node — works on components too,
where an attribute would be a TypeScript error -->
<!-- malte -->
<Card>…</Card>A marked element is never descended into — its whole inner content belongs to its
block. Mark a leaf (<h1>, <p>, <span>) for a single run of text; mark a
container (<section>, a card, a <ul>) to fill a richer structure.
When a block targets a container, malte renders the Markdown to HTML and
re-skins it onto your placeholder markup, copying class and style
recursively. A styled list template becomes as many styled items as the Markdown
has bullets — cycling through your <li> templates:
<ul data-malte class="cards">
<li class="card odd">…</li>
<li class="card even">…</li>
</ul>- First feature
- Second feature
- Third feature
- Fourth feature
- Fifth feature→ five <li>s with classes odd, even, odd, even, odd. Because the markup is
baked at build time, Svelte's scoped CSS applies to the generated content
just like hand-written markup. If the Markdown structure can't be mapped onto the
placeholder (e.g. a paragraph where a list is expected), that's a hard error.
Let the Markdown decide how many elements render. Mark a container with
data-malte-each and give it one child as the template — it repeats once per
block:
<div class="cards" data-malte-each>
<FactBox>placeholder</FactBox>
</div>First fact
---
Second fact
---
Third fact→ three <FactBox>es. The fixed markers each take one block and the repeatable
region takes the remainder, so the count can differ per locale (e.g. eight cards
in +page.en.md, six in +page.no.md) with no parity errors. The Markdown stays
ordinary ----separated blocks — no group dividers to corrupt.
A file has one repeatable region at most; for a second grid, give it its own component and Markdown companion (malte composes per component).
pnpm add -D @oselvar/malteAdd it to vite.config.ts before the Svelte plugin:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { malte } from '@oselvar/malte';
export default defineConfig({
plugins: [malte(), sveltekit()]
});Any Foo.svelte with a sibling Foo.md is now content-managed, with HMR:
edit the Markdown and the page reloads.
malte complements i18n libraries like
Paraglide and
sveltia-i18n by keeping localized prose in Markdown. (i18n libraries are
great for buttons, labels and menus — less so for paragraphs.)
Pass two or more locales and a runtimeLocale accessor; each component then
pairs with one companion per locale (Hero.en.md, Hero.no.md, …). Every locale
is baked behind an {#if} branch, so switching locale is reactive and fully
styled:
malte({
locales: ['en', 'no'],
baseLocale: 'en',
runtimeLocale: {
importStatement: "import { getLocale } from '$lib/i18n';",
expression: 'getLocale()'
}
});Using Paraglide? Point malte at your project and it wires up the locale list
and runtime for you:
malte({ paraglideProject: './project.inlang' });Companions for every locale must define the same number of blocks, so a translation can't silently drift out of sync.
malte extract <file|dir|glob> bootstrap a Markdown companion from a .svelte file
malte check [--dir src] validate that markers and blocks line up. No writes.
malte apply [--dir src] validate, then bake the content into .svelte
extractaddsdata-maltemarkers to a component's static prose and writes the matching Markdown, self-verifying with a round-trip.checkis CI-friendly: it fails loudly if counts or structure don't match.--locale <L>selects the.L.mdextension;--extoverrides (default.md).
pnpm malte check --dir srcmdsvex lets you use Svelte components inside
Markdown — ideal for blog posts. malte is the inverse: it keeps prose out of
your components in Markdown and injects it back, which suits the rich, bespoke
layouts of landing pages and marketing sites where mdsvex falls short.
Fail loudly. A marker with no block, a block with no marker, or a Markdown structure that doesn't fit the placeholder is a hard error with a non-zero exit code — friendly for CI and pre-commit hooks.
All of malte's code was written by Claude Code. It took a human to come up
with the idea, the UX, the DX, and the syntax — a.k.a. intuition.
MIT