mirror of
https://github.com/DefectingCat/DefectingCat.github.io
synced 2025-07-15 16:51:37 +00:00
Generate post toc on build time
This commit is contained in:
@ -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;
|
||||
|
@ -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;
|
@ -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.
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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$/, '');
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user