next-mdx-relations
July 7, 2021
a tool for turning you markdown files into a digital garden by drawing relations between static files
Much of my thinking over the last year or so has been about digital gardening. I've written about rethinking the digital gardening metaphor and started building out digital-garden.dev as a place to practice some of that thinking. In really broad strokes, I'm interested in the following:
- Low friction authoring: write in markdown, but the system you use takes care of the rest
- alternative/anti-hierarchical data arrangements: no pagination, but easily parsed lists of information
- relational data: content is not the 'thing'. relations between things is the 'thing'
Some of this is idiosyncratic and theoretical, but a lot of it is practical. I want to develop tools that allow people to quickly and easily create digital gardens. digital-garden.dev was supposed to be that tool, but I wanted to try developing a package rather than boilerplate for gardening. next-mdx-relations
is a lower level tool for gardening that provides a more agnostic api for generating pages from md/x
and drawing relations between those files.
overview
If you want to spin up an md/x
powered nextjs
site, you nearly always are going to write some boilerplate. Either you're using next-mdx
and letting webpack sort things out, or, more likely, you're using some kind of mdx
parser like next-mdx-remote
or mdx-bundler
. In whatever case, you need to write functions for getStaticProps
and getStaticPaths
to generate paths and props for each page.
next-mdx-remote
provides a really simple API that takes care of writing that boilerplate for you. Additionally, it allows for granular processing of md/x
files and generating both extra metadata as well as relational data among static files in a collection.
getting started
Install peer dependencies and package.1
yarn add fast-glob matter next-mdx-remote next-mdx-relations
Create a config file that imports createUtils
from next-mdx-relations
and both returns and exports functions for getting paths, pages, and pageProps.
// next-mdx-relations.config.js
import { createUtils } from 'next-mdx-relations';
export const {
getPaths, // for `getStaticPaths`
getPages, // for index page / `garden.js`
getPageProps, // for `getStaticProps`
getPathsByProp // for `[tag].js`
} = createUtils({
content: '/content',
});
You get the following async helper functions
getPaths()
: use withgetStaticPaths
to get a list of file system based paths to your contentgetPages()
: use in on your homepage to get a list of all your pages with accompanying frontmattergetPageProps()
: use withgetStaticProps
to get your content and serialized frontmatter to pass tonext-mdx-remote
getPathsByProp()
: use to make a tag page -- it returns a list of values of a given property from all of your content
If this pattern looks familiar, it's because it's heavily indebted to stitches. I liked the idea of having the config file also be the place all of the package functionality is exported from. Additionally, this pattern allows you to have multiple instances of the package with their own scoped configurations. So if you have, like I did at one point, a blog and a garden, and those content types need to be handled differently, you can spin up two different configuration files (or put the createUtils
function elsewhere) to handle either the blog or the garden.
Finally, create a [...slug].js
file.
// [...slug].js
import React from 'react';
import PropTypes from 'prop-types';
import { MDXRemote } from 'next-mdx-remote';
import { getPaths, getPageProps } from '../next-mdx-relations.config.js';
function Slug({ mdx, ...pageNode }) {
const { frontmatter: { title, excerpt } } = pageNode;
return (
<article>
<h1>{title}</h1>
<p>{excerpt}</p>
<MDXRemote {...mdx} />
</article>
);
}
export async function getStaticProps({ params: { slug } }) {
const props = await getPageProps(slug);
return {
props
};
}
export async function getStaticPaths() {
const paths = await getPaths();
return {
paths,
fallback: false
};
}
export default Slug;
I've opted to use this catchall routing because it's the most generic way to handle routing. If you need something more granular, you can always make scoped catchall routes (ex. /garden/[...slug].js
).
meta and relational data
Relational data - the way things fit together, interact with each other, and (over)determine the meaning and value of other bits of data - seems to be the most important part of digital gardening. next-mdx-relations
makes generating meta and relational data a first class feature by exposing two points of intervention when content is being processed.
const { getPageProps } = createUtils({
content: '/content',
// page level
metaGenerators: {
mentions: node => markdownLinkExtractor(node.content).filter(l => l[0] === '/')
},
// collection level
relationGenerators: {
mentionedIn: nodes => nodes.map((node) => ({
...node,
meta: {
...node.meta,
mentionedIn: nodes.filter(
n => n.meta?.mentions.includes(`/${node.params.slug.join('/')}`))
}
}))
}
})
In the above example, we use metaGenerators
and relationGenerators
to add extra meta and relational data to each page. metaGenerators
work at the page or node level -- key value pairs have access to each md/x
file in isolation. I'm using a package to return an array of links that point to local content. The return value is added to a node's meta
object. relationalGenerators
work at the collection level -- key value pairs have access to nodes after the metaGenerators
have run. Unlike metaGenerators
you can stash whatever the return value is anywhere on the nodes. Here, we see if any other node 'mentions' the current node, and add those nodes to the current's meta
object. The result is a basic version of bi-directional links.
The above example is pretty straight forward, but achieves something slightly more difficult when working with static files alone. Additionally, metaGenerators
can be used to generate data or mutate content outside of the markdown serialization.
to follow
This is early days for the api. I'm still working out ways to keep things on the rails and not mutate data that shouldn't be, but I'm excited about what this has enabled me to build and what it might help others build, too.
Check it out on github.
- I chose to keep these as peer dependencies in case you need functionality that
next-mdx-relations
doesn't offer. In that case, you have the raw material to glob, get the frontmatter, and serialize your markdown the way you like.↩