A blog might need a list of articles on its index page. Here's now I did it with Next.js.
Files are stored as Markdown files in a folder like articles/
. Each Markdown file has YAML frontmatter.
articles/
├── bolognese.md
├── marinara.md
└── putanesca.md
---
title: Putanesca
---
Here is a recipe for Putanesca. First, make some noodles and add some sauce. Serve and enjoy!
I used gray-matter for parsing frontmatter, raw-loader for loading file contents onto Webpack. By using raw-loader, we get Webpack's auto file reloading in development for free. glob is used for tests as a substitute for raw-loader.
yarn add gray-matter glob raw-loader
In a page, I load the articles in getStaticProps()
so frontmatter parsing happens on build time.
import articles from '../lib/articles'
import Link from 'next/link'
export default function Home(props) {
const { links } = props
return (
<ul>
{links.map(({ id, title }) => (
<li key={id}>
<Link href={`/article/${id}`}>
<a>
{id} / {title}
</a>
</Link>
</li>
))}
</ul>
)
}
export async function getStaticProps() {
const links = Object.values(articles).map((article) => {
return { id: article.id, title: article.meta?.title || article.id }
})
const props = { links }
return { props }
}
The articles.js
file takes care of converting a list of files
to metadata.
import matter from 'gray-matter'
import files from './articleFiles'
/**
* Transform the list of articles and inject more metadata
*/
const articles = Object.fromEntries(
files.map(({ raw, filename }) => {
const { data, content } = matter(raw)
const id = filename.replace(/\.mdx?$/, '')
return [id, { meta: data, content, filename, id }]
})
)
// result:
// {
// marinara:
// { meta: { title: 'Marinara' }, content: '...', filename: 'marinara.md', id: 'marinara' },
// putanesca:
// { meta: { title: 'Putanesca' }, content: '...', filename: 'putanesca.md', id: 'putanesca' },
// bolognese:
// { meta: { title: 'Bolognese' }, content: '...', filename: 'bolognese.md', id: 'bolognese' },
// }
export default articles
This file will load .md
files from ../articles
. This has a Webpack version that uses raw-loader for auto-reloading on dev, and a Jest version that's suitable for tests where Webpack loaders aren't available. This code stays on the server and never makes it to the browser.
import fs from 'fs'
import glob from 'glob'
import path from 'path'
const files = require.context ? getFilesForWebpack() : getFilesForJest()
function getFilesForWebpack() {
const req = require.context('!!raw-loader!../articles/', true, /\.mdx?$/)
return req.keys().map((key) => ({
raw: req(key).default,
filename: key.substr(2),
}))
}
function getFilesForJest() {
const root = path.resolve(process.cwd(), './articles')
const paths = glob.sync('**/*.{md,mdx}', { cwd: root })
return paths.map((filename) => {
const fullpath = path.resolve(root, filename)
const raw = fs.readFileSync(fullpath, 'utf-8')
return { raw, filename }
})
}
export default files
I used next-mdx-remote to render the Markdown to HTML. This can be done by any other markdown solution &mdash hydrate/renderToString can be swapped out for others like mdx-bundler or markdown-it or remark.
import renderToString from 'next-mdx-remote/render-to-string'
import hydrate from 'next-mdx-remote/hydrate'
import articles from '../lib/articles'
export default function ArticlePage(props) {
const { meta, mdxSource, id } = props
if (!mdxSource) return null
// Hydrate mdxSource → jsx
const content = hydrate(mdxSource, { components: {} })
return (
<main>
<h1>{meta.title || id}</h1>
{content}
<pre>{JSON.stringify(meta, null, 2)}</pre>
</main>
)
}
/** Return a list of pages to be rendered. */
export async function getStaticPaths() {
const paths = Object.values(articles).map((article) => {
return { params: { id: article.id } }
})
return { paths, fallback: false }
}
/** Return props to give mdx-rendered markup. */
export async function getStaticProps({ params }) {
const { id } = params
const article = articles[id]
if (!article) return { notFound: true }
// Render mdx → mdxSource
const { meta, content } = article
const mdxSource = await renderToString(content, { components: {} })
const props = { meta, mdxSource, id }
return { props }
}