I am trying to create an MDX blog and integrate it into my project. I currently have most of the setup done but I cannot for the life of me get MDX file into a component to render between my Layout. Here's my issue, all of the examples showcase something weird, for example,
MDX docs aren't typed, and I cannot get this to work in typescript
export default function Page({code}) {
const [mdxModule, setMdxModule] = useState()
const Content = mdxModule ? mdxModule.default : Fragment
useEffect(() => {
;(async () => {
setMdxModule(await run(code, runtime))
})()
}, [code])
return <Content />
}
And Nextjs just advises to put mdx files under /pages/
, which doesn't fit my usecase
What I want:
- Have an MDX file with Frontmatter YAML metadata
- Load that MDX file into a component and display it
What I have currently
Nextjs config
import mdx from '@next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
/** @type {import('next').NextConfig} */
const config = {
eslint: {
dirs: ['src'],
},
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'tsx', 'ts'],
images: {
domains: [
'res.cloudinary.com',
'picsum.photos', //TODO
],
},
// SVGR
webpack: (config, options) => {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: [
{
loader: '@svgr/webpack',
options: {
typescript: true,
icon: true,
},
},
],
});
config.module.rules.push({
test: /\.mdx?$/,
use: [
options.defaultLoaders.babel,
{
loader: '@mdx-js/loader',
options: {
// providerImportSource: '@mdx-js/react',
remarkPlugins: [remarkFrontmatter, remarkGfm],
},
},
],
});
return config;
},
};
export default config;
My projects/[slug].tsx
file
import { GetStaticPaths, GetStaticProps } from 'next';
import { getFileBySlugAndType, getFiles } from '@/lib/mdx/helpers';
import { Layout } from '@/components/layout/Layout';
import Seo from '@/components/Seo';
import * as runtime from 'react/jsx-runtime.js'
import { ProjectFrontMatter } from '@/types/frontmatter';
import { useEffect, useState } from 'react';
import {compile, run} from '@mdx-js/mdx'
import { MDXContent } from 'mdx/types';
type SingleProjectPageProps = {
frontmatter: ProjectFrontMatter;
content: string;
};
export default function SingleProjectPage({
frontmatter,
content,
}: SingleProjectPageProps) {
return (
<Layout>
<Seo
templateTitle={frontmatter.name}
description={frontmatter.description}
date={new Date(frontmatter.publishedAt).toISOString()}
/>
<main>
<section className=''>
<div className='layout'>
<div className='mt-8 flex flex-col items-start gap-4 md:flex-row-reverse md:justify-between'>
{/* <CustomLink
href={`https://github.com/theodorusclarence/theodorusclarence.com/blob/main/src/contents/projects/${frontmatter.slug}.mdx`}
>
Edit this on GitHub
</CustomLink>
<CustomLink href='/projects'>← Back to projects</CustomLink> */}
</div>
</div>
</section>
</main>
</Layout>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getFiles('projects');
return {
paths: posts.map((p) => ({
params: {
slug: p.replace(/\.mdx/, ''),
},
})),
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params!['slug'] as string;
const { data, content } = getFileBySlugAndType(slug, 'projects');
return {
props: { frontmatter: data as ProjectFrontMatter, content },
};
};
And my helper function
export const getFileBySlugAndType = (slug: string, type: ContentType) => {
const file = readFileSync(
join(process.cwd(), 'src', 'content', type, slug + '.mdx'),
'utf-8'
);
const { data, content } = matter(file);
return { data, content };
};
I get as props in my SingleProjectPage, frontmatter data, and a string of the rest of the MDX content, which is correct, but I need to turn this string into MDX component. One of the libraries that does this is MDX-Bundler
, but it hasn't been updated this year, and I'd prefer to use mdx-js
if possible as it just release 2.0 version.