Skip to content

Instantly share code, notes, and snippets.

@rstacruz
Last active March 16, 2023 05:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rstacruz/e4db31ccb172f263cf96ffbc80b90533 to your computer and use it in GitHub Desktop.
Save rstacruz/e4db31ccb172f263cf96ffbc80b90533 to your computer and use it in GitHub Desktop.
Next.js: loading a list of articles for a blog

Next.js: loading a list of articles for a blog

A blog might need a list of articles on its index page. Here's now I did it with Next.js.

Article files

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!

Packages

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

pages/index.js

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 }
}

lib/articles.js

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

lib/articleFiles.js

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

pages/[id].js

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 }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment