The benefit of MDX is simply that you can use components inside your Markdown. Hey look a button:
Create a Remix.run project using npx create-remix@latest
and choose TypeScript and to host it on Vercel.
tsconfig.json
"target"
to "ES2020"
"module": "ES2020",
npm install --save-exact mdx-bundler front-matter rehype-highlight remark-gfm rehype-autolink-headings rehype-toc rehype-slug esbuild@0.12.09
Create a folder in your project app/utils
and add these files:
app/utils/fs.server.ts
export { readFile } from "fs/promises";
export { resolve } from "path";
app/utils/mdx.server.ts
export { bundleMDX } from "mdx-bundler";
app/utils/post.tsx
import parseFrontMatter from "front-matter";
import { readFile } from "./fs.server"
import path from "path";
import { bundleMDX } from "./mdx.server";
import haskell from "highlight.js/lib/languages/haskell";
export type Post = {
slug: string;
title: string;
};
export type PostMarkdownAttributes = {
title: string;
};
export async function getPost(slug: string) {
const source = await readFile(
path.join(`${__dirname}/../../blog-posts`, slug + ".mdx"),
"utf-8"
);
const rehypeHighlight = await import("rehype-highlight").then(
(mod) => mod.default
);
const { default: remarkGfm } = await import("remark-gfm");
const { default: rehypeAutolinkHeadings } = await import(
"rehype-autolink-headings"
);
const { default: rehypeToc } = await import("rehype-toc");
const { default: rehypeSlug } = await import("rehype-slug");
const post = await bundleMDX({
source,
xdmOptions(options, frontmatter) {
options.remarkPlugins = [
...(options.remarkPlugins ?? []),
// remarkMdxImages,
remarkGfm,
// remarkBreaks,
// [remarkFootnotes, { inlineNotes: true }],
];
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
rehypeAutolinkHeadings,
rehypeSlug,
rehypeToc,
[
rehypeHighlight,
{ format: "detect", ignoreMissing: true, languages: { haskell } },
],
];
return options;
},
}).catch((e) => {
console.error(e);
throw e;
});
return post;
}
export async function getPosts() {
const postsPath = await fs.readdir(`${__dirname}/../../blog-posts`, {
withFileTypes: true,
});
const posts = await Promise.all(
postsPath.map(async (dirent) => {
const file = await readFile(
path.join(`${__dirname}/../../blog-posts`, dirent.name)
);
const { attributes } = parseFrontMatter(file.toString());
return {
slug: dirent.name.replace(/\.mdx/, ""),
//@ts-ignore
title: attributes.title,
};
})
);
return posts;
}
app/routes/index.tsx
import { Link, useLoaderData } from "remix";
import { getPosts } from "~/utils/post";
import type { Post } from "~/utils/post";
export const loader = async () => {
return getPosts();
};
export default function Posts() {
const posts = useLoaderData<Post[]>();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link to={post.slug}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
app/routes/$.tsx
This is the "slug" handler.
import { getMDXComponent } from "mdx-bundler/client";
import { useMemo } from "react";
import { json, Link, LoaderFunction, useLoaderData } from "remix";
import { getPost } from "~/utils/post";
type LoaderData = {
frontmatter: any;
code: string;
};
export const loader: LoaderFunction = async ({ params, request }) => {
const slug = params["*"];
if (!slug) throw new Response("Not found", { status: 404 });
const post = await getPost(slug);
if (post) {
const { frontmatter, code } = post;
return json({ frontmatter, code });
} else {
throw new Response("Not found", { status: 404 });
}
};
export default function Post() {
const { code, frontmatter } = useLoaderData<LoaderData>();
const Component = useMemo(() => getMDXComponent(code), [code]);
return (
<>
<Link to="/">← Back to blog index</Link>
<h1>{frontmatter.title}</h1>
<Component />
</>
);
}
Create a folder in your project called blog-posts
and add a file blog-posts/first-post.mdx
:
---
title: "First post"
description: "the first post."
date: 2022-02-03
---
I'm trying something out with Remix.
## code
```js
import { Outlet } from "remix";
export default function Blog() {
return (
<div className="flex justify-center">
<div className="prose lg:prose-xl py-10">
<Outlet />
</div>
</div>
);
}
```
See https://github.com/remix-run/remix/tree/main/examples/tailwindcss for how to configure Tailwind CSS.
I ran into some problems with esbuild EACCESS errors on Vercel and I'm now hosting this on Fly.io
Use the template at https://github.com/jmn/remix-mdx-blog to get started.