mdxページに目次をつける
2023年10月16日
目次
page.tsxにmdx.defaultを使った書き方をした
const headers = createNestedHeadings(
mdx
.default()
.props.children.filter((v: any) => v.type?.name?.search(/^h\d+/) === 0)
.map((v: any) => {
return {
title: v.props.children,
id: v.props.id,
name: v.type?.name,
};
}),
2
);
npm run build時にmdx.defaultが存在しないためうまく行かない
下記のようになってしまう。未解決
{ frontmatter: [Getter] }
remark-mdx-frontmatterの中身を借りてきて自前で実装
IDの付与
rehype-slugを使う。
肝の部分
headersを自前で作って渡してあげる。
valueToEstree({
data,
headers,
})
ただし、rehype-slugでつけているIDと一部違うため、合わせる実装が必要。未解決
全体
createNestedHeadingsを中で持っているので、utilsで定義してるものとかぶっている。 (commonjsとtypescriptの違いがあるため、シンプルな方法があればリファクタリング検討)
import nextMDX from '@next/mdx';
import nextPWA from '@ducanh2912/next-pwa';
import remarkFrontmatter from 'remark-frontmatter';
import { name as isIdentifierName } from 'estree-util-is-identifier-name';
import rehypeSlug from 'rehype-slug';
import { valueToEstree } from 'estree-util-value-to-estree';
import toml from 'toml';
import yaml from 'yaml';
const createNestedHeadings = (
headings,
currentLabel,
tableOfContentsItems,
isBreak
) => {
const items = tableOfContentsItems || [];
for (let i = 0; i < headings.length; i++) {
const heading = headings[i];
const label = parseInt(heading.name.replace('h', ''));
const nextElement = headings[i + 1];
const nextLabel =
nextElement && parseInt(nextElement.name.replace('h', ''));
const item = { ...heading };
if (nextLabel > currentLabel) {
item.items = createNestedHeadings(
headings.slice(i + 1),
nextLabel,
item.items,
true
);
}
if (label > currentLabel) {
continue;
}
if (isBreak && label < currentLabel) {
break;
}
items.push(item);
}
return items;
};
/** @type {import('next').NextConfig} */
const withMDX = nextMDX({
extension: /\.mdx$/,
options: {
remarkPlugins: [
remarkFrontmatter,
({ name = 'frontmatter', parsers } = {}) => {
if (!isIdentifierName(name)) {
throw new Error(
`Name this should be a valid identifier, got: ${JSON.stringify(
name
)}`
);
}
const allParsers = {
yaml: yaml.parse,
toml: toml.parse,
...parsers,
};
return (ast) => {
let data;
const headers = ast.children
.filter((child) => child.type === 'heading')
.map((v) => {
const text = v.children
.filter((v) => v.type === 'text')
.map((v) => v.value)
.join('');
return {
id: text,
title: text,
name: `h${v.depth}`,
};
});
const node = ast.children.find((child) =>
Object.hasOwn(allParsers, child.type)
);
if (node) {
const parser = allParsers[node.type];
const { value } = node;
data = parser(value);
}
ast.children.unshift({
type: 'mdxjsEsm',
value: '',
data: {
estree: {
type: 'Program',
sourceType: 'module',
body: [
{
type: 'ExportNamedDeclaration',
specifiers: [],
declaration: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name },
init: valueToEstree({
data,
headers: createNestedHeadings(headers, 2),
}),
},
],
},
},
],
},
},
});
};
},
],
rehypePlugins: [rehypeSlug],
},
});
const withPWA = nextPWA({
dest: 'public',
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
});
const options = (phase, { defaultConfig }) => {
const plugins = [withMDX, withPWA];
return plugins.reduce((acc, plugin) => plugin(acc), {
...defaultConfig,
reactStrictMode: process.env.NODE_ENV === 'development' ? false : true,
pageExtensions: ['ts', 'tsx', 'mdx'],
experimental: {
// mdxRs: true,
},
images: {
unoptimized: true,
domains: ['images.microcms-assets.io'],
},
// output: 'export',
});
};
export default options;