Skip to content

Instantly share code, notes, and snippets.

@sudkumar
Last active November 3, 2024 17:56
Show Gist options
  • Save sudkumar/70834062f9243558846249f2c2f98902 to your computer and use it in GitHub Desktop.
Save sudkumar/70834062f9243558846249f2c2f98902 to your computer and use it in GitHub Desktop.
MDX Remark plugin to handle frontmatter
// helps us in parsing the frontmatter from text content
const matter = require('gray-matter')
// helps us safely stringigy the frontmatter as a json object
const stringifyObject = require('stringify-object')
// helps us in getting the reading time for a given text
const readingTime = require('reading-time')
// please make sure you have installed these dependencies
// before proceeding further, or remove the require statements
// that you don't use
/**
* This is a plugin for remark in mdx.
* This should be a function that may take some options and
* should return a function with the following signature
* @param tree - the MDXAST
* @param file - the file node
* @return void - it should mutate the tree if needed
*/
module.exports = () => (tree, file) => {
// we will get the frontMatter using `gray-matter`
const { data: frontMatter, content } = matter(file.contents)
// the frontMatter holds the json object of the frontmatter
// the content holds the text of markdown except frontmatter
// we can do whatever we want with the frontmatter
// like, adding the time to read, formatting the date to display,
// adding a short description using the content
const { text } = readingTime(content)
frontMatter.timeToRead = text
// finally we will add a `export` node to the tree
tree.children.push({
type: 'export',
value: `export const frontMatter = ${stringifyObject(frontMatter)}`,
})
// now `frontMatter` will be available to use in our codebase
// we essentically changed the frontmatter of yml form to a
// constant and exported it
// now we need to remove the frontmatter from the tree
// because it has already been processed by mdx and nodes
// have beed created for it assuming it was a markdown content
//
// remove the thematicBreak "<hr />" to first heading
// --- => thematicBreak
// title: this
// date: 2020-12-12 => becomes heading
// ---
if (tree.children[0].type === 'thematicBreak') {
const firstHeadingIndex = tree.children.findIndex(t => t.type === 'heading')
if (firstHeadingIndex !== -1) {
// we will mutate the tree.children by removing these nodes
tree.children.splice(0, firstHeadingIndex + 1)
}
}
}
---
title: Hello World
date: 2020-12-12
---
Some content
<!-- is essentially same as writting -->
export const frontMatter = {
title: "Hello World"
date: "2020-12-12",
timeToRead: "3 min read" <!-- this is automatically added so that's an advantage -->
}
Some content
// we will import our custom remark plugin
const frontmatterRemarkPlugin = require('./frontmatter')
// here I'm using next.config.js example to use our custom plugin, which
// internally passes these options to `@mdx-js/loader`.
// so you can our custom plugin wherever we can use `@mdx-js`
// add it to the remarkPlugins option to the @next/mdx plugin
const mdxPlugin = require('@next/mdx')({
// these options directly gets passed to `@mdx-js/loader`
options: {
remarkPlugins: [frontmatterRemarkPlugin],
},
})
// export the configuration
module.exports = mdxPlugin({
pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
})
@lingdocs
Copy link

Great! Just using Next.js 12 I had to change line 22 of frontmatter.js to:

  const { data: frontMatter, content } = matter(file.value)

@itsjavi
Copy link

itsjavi commented Sep 26, 2022

For some reason, the export appears as text in the rendered page. I followed every step.
Any ideas?

image

thanks a lot for this gist anyway

EDIT: it seems more complex than I thought vercel/next.js#39590
Maybe the AST changed from MDX 1.0. to 2.0

After hours of digging, I ended up creating my own @next/mdx loader that works the way I needed (loading frontmatter into a custom layout, customizable via options). Its here: https://github.com/itsjavi/next-mdx-frontmatter

@pgarciacamou
Copy link

pgarciacamou commented Dec 31, 2022

EDIT

See also: mdx-js/mdx#1971 (comment)


I hope this helps someone.

I'm a newb at this, so I couldn't figure out for the life of me what was going on!
After a couple of days, it finally clicked!!

I did not want to create a custom loader! So, I found that we can use abstract-syntax-tree to parse the code into AST and inject it into the tree!

// <root>/unified/plugins/remark-default-export.mjs
import AST from "abstract-syntax-tree";

export default function remarkDefaultExport({
  path = "../../app/components/blog.layout",
  name = "Layout",
} = {}) {
  return (tree, file) => {
    const { frontmatter = {}, ...rest } = file.data;
    const data = { ...rest, ...frontmatter };
    const LAYOUT = {
      IMPORT: `import ${name} from "${path}";`,
      EXPORT: `export default ${name}(JSON.parse(\`${JSON.stringify(data)}\`));`,
    };
    tree.children.unshift(
      {
        type: "mdxjsEsm",
        value: LAYOUT.IMPORT,
        data: {
          estree: AST.parse(LAYOUT.IMPORT),
        },
      },
      {
        type: "mdxjsEsm",
        default: true,
        value: LAYOUT.EXPORT,
        data: {
          estree: AST.parse(LAYOUT.EXPORT),
        },
      }
    );
  };
}

And then:

// <root>/next.config.mjs
import nextMDX from "@next/mdx";

// 3rd party remark plugins
import remarkFrontmatter from "remark-frontmatter";
import remarkParseFrontmatter from "remark-parse-frontmatter";
import remarkReadingTime from "remark-reading-time";
import remarkGfm from "remark-gfm"; 

// Custom remark plugins
import remarkDefaultExport from "./unified/plugins/remark-default-export.mjs";

const mdxPlugin = nextMDX({
  // these options directly gets passed to `@mdx-js/loader`
  options: {
    remarkPlugins: [
      remarkFrontmatter,
      remarkParseFrontmatter,
      remarkUnwrapTexts,
      remarkReadingTime,
      [remarkDefaultExport, { path: "../../app/components/blog.layout" }],
      remarkGfm,
    ],
    // etc...
    // rehypePlugins: [ rehypePrettyCode /* https://rehype-pretty-code.netlify.app/ */, rehypeSlug, rehypeAutolinkHeadings ]
  },
})

And my component looks like:

// <root>/app/components/blog.layout.tsx
function BlogPostLayout(props) { /* ... */ }

type LayoutProps = { /* ... */ }
export default function Layout(layoutProps: LayoutProps) {
  return (props: PropsWithChildren<TBlogPostLayout>) => (
    <BlogPostLayout {...layoutProps} {...props} />
  )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment