Est. reading time: 6 minutes
You know, this is the really nice thing about being a developer and having a blog "on the side". You do things in personal projects, stumble over a bug or improvement at work and you want to share it with the world. There's always something floating around your mind that would fit in a blog post. Even if it's just a quick note.
Well, MDX had a 2.0 release - quite a while ago, to be honest - and I wanted to upgrade. This also gave me a chance to improve some things that I did not quite like about how this whole setup here works.
For example I was using this snippet I found somewhere on the internet to get a list of all my blogposts and their metadata and show them under /blog
function importAll(r: any) { return r.keys().map((fileName: string) => ({ link: fileName.substr(1).replace(/.mdx$/, ''), module: r(fileName), }));}
export const posts: { link: string, module: any }[] = importAll( require.context('.', true, /\.\/.*.mdx/),);
I'm sorry that I cannot give credit to the creator anymore, since it was a long time ago and I don't remember where I picked it up 🙇♂️ But this always was a bit of a "meh" thing, since NextJS gives you all those nice features like getStaticProps
that make this so much nicer to look at...plus I saw this require.context
thing as a bit of a hack 🤷♂️. So I ditched it. I'm using @next/mdx
which allows me to use .mdx
files as pages directly, so all my blogposts live in the /pages/blog
directory and get turned into blogposts automagically.
export const getStaticProps: GetStaticProps = async () => { const postsDirectory = join(process.cwd(), 'pages/blog'); const postSlugs = fs .readdirSync(postsDirectory) .filter((filename) => filename.endsWith('.mdx'));
const posts = postSlugs.map((slug) => { const fileContents = fs.readFileSync( join(`${postsDirectory}/${slug}`), 'utf8', ); const { data } = matter(fileContents); return { slug: slug.replace(/\.mdx$/, ''), ...data }; });
return { props: { posts }, };};
Another thing that changed was the blogpost metadata. Previously this was an javascript meta object embedded in the blogpost:
export const meta = { title: 'How to use TailwindCSS with Angular 11+', description: "Let's see how much work it actually is to get Angular 11 working with TailwindCSS...", date: '15.02.2021',};
Now it's a YAML frontmatter block that get's parsed by a simple one-liner const { data } = matter(fileContents);
using gray-matter.
---title: 'How to use TailwindCSS with Angular 11+'description: "Let's see how much work it actually is to get Angular 11 working with TailwindCSS..."date: '15.02.2021'---
This works fine for the blogpost list overview where I'm doing this manually (in getStaticProps
) but frontmatter is not supported by default in @next/mdx
. It can be enabled by adding 2 remark plugins though - remarkFrontmatter and remarkMdxFrontmatter. This also made it necessary to move next.config.js
over to ESM land since remarkFrontmatter
is ESM only. This meant slapping a .mjs
file extension on to the config file and porting the whole thing over to ESM syntax. Done 👍
// nextjs.config.mjs
import nextMdx from '@next/mdx';import remarkFrontmatter from 'remark-frontmatter';import { remarkMdxFrontmatter } from 'remark-mdx-frontmatter';
const withMDX = nextMdx({ extension: /\.mdx?$/, options: { remarkPlugins: [ remarkFrontmatter, [remarkMdxFrontmatter, { name: 'meta' }], ], rehypePlugins: [], providerImportSource: '@mdx-js/react', },});
export default withMDX({ images: { domains: ['pbs.twimg.com'], }, pageExtensions: ['ts', 'tsx', 'mdx'],});
This gives my BlogPost
component, which I use as a wrapper for to content defined in the .mdx
files, direct access to the metadata of the blogpost without much hassle.
<!--- example blogpost .mdx file -->
---
title: 'How to use TailwindCSS with Angular 11+'description: "Let's see how much work it actually is to get Angular 11 working with TailwindCSS..."date: '15.02.2021'
---
import BlogPost from 'components/BlogPost';
export default ({ children }) => <BlogPost meta={meta}>{children}</BlogPost>;
# Angular 11(+) and TailwindCSS 🎐
blogpost content goes here...
// BlogPost component wrapper used for every blogpost
const BlogPost = ({ children, meta }: { children: any, meta: any }) => { const router = useRouter(); const readingTime = Math.ceil(countWords(children) / 200);
return ( <> <NextSeo title={`csim | blog - ${meta.title}`} description={meta.description} canonical={`https://www.csim.xyz${router.pathname}`} openGraph={{ url: `https://www.csim.xyz${router.pathname}`, title: `csim | blog - ${meta.title}`, description: `${meta.description}`, site_name: 'csim | blog', }} /> <div className="flex justify-center px-4 dark:bg-night-500 dark:text-white min-h-screen"> <div className="w-full max-w-3xl pt-20 pb-24"> <p className="text-sm text-gray-400"> Est. reading time: {readingTime} minutes </p> {children} </div> </div> </> );};
export default BlogPost;
This whole thing took me an evening. The upgrade was not much of an issue but I hit a bit of a wall when it came to the new way of handling the metadata. I wanted to get rid of the old snippet but keep the implementation of having my .mdx
files as pages. Luckily my Github-fu did not forsake me and I stumbled upon this PR that nudged me in the right direction 🎉