July 17, 2024
pomber
Rodrigo Pombo
@pomber

Fine-grained Markdown

Flexible content, richer presentation

Markdown is great. Easy to write, easy to read, and covers most use cases for content websites.

But sometimes it feels a bit limited. Sometimes your content is not so linear, it has more structure, and you want to present it in a way that reflects that structure. You want a richer medium to present your content.

Let me show you what I mean, and how Code Hike can help you solve this problem.


Rendering markdown using React usually looks something like this:

content.md
# Hello
Lorem ipsum dolor sit
## Step one
Amet consectetur adipiscing
## Step two
Elit sed do eiusmod
## Step three
Tempor incididunt ut labore
page.jsx
import Content from "./content.md"
export function Page() {
return (
<div className="...">
<SideBar />
<Content />
</div>
)
}
sidebar
<Content/>

We get the content as a component. We can render the rest of the page any way we want, but the content itself is a single rigid block.

Frontmatter

We can convert part of the content into metadata using frontmatter.

content.md
---
title: Hello
---
Lorem ipsum dolor sit
## Step one
Amet consectetur adipiscing
## Step two
Elit sed do eiusmod
## Step three
page.jsx
import {
Content,
frontmatter,
} from "./content.md"
const { title } = frontmatter
export function Page() {
return (
<div>
<SideBar />
<main>
<h1>{title}</h1>
<Content />
</main>
</div>
)
}
sidebar
<h1/>
<Content/>

This is a good start, it gives us some flexibility, but the main content is still a single block of markdown.

If we want more flexibility, we need a way to break the content into smaller pieces.

Code Hike Blocks

To make the content flexible, Code Hike introduces the concept of blocks.

content.md
## !intro Hello
Lorem ipsum dolor sit
## !!steps Step one
Amet consectetur adipiscing
## !!steps Step two
Elit sed do eiusmod
## !!steps Step three
Tempor incididunt ut labore
page.jsx
import Content from "./content.md"
import { parse } from "codehike"
const { intro, steps } = parse(Content)
export function Page() {
return (
<div>
<SideBar />
<main>
{intro.children}
{steps.map((step) => (
<div>
<h2>{step.title}</h2>
{step.children}
</div>
))}
</main>
</div>
)
}
sidebar
intro
step one
step two
step three

We define blocks by adding a special decoration to markdown headings.

Blocks can now be destructured from the content, giving us fine-grained control to render them any way we want.

We can go even further and decorate paragraphs, images, and codeblocks.

For example, we can add a cover image to each step.

content.md
## !intro Hello
Lorem ipsum dolor sit
## !!steps Step one
![!cover](/one.png)
Amet consectetur adipiscing
## !!steps Step two
![!cover](/two.png)
## !!steps Step three
page.jsx
import Content from "./content.md"
import { parse } from "codehike"
const { intro, steps } = parse(Content)
export function Page() {
return (
<div>
<SideBar />
<main>
{intro.children}
{steps.map((step) => (
<div>
<img src={step.cover} />
<h2>{step.title}</h2>
{step.children}
</div>
))}
</main>
</div>
)
}
sidebar
intro
<img>
step one
<img>
step two
<img>
step three

Content Schema

We can define a schema so we get autocompletion and all the benefits of typescript in the editor.

content.md
## !intro Hello
Lorem ipsum dolor sit
## !!steps Step one
![!cover](/one.png)
Amet consectetur adipiscing
## !!steps Step two
![!cover](/two.png)
## !!steps Step three
page.jsx
import Content from "./content.md"
import {
parse,
Block,
ImageBlock,
} from "codehike"
import { z } from "zod"
const Schema = Block.extend({
intro: Block,
steps: z.array(
Block.extend({ cover: ImageBlock }),
),
})
const { intro, steps } = parse(
Content,
Schema,
)
export function Page() {
return <div>...</div>
}
sidebar
intro
<img>
step one
<img>
step two
<img>
step three

Type-safe Markdown

Adding a schema also means that now we validate that the content follows the structure we defined.

For example, since we defined that each step should have a cover image, we get a type error if we forget to add the image inside a step.

content.md
## !intro Hello
Lorem ipsum dolor sit
## !!steps Step one
> no cover image!
Amet consectetur adipiscing
## !!steps Step two
![!cover](/two.png)
## !!steps Step three
page.jsx
import Content from "./content.md"
import {
parse,
Block,
ImageBlock,
} from "codehike"
import { z } from "zod"
const Schema = Block.extend({
intro: Block,
steps: z.array(
Block.extend({ cover: ImageBlock }),
),
})
const { intro, steps } = parse(
Content,
Schema,
)
export function Page() {
return <div>...</div>
}

Unhandled Error

Error at:
## !!steps Step one
missing `cover`


And that's how Code Hike gives you type-safe fine-grained markdown, making your content more flexible so you can use the whole power of React to present it in any way you want.


Learn more: