| 1 | # Markodown |
| 2 | |
| 3 | This 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 |
| 5 | components are really easy to write, it makes this a great tool for writing |
| 6 | interactive blog posts. Markdown is compiled directly into `.marko` syntax, |
| 7 | leveraging the existing ecosystem. |
| 8 | |
| 9 | Markodown is used in production for |
| 10 | [my blog posts on paperclover.net](https://paperclover.net), where I ported my |
| 11 | posts 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 | |
| 26 | Here'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. |
| 32 | meta: |
| 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 | |
| 43 | i'm a catgirl and i love to **meow**! <rainbow-text>meow meow meow!</> |
| 44 | |
| 45 | ```ts |
| 46 | function doMyFavoriteThing() { |
| 47 | return "meow! ".repeat(Math.floor(Math.random() * 1000)).trim(); |
| 48 | } |
| 49 | ``` |
| 50 | |
| 51 | ## photos of my favorite people |
| 52 | |
| 53 | i love using Marko components because they're extremely concise to write. |
| 54 | a 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 | |
| 70 | i love being alive. ${'<3'} from ${new Date().getFullYear()}. |
| 71 | |
| 72 | </blog-layout> |
| 73 | ```` |
| 74 | |
| 75 | ## Usage |
| 76 | |
| 77 | Markodown 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 |
| 83 | npm i @clo/markodown@npm:@paperclover/markodown |
| 84 | # or |
| 85 | npx jsr add @clo/markodown |
| 86 | ``` |
| 87 | |
| 88 | The compiler can be directly used from `transform`, and there are also plugins |
| 89 | for Rollup/Rolldown/Vite and esbuild. For example, configure Markodown with |
| 90 | Marko Run: |
| 91 | |
| 92 | ```ts |
| 93 | import marko from "@marko/run/vite"; |
| 94 | import markodown from "@clo/markodown"; |
| 95 | import { defineConfig } from "vite"; |
| 96 | |
| 97 | export 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 | |
| 112 | All Marko features are supported, such as [tag resolution], [attribute tags], |
| 113 | [class shorthands], and template expressions. This makes it so much easier to |
| 114 | add 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 | |
| 134 | You can use Markodown to write blogs and long documents, then extract a table of |
| 135 | contents. 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 | |
| 145 | Using the basic heading tags is awesome, because you can very easily customize |
| 146 | the generated permalinks for each heading, and still use Markdown within the |
| 147 | heading 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 | |
| 167 | All frontmatter fields are converted into exports. For example, a framework that |
| 168 | reads the `meta` export for Open Graph can be easily satisfied with frontmatter. |
| 169 | |
| 170 | ``` |
| 171 | --- |
| 172 | meta: |
| 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 | |
| 187 | Line, Block, and HTML comments work like they do in Marko/JavaScript. |
| 188 | |
| 189 | ``` |
| 190 | # My Blog |
| 191 | |
| 192 | Text that is complete. |
| 193 | |
| 194 | // ## An unfinished section of the blog |
| 195 | // |
| 196 | // TODO: we gotta finish it! |
| 197 | ``` |
| 198 | |
| 199 | ### Paragraph Detection |
| 200 | |
| 201 | Like Markdown, you can place content between components, but you can also place |
| 202 | inline markdown anywhere between tags. Effectively, this means that text gets |
| 203 | wrapped in `<p>` tags if there is a blank line above and below it. |
| 204 | |
| 205 | ``` |
| 206 | <div>not wrapped</div> |
| 207 | <div> |
| 208 | not wrapped either |
| 209 | </div> |
| 210 | |
| 211 | <div> |
| 212 | |
| 213 | this paragraph gets wrapped in a `<p>` tag! |
| 214 | |
| 215 | </div> |
| 216 | ``` |
| 217 | |
| 218 | ### Static Statements |
| 219 | |
| 220 | You can define module-level functions and variables, |
| 221 | [same as you can in Marko](https://markojs.com/docs/reference/language#statements). |
| 222 | |
| 223 | ``` |
| 224 | static function sort(items: string[]) { |
| 225 | while (!isSorted()) { |
| 226 | shuffle(items); |
| 227 | } |
| 228 | return items; |
| 229 | } |
| 230 | ``` |
| 231 | |
| 232 | Note 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 | |
| 242 | Though you can say import as long as it's not the first item. |
| 243 | ``` |
| 244 | |
| 245 | ## Config |
| 246 | |
| 247 | You can configure Markodown globally via arguments to the `transform` function. |
| 248 | |
| 249 | ### Frontmatter Layout Configuration |
| 250 | |
| 251 | If frontmatter defines a `layout` property, is acts as a component import that |
| 252 | wraps the page. (This can also be configured globally with the `layoutImport` |
| 253 | property to `transform`). |
| 254 | |
| 255 | ``` |
| 256 | --- |
| 257 | title: my amazing post |
| 258 | layout: ../layout.marko |
| 259 | --- |
| 260 | |
| 261 | ## my document |
| 262 | |
| 263 | yap yap |
| 264 | ``` |
| 265 | |
| 266 | In `layout.marko`, you can customize extensively how the document is formatted. |
| 267 | |
| 268 | ```marko |
| 269 | import { Heading } from "@clo/markodown"; |
| 270 | |
| 271 | export 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. |
| 295 | import CustomHeader from "./custom-header.marko"; |
| 296 | export const components = { |
| 297 | heading: CustomHeader, |
| 298 | // link, image, codeBlock, blockquote |
| 299 | }; |
| 300 | ``` |