Skip to content

oselvar/malte

Repository files navigation

malte

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)

The problem

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.

The idea

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.

Marking elements

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.

Style transfer

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.

Repeating a region

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).

Install

pnpm add -D @oselvar/malte

Add 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.

Internationalization

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.

CLI

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
  • extract adds data-malte markers to a component's static prose and writes the matching Markdown, self-verifying with a round-trip.
  • check is CI-friendly: it fails loudly if counts or structure don't match.
  • --locale <L> selects the .L.md extension; --ext overrides (default .md).
pnpm malte check --dir src

How it compares

mdsvex 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.

Philosophy

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.

Credits

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.

License

MIT

About

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors