| crates/markdown-it | ||
| examples | ||
| lib | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| publish.sh | ||
| README.md | ||
| wasm.sh | ||
Markodown
This is a weird markup language that combines features of Markdown and
Marko. You can think of this as an alternative universe to MDX. Since Marko
components are really easy to write, it makes this a great tool for writing
interactive blog posts. Markdown is compiled directly into .marko syntax,
leveraging the existing ecosystem.
Markodown is used in production for my blog posts on paperclover.net, where I ported my posts from MDX to it.
Here's a glance at how things look. Complete example documents in examples.
---
// in `clover` static site generator, `export const meta` powers meta and
// open graph tags. frontmatter becomes exports and in-scope constants.
meta:
title: some interesting blog post
description: i could be really interesting
embed:
image: /something/fire.png
---
// this component will look for `blog-layout.marko`
<blog-layout title=meta.title description=meta.description>
# ${meta.title}
i'm a catgirl and i love to **meow**! <rainbow-text>meow meow meow!</>
```ts
function doMyFavoriteThing() {
return "meow! ".repeat(Math.floor(Math.random() * 1000)).trim();
}
```
## photos of my favorite people
i love using Marko components because they're extremely concise to write.
a lot less brace hell for non-string attributes, and more treats!
<photo-grid
base="/friends"
cols=[40, 30, 30]
rows=[300, 400, 200]
>
<@img src="IMG_4831.jpeg" />
<@img src="IMG_4838.jpeg" w=2 />
<@img src="IMG_4839.jpeg" w=2 align="top" />
<@img src="IMG_4833.jpeg" h=2 />
<@img src="IMG_4832.jpeg" w=2 />
</>
## in conclusion
i love being alive. ${'<3'} from ${new Date().getFullYear()}.
</blog-layout>
Usage
Markodown is distributed on NPM and JSR. The compiler runs anywhere JS+WASM runs.
# alias install
npm i @clo/markodown@npm:@paperclover/markodown
# or
npx jsr add @clo/markodown
The compiler can be directly used from transform, and there are also plugins
for Rollup/Rolldown/Vite and esbuild. For example, configure Markodown with
Marko Run:
import marko from "@marko/run/vite";
import markodown from "@clo/markodown";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
marko(),
markodown({
// optionally wrap all markdown files in a layout, this component is
// given a list of headers to construct a table of contents.
layoutImport: "../tags/markdown-layout.marko",
// ...more customization options are well-documented in the types.
}),
],
});
Components
All Marko features are supported, such as tag resolution, attribute tags, class shorthands, and template expressions. This makes it so much easier to add complex content to your pages.
## cool video
<clover-video src="/2025/in the summer/in the summer.mp4">
<@header>**music video**: in the summer</>
</clover-video>
<footer.copyright-info>
made with love... (c) ${new Date().getFullYear()}
</footer>
Outline / Table of Contents
You can use Markodown to write blogs and long documents, then extract a table of contents. This is done with two mechanisms.
layoutImportwhich wraps the entire document in a component, which is given three attributes. (see typescript types on the plugin /transformfunction)content: the rendered content.module: the module namespace for the compiled Markodown file.outline: an array ofHeadingobjects.
componentImports, which can let you customize the rendering of the headers themselves.
Using the basic heading tags is awesome, because you can very easily customize the generated permalinks for each heading, and still use Markdown within the heading titles.
# my blog post
<h2#markdown>about `markdown`</>
...
<h2#marko>about `marko`</>
...
<h3#marko-extras>some extra details</>
...
Frontmatter
All frontmatter fields are converted into exports. For example, a framework that
reads the meta export for Open Graph can be easily satisfied with frontmatter.
---
meta:
title: I Love Modular Software
description: a very cute little post by me
author: clover caruso
embed:
thumbnail: /file/blog.png
---
// And since `export const` puts the value in scope, this works too:
# ${meta.title}
Comments
Line, Block, and HTML comments work like they do in Marko/JavaScript.
# My Blog
Text that is complete.
// ## An unfinished section of the blog
//
// TODO: we gotta finish it!
Paragraph Detection
Like Markdown, you can place content between components, but you can also place
inline markdown anywhere between tags. Effectively, this means that text gets
wrapped in <p> tags if there is a blank line above and below it.
<div>not wrapped</div>
<div>
not wrapped either
</div>
<div>
this paragraph gets wrapped in a `<p>` tag!
</div>
Static Statements
You can define module-level functions and variables, same as you can in Marko.
static function sort(items: string[]) {
while (!isSorted()) {
shuffle(items);
}
return items;
}
Note that this means writing a paragraph starting with the lowercase words
static, export, import, server, and client all must be escaped.
# All About RSC
\server components are a bad idea. (markdown backslash)
${"server"} components are a bad idea. (template literal)
Though you can say import as long as it's not the first item.
Config
You can configure Markodown globally via arguments to the transform function.
Frontmatter Layout Configuration
If frontmatter defines a layout property, is acts as a component import that
wraps the page. (This can also be configured globally with the layoutImport
property to transform).
---
title: my amazing post
layout: ../layout.marko
---
## my document
yap yap
In layout.marko, you can customize extensively how the document is formatted.
import { Heading } from "@clo/markodown";
export interface Input {
content: Marko.Body;
// Markdown scans for headings (h1..h6)
outline: Heading[];
// This is the namespace import of the main document.
// You can reflect frontmatter, or do whatever with this.
module: Record<string, unknown>;
}
<main>
<h1>${input.module.title ?? "Blog Post"}</h1>
<aside>
<ul>
<for|heading| of=input.outline>
// heading content includes formatting, even custom tags.
<li><a href=`#${heading.id}`><${heading.content}/></a></li>
</for>
</ul>
</aside>
<${input.content} />
</main>
// Additionally, built-in components can be altered.
import CustomHeader from "./custom-header.marko";
export const components = {
heading: CustomHeader,
// link, image, codeBlock, blockquote
};