Generate post toc on build time

This commit is contained in:
DefectingCat
2022-08-19 10:50:02 +08:00
parent 03d6fd1a0f
commit f6cb2a8f74
7 changed files with 114 additions and 75 deletions

View File

@ -1,4 +1,4 @@
import { getHeadings } from 'lib/utils';
import { getHeadings, SingleToc } from 'lib/utils';
import Anchor from 'components/mdx/Anchor';
import styles from './PostToc.module.css';
import classNames from 'classnames';
@ -6,10 +6,39 @@ import { useCallback, useState } from 'react';
import { FiChevronDown } from 'react-icons/fi';
interface Props {
headings: ReturnType<typeof getHeadings>;
toc: SingleToc[];
tocLength: number;
}
const PostTOC = ({ headings }: Props) => {
const TocItem = ({ item }: { item: SingleToc }) => {
return (
<li key={item.head}>
<Anchor href={item.link} external={false}>
{item.head}
</Anchor>
</li>
);
};
const TocList = ({
toc,
children,
}: {
toc: SingleToc[];
children?: React.ReactElement;
}) => {
return (
<ul className="pl-4 border-l-4 border-gray-300 toc">
{toc.map((h) => (
<>
<TocItem key={h.head} item={h} />
{children}
</>
))}
</ul>
);
};
const PostToc = ({ toc, tocLength }: Props) => {
const [show, setShow] = useState(false);
const handleClick = useCallback(() => setShow((show) => !show), []);
@ -22,7 +51,7 @@ const PostTOC = ({ headings }: Props) => {
'my-4'
)}
style={{
maxHeight: show ? (headings?.length ?? 0) * 50 + 70 : 70,
maxHeight: show ? (tocLength ?? 0) * 50 + 70 : 70,
}}
>
<h2
@ -47,18 +76,23 @@ const PostTOC = ({ headings }: Props) => {
/>
</h2>
<ul className="pl-4 border-l-4 border-gray-300 toc">
{headings?.map((h) => (
<li key={h.link}>
<Anchor href={h.link} external={false}>
{h.text}
</Anchor>
</li>
<div className="pl-4 border-l-4 border-gray-300 toc">
<ul className="!pl-[unset]">
{toc?.map((h) => (
<>
<TocItem item={h} key={h.link} />
{h.children.map((child) => (
<ul className="!pl-4" key={child.link}>
<TocItem item={child} />
</ul>
))}
</>
))}
</ul>
</div>
</div>
</>
);
};
export default PostTOC;
export default PostToc;

View File

@ -1,35 +0,0 @@
import dynamic from 'next/dynamic';
import { MyMatters } from 'types';
const Footer = dynamic(() => import('components/Footer'));
const HeadBar = dynamic(() => import('components/NavBar'));
const PostComment = dynamic(() => import('components/post/PostComment'));
const SlideToc = dynamic(() => import('components/post/SlideToc'));
interface Props extends MyMatters {
showTOC?: boolean;
children: React.ReactElement;
}
const MDXLayout = ({ title, date, showTOC = true, children }: Props) => {
return (
<>
<HeadBar />
<main id="article" className="relative max-w-4xl px-4 mx-auto my-10">
<h1>{title}</h1>
<time>{date}</time>
{showTOC && <SlideToc />}
<article id="post-content">
{children}
<PostComment />
</article>
</main>
<Footer />
</>
);
};
export default MDXLayout;

View File

@ -4,7 +4,7 @@ import matter from 'gray-matter';
import { MyMatters, Post } from 'types';
import { sortByDate } from 'lib/utils';
const dataPath = 'data/posts';
export const dataPath = 'data/posts';
/**
* Read post meta info with gray-matter.

View File

@ -33,3 +33,45 @@ export const getHeadings = (source: string) => {
};
});
};
export type SingleToc = {
level: number;
head: string;
link: string;
children: SingleToc[];
};
export const generateToc = (source: string) => {
const regex = /^#{2,3}(?!#)(.*)/gm;
let lastH2: SingleToc | null = null;
const toc: SingleToc[] = [];
source.match(regex)?.map((h) => {
const heading = h.split(' ');
const level = heading[0].length;
const head = h.substring(level + 1);
switch (level) {
case 2: {
lastH2 = {
level,
head,
link: `#${head.toLocaleLowerCase().replace(/ /g, '-')}`,
children: [],
};
toc.push(lastH2);
break;
}
case 3: {
lastH2?.children.push({
level,
head,
link: `#${head.toLocaleLowerCase().replace(/ /g, '-')}`,
children: [],
});
break;
}
}
});
return toc;
};

View File

@ -8,6 +8,8 @@ import rehypePrism from '@mapbox/rehype-prism';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import dynamic from 'next/dynamic';
import { generateToc, SingleToc } from 'lib/utils';
import PostToc from 'components/post/PostToc';
const Footer = dynamic(() => import('components/Footer'));
const HeadBar = dynamic(() => import('components/NavBar'));
@ -15,6 +17,8 @@ const PostComment = dynamic(() => import('components/post/PostComment'));
const Slug = ({
mdxSource,
toc,
tocLength,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
@ -23,6 +27,7 @@ const Slug = ({
<main id="article" className="relative max-w-4xl px-4 mx-auto my-10">
<h1>{mdxSource.frontmatter?.title}</h1>
<time>{mdxSource.frontmatter?.date}</time>
<PostToc toc={toc} tocLength={tocLength} />
<article id="post-content">
<MDXRemote {...mdxSource} components={components as {}} />
@ -44,6 +49,8 @@ export const getStaticPaths: GetStaticPaths = async () => {
export const getStaticProps: GetStaticProps<{
mdxSource: MDXRemoteSerializeResult;
toc: SingleToc[];
tocLength: number;
}> = async ({ params }) => {
const slug = params?.slug?.toString();
if (!slug)
@ -52,6 +59,12 @@ export const getStaticProps: GetStaticProps<{
};
const post = await readSinglePost(slug);
const toc = generateToc(post);
let tocLength = toc.length;
toc.forEach(
(item) => item.children.length && (tocLength += item.children.length)
);
const mdxSource = await serialize(post, {
mdxOptions: {
remarkPlugins: [remarkGfm],
@ -63,9 +76,12 @@ export const getStaticProps: GetStaticProps<{
parseFrontmatter: true,
scope: data,
});
return {
props: {
mdxSource,
toc,
tocLength,
},
};
};

View File

@ -4,17 +4,19 @@ import fs from 'fs';
import path from 'path';
import { nanoid } from 'nanoid';
const dataPath = 'data/posts';
/**
* Build post information for Algolia search.
* @param filename
* @returns
*/
const postLists = () => {
const files = fs.readdirSync(path.join('pages/p'));
const files = fs.readdirSync(path.join(dataPath));
const myPosts = [];
files.map((f) => {
const content = fs.readFileSync(path.join('pages/p', f), 'utf-8');
const content = fs.readFileSync(path.join(dataPath, f), 'utf-8');
// const { data: meta, content } = matter(markdownWithMeta);
const slug = f.replace(/\.mdx$/, '');

View File

@ -93,34 +93,14 @@
}
#article .toc {
@apply hidden dark:bg-rua-gray-700;
@apply fixed z-10 p-6 bg-white rounded-md xl:inline-block;
@apply translate-x-[110%] top-1/2 -translate-y-1/2;
@apply max-h-[85%] overflow-auto;
padding-left: 0.8em;
@apply my-4;
}
#article .toc .toc-list .toc-list {
padding-left: 1rem;
padding-top: 0.5rem;
#article .toc li {
list-style-type: none;
}
#article .toc .toc-list-item {
padding-bottom: 0.5rem;
@apply transition-all;
}
#article .toc .toc-list-item:last-child {
padding-bottom: 0;
}
/* #article .toc .toc-link {
} */
#article .toc .is-active-link {
@apply font-semibold;
}
#article p {
margin: 1em 0;
}