Skip to content

Instantly share code, notes, and snippets.

@BretCameron
Created September 16, 2024 11:11
Show Gist options
  • Save BretCameron/77fbe5c6292ecdc7a97fe7c0e86d565f to your computer and use it in GitHub Desktop.
Save BretCameron/77fbe5c6292ecdc7a97fe7c0e86d565f to your computer and use it in GitHub Desktop.
A custom Remark plugin to create a super minimal MDX writing
// src/lib/frontmatter.plugin.mjs
import yaml from "js-yaml";
import { valueToEstree } from "estree-util-value-to-estree";
// Helper function to traverse nodes recursively
const traverse = (node, callback) => {
callback(node);
if ("children" in node && Array.isArray(node.children)) {
node.children.forEach((child) => traverse(child, callback));
}
};
function calculateReadingTimeInMinutes(content) {
const wordsPerMinute = 200;
const words = content.trim().split(/\s+/).length;
const minutes = words / wordsPerMinute;
return Math.ceil(minutes);
}
const isThematicBreak = (node) => node.type === "thematicBreak";
const isMdxjsEsm = (node) => node.type === "mdxjsEsm";
const isMdxJsxFlowElement = (node) => node.type === "mdxJsxFlowElement";
// This plugin checks for the presence of YAML front matter and passes it to predefined JSX elements, as well as exporting the Next.js `metadata` object for the given page.
const frontMatterPlugin = ({ jsxElementNames = [] }) => {
return (tree, file) => {
const children = tree.children;
if (!isThematicBreak(children[0])) {
return;
}
// Find the second thematic break indicating the end of front matter
let secondThematicBreakIndex = children.findIndex(
(node, index) => index > 0 && isThematicBreak(node)
);
if (secondThematicBreakIndex === -1) {
// If no second thematic break is found, assume front matter ends at index 1
secondThematicBreakIndex = 1;
}
// Remove front matter and thematic breaks from the tree
children.splice(0, secondThematicBreakIndex + 1);
const match = file.value.match(/---\n([\s\S]*?)---\n([\s\S]*)/);
const url =
"/" + file.history[0]?.split("/src/app/")[1]?.replace("/page.mdx", "");
let frontMatterData = {
url,
title: url,
};
if (!match) {
frontMatterData.readTime = `${calculateReadingTimeInMinutes(
file.value
)} min read`;
return;
}
const [, rawFrontMatter, restOfContent] = match;
try {
const yamlData = yaml.load(rawFrontMatter.trim());
frontMatterData = {
...frontMatterData,
...yamlData,
};
} catch (error) {
file.message(`Error parsing YAML front matter: ${error.message}`);
console.error(
`Error parsing YAML front matter: ${
error.message
}. File history: ${file.history.join("\n")}`
);
return;
}
const readTime = `${calculateReadingTimeInMinutes(restOfContent)} min read`;
frontMatterData.readTime = readTime;
const estreeFrontMatter = valueToEstree(frontMatterData);
const programData = {
type: "Program",
body: [
{
type: "ExpressionStatement",
expression: estreeFrontMatter,
},
],
sourceType: "module",
};
traverse(tree, (node) => {
if (
isMdxJsxFlowElement(node) &&
(jsxElementNames.includes(node.name ?? "") ||
jsxElementNames.includes("*"))
) {
node.attributes.push({
type: "mdxJsxAttribute",
name: "frontMatter",
value: {
type: "mdxJsxAttributeValueExpression",
value: JSON.stringify(frontMatterData),
data: { estree: programData },
},
});
}
});
const indexOfLastImportStatement = children.findLastIndex(
(node) =>
isMdxjsEsm(node) && (node.value ?? "").trim().startsWith("import")
);
const importDeclaration = {
type: "ImportDeclaration",
specifiers: [
{
type: "ImportSpecifier",
imported: { type: "Identifier", name: "generateMetadata" },
local: { type: "Identifier", name: "generateMetadata" },
},
],
source: { type: "Literal", value: "../generateMetadata" },
};
const exportDeclaration = {
type: "Program",
body: [
{
type: "ExportNamedDeclaration",
declaration: {
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "metadata",
},
init: {
optional: false,
type: "CallExpression",
callee: {
type: "Identifier",
name: "generateMetadata",
},
arguments: [estreeFrontMatter],
},
},
],
kind: "const",
},
specifiers: [],
source: null,
},
],
sourceType: "module",
};
const importNode = {
type: "mdxjsEsm",
value: `import { generateMetadata } from "../generateMetadata";`,
data: {
estree: {
type: "Program",
body: [importDeclaration],
sourceType: "module",
},
},
};
const exportNode = {
type: "mdxjsEsm",
value: `export const metadata = generateMetadata(${JSON.stringify(
frontMatterData
)});`,
data: { estree: exportDeclaration },
};
children.splice(indexOfLastImportStatement + 1, 0, importNode, exportNode);
};
};
export default frontMatterPlugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment