Parse Markdown with Markdoc in Remix
Markdoc is this new Markdown parser by Stripe, and it's a simple to use yet extendable library we can use in our Remix applications.
Markdown service
First, we can create our server-side service to parse Markdown.
// app/services/markdown.server.ts
import { parse, transform, type RenderableTreeNodes } from "@markdoc/markdoc";
export function markdown(markdown: string): RenderableTreeNodes {
return transform(parse(markdown));
}
Markdown component
Now, we can create a Markdown component to get that RenderableTreeNodes
(the JSON) to React elements.
// app/components/markdown.tsx
import { renderers, type RenderableTreeNodes } from "@markdoc/markdoc";
import * as React from "react";
type Props = { content: RenderableTreeNodes };
export function Markdown({ content }: Props) {
return <>{renderers.react(content, React)}</>;
}
Usage in a route
Finally, we can use both on our route.
// app/routes/articles.$article.tsx
import { json, type LoaderArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Markdown } from "~/components/markdown";
import { parseMarkdown } from "~/services/markdown.server";
export function loader({ params }: LoaderArgs) {
let markdown = await getArticleContent(params.article);
return json({ content: parseMarkdown(markdown) });
}
export default function Index() {
let { content } = useLoaderData<typeof loader>();
return <Markdown content={content} />;
}
Adding custom tags and components
Like MDX, Markdoc let us use custom React components we can then use in our Markdown content. We do this by extending both the service and component we created.
Let's say we want to add a counter inside the Markdown. We could create a component like this one and export a scheme object to use server-side.
import { useState } from "react";
type CounterProps = {
initialValue: number;
};
export function Counter({ initialValue }: CounterProps) {
let [count, setCount] = useState(initialValue);
return (
<div>
<output>{count}</output>
<button onClick={() => setCount((current) => current + 1)} type="button">
Increment
</button>
<button onClick={() => setCount((current) => current - 1)} type="button">
Decrement
</button>
</div>
);
}
export let scheme = {
render: Counter.name,
description: "Displays a counter with the initial value provided",
children: [],
attributes: {
initialValue: {
type: Number,
default: 0,
},
},
};
Now we can change our Markdown service to use the scheme.
import { parse, transform, type RenderableTreeNodes } from "@markdoc/markdoc";
import { scheme as counter } from "~/components/counter";
export function parseMarkdown(markdown: string): RenderableTreeNodes {
return transform(parse(markdown), {
tags: { counter },
});
}
And our Markdown component includes the Counter.
import { renderers, type RenderableTreeNodes } from "@markdoc/markdoc";
import * as React from "react";
import { Counter } from "./counter";
type Props = { content: RenderableTreeNodes };
export function Markdown({ content }: Props) {
return <>{renderers.react(content, React, { components: { Counter } })}</>;
}
With this, our Markdown can reference the Counter using the {% counter /%}
tag. We can even provide the value for the props {% counter initialValue=10 /%}
.
And the best thing is that we don't have to change our route!