1# Markodown
2
3This is a weird markup language that combines features of [Markdown] and
4[Marko]. You can think of this as an alternative universe to MDX. Since Marko
5components are really easy to write, it makes this a great tool for writing
6interactive blog posts. Markdown is compiled directly into `.marko` syntax,
7leveraging the existing ecosystem.
8
9Markodown is used in production for
10[my blog posts on paperclover.net](https://paperclover.net), where I ported my
11posts from MDX to it.
12
13[Markdown]: https://en.wikipedia.org/wiki/Markdown
14[Marko]: https://markojs.com/
15
16> - [Usage](#usage)
17> - [Components](#components)
18> - [Outline / Table of Contents](#outline-table-of-contents)
19> - [Frontmatter](#frontmatter)
20> - [Comments](#comments)
21> - [Paragraph Detection](#paragraph-detection)
22> - [Static Statements](#static-statements)
23> - [Config](#config)
24> - [Frontmatter Layout Configuration](#frontmatter-layout-configuration)
25
26Here's a glance at how things look. Complete example documents in `examples`.
27
28````
29---
30// in `clover` static site generator, `export const meta` powers meta and
31// open graph tags. frontmatter becomes exports and in-scope constants.
32meta:
33 title: some interesting blog post
34 description: i could be really interesting
35 embed:
36 image: /something/fire.png
37---
38// this component will look for `blog-layout.marko`
39<blog-layout title=meta.title description=meta.description>
40
41# ${meta.title}
42
43i'm a catgirl and i love to **meow**! <rainbow-text>meow meow meow!</>
44
45```ts
46function doMyFavoriteThing() {
47 return "meow! ".repeat(Math.floor(Math.random() * 1000)).trim();
48}
49```
50
51## photos of my favorite people
52
53i love using Marko components because they're extremely concise to write.
54a lot less brace hell for non-string attributes, and more treats!
55
56<photo-grid
57 base="/friends"
58 cols=[40, 30, 30]
59 rows=[300, 400, 200]
60>
61 <@img src="IMG_4831.jpeg" />
62 <@img src="IMG_4838.jpeg" w=2 />
63 <@img src="IMG_4839.jpeg" w=2 align="top" />
64 <@img src="IMG_4833.jpeg" h=2 />
65 <@img src="IMG_4832.jpeg" w=2 />
66</>
67
68## in conclusion
69
70i love being alive. ${'<3'} from ${new Date().getFullYear()}.
71
72</blog-layout>
73````
74
75## Usage
76
77Markodown is distributed on
78[NPM](https://npmjs.com/package/@paperclover/markodown) and
79[JSR](https://jsr.io/@clo/markodown). The compiler runs anywhere JS+WASM runs.
80
81```sh
82# alias install
83npm i @clo/markodown@npm:@paperclover/markodown
84# or
85npx jsr add @clo/markodown
86```
87
88The compiler can be directly used from `transform`, and there are also plugins
89for Rollup/Rolldown/Vite and esbuild. For example, configure Markodown with
90Marko Run:
91
92```ts
93import marko from "@marko/run/vite";
94import markodown from "@clo/markodown";
95import { defineConfig } from "vite";
96
97export default defineConfig({
98 plugins: [
99 marko(),
100 markodown({
101 // optionally wrap all markdown files in a layout, this component is
102 // given a list of headers to construct a table of contents.
103 layoutImport: "../tags/markdown-layout.marko",
104 // ...more customization options are well-documented in the types.
105 }),
106 ],
107});
108```
109
110### Components
111
112All Marko features are supported, such as [tag resolution], [attribute tags],
113[class shorthands], and template expressions. This makes it so much easier to
114add complex content to your pages.
115
116```
117## cool video
118
119<clover-video src="/2025/in the summer/in the summer.mp4">
120 <@header>**music video**: in the summer</>
121</clover-video>
122
123<footer.copyright-info>
124 made with love... (c) ${new Date().getFullYear()}
125</footer>
126```
127
128[tag resolution]: https://markojs.com/docs/reference/custom-tag#relative-custom-tags
129[attribute tags]: https://markojs.com/docs/reference/language#attribute-tags
130[class shorthands]: https://markojs.com/docs/reference/language#shorthand-class-and-id
131
132### Outline / Table of Contents
133
134You can use Markodown to write blogs and long documents, then extract a table of
135contents. This is done with two mechanisms.
136
137- `layoutImport` which wraps the entire document in a component, which is given
138 three attributes. (see typescript types on the plugin / `transform` function)
139 - `content`: the rendered content.
140 - `module`: the module namespace for the compiled Markodown file.
141 - `outline`: an array of `Heading` objects.
142- `componentImports`, which can let you customize the rendering of the headers
143 themselves.
144
145Using the basic heading tags is awesome, because you can very easily customize
146the generated permalinks for each heading, and still use Markdown within the
147heading titles.
148
149```
150# my blog post
151
152<h2#markdown>about `markdown`</>
153
154...
155
156<h2#marko>about `marko`</>
157
158...
159
160<h3#marko-extras>some extra details</>
161
162...
163```
164
165### Frontmatter
166
167All frontmatter fields are converted into exports. For example, a framework that
168reads the `meta` export for Open Graph can be easily satisfied with frontmatter.
169
170```
171---
172meta:
173 title: I Love Modular Software
174 description: a very cute little post by me
175 author: clover caruso
176 embed:
177 thumbnail: /file/blog.png
178---
179
180// And since `export const` puts the value in scope, this works too:
181
182# ${meta.title}
183```
184
185### Comments
186
187Line, Block, and HTML comments work like they do in Marko/JavaScript.
188
189```
190# My Blog
191
192Text that is complete.
193
194// ## An unfinished section of the blog
195//
196// TODO: we gotta finish it!
197```
198
199### Paragraph Detection
200
201Like Markdown, you can place content between components, but you can also place
202inline markdown anywhere between tags. Effectively, this means that text gets
203wrapped in `<p>` tags if there is a blank line above and below it.
204
205```
206<div>not wrapped</div>
207<div>
208not wrapped either
209</div>
210
211<div>
212
213this paragraph gets wrapped in a `<p>` tag!
214
215</div>
216```
217
218### Static Statements
219
220You can define module-level functions and variables,
221[same as you can in Marko](https://markojs.com/docs/reference/language#statements).
222
223```
224static function sort(items: string[]) {
225 while (!isSorted()) {
226 shuffle(items);
227 }
228 return items;
229}
230```
231
232Note that this means writing a paragraph starting with the lowercase words
233`static`, `export`, `import`, `server`, and `client` all must be escaped.
234
235```
236# All About RSC
237
238\server components are a bad idea. (markdown backslash)
239
240${"server"} components are a bad idea. (template literal)
241
242Though you can say import as long as it's not the first item.
243```
244
245## Config
246
247You can configure Markodown globally via arguments to the `transform` function.
248
249### Frontmatter Layout Configuration
250
251If frontmatter defines a `layout` property, is acts as a component import that
252wraps the page. (This can also be configured globally with the `layoutImport`
253property to `transform`).
254
255```
256---
257title: my amazing post
258layout: ../layout.marko
259---
260
261## my document
262
263yap yap
264```
265
266In `layout.marko`, you can customize extensively how the document is formatted.
267
268```marko
269import { Heading } from "@clo/markodown";
270
271export interface Input {
272 content: Marko.Body;
273 // Markdown scans for headings (h1..h6)
274 outline: Heading[];
275 // This is the namespace import of the main document.
276 // You can reflect frontmatter, or do whatever with this.
277 module: Record<string, unknown>;
278}
279
280<main>
281<h1>${input.module.title ?? "Blog Post"}</h1>
282<aside>
283 <ul>
284 <for|heading| of=input.outline>
285 // heading content includes formatting, even custom tags.
286 <li><a href=`#${heading.id}`><${heading.content}/></a></li>
287 </for>
288 </ul>
289</aside>
290
291<${input.content} />
292</main>
293
294// Additionally, built-in components can be altered.
295import CustomHeader from "./custom-header.marko";
296export const components = {
297 heading: CustomHeader,
298 // link, image, codeBlock, blockquote
299};
300```