alternate universe where markdown lets you write marko components inline
Find a file
2026-02-18 02:46:06 -08:00
crates/markdown-it chore: fork markdown-it to fix a bug 2026-02-18 02:46:06 -08:00
examples chore: ok it might be usable now 2026-02-18 02:46:06 -08:00
lib fix: little cheecky outline bug 2026-02-18 02:46:06 -08:00
src fix: some more bugs 2026-02-18 02:46:06 -08:00
tests fix: some more bugs 2026-02-18 02:46:06 -08:00
.gitignore chore: more meows 2026-02-16 20:08:44 -08:00
AGENTS.md fix: parsing edge cases 2026-02-13 11:09:29 -08:00
ARCHITECTURE.md chore: fork markdown-it to fix a bug 2026-02-18 02:46:06 -08:00
Cargo.lock chore: fork markdown-it to fix a bug 2026-02-18 02:46:06 -08:00
Cargo.toml chore: fork markdown-it to fix a bug 2026-02-18 02:46:06 -08:00
LICENSE chore: jsr release candidate 2026-02-16 20:08:44 -08:00
publish.sh chore: some stuff silly 2026-02-18 02:46:06 -08:00
README.md fix: little cheecky outline bug 2026-02-18 02:46:06 -08:00
wasm.sh fix: some more bugs 2026-02-18 02:46:06 -08:00

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.

  • layoutImport which wraps the entire document in a component, which is given three attributes. (see typescript types on the plugin / transform function)
    • content: the rendered content.
    • module: the module namespace for the compiled Markodown file.
    • outline: an array of Heading objects.
  • 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
};