From d95dcc39bee706fcd0d6a03fb02f5b5bf50a1a7b Mon Sep 17 00:00:00 2001 From: hayao Date: Mon, 8 Jul 2024 07:17:02 +0900 Subject: [PATCH] Add: Add TOC --- package.json | 2 + pnpm-lock.yaml | 30 ++++++ src/app/(hayao)/blog/posts/[...slug]/page.tsx | 9 +- src/components/elements/Markdown.tsx | 95 +++++++++++++------ src/components/elements/Toc.tsx | 29 ++++++ src/components/elements/Tweet.tsx | 14 ++- src/components/layouts/Drawer/Drawer.tsx | 5 +- .../layouts/Drawer/DrawerToggle.tsx | 5 +- src/components/layouts/Drawer/index.ts | 6 +- src/hooks/useDrawerAtom.ts | 6 ++ src/hooks/useNoColonId.ts | 9 ++ src/lib/atom.ts | 3 - src/lib/type.ts | 4 + 13 files changed, 170 insertions(+), 47 deletions(-) create mode 100644 src/components/elements/Toc.tsx create mode 100644 src/hooks/useDrawerAtom.ts create mode 100644 src/hooks/useNoColonId.ts delete mode 100644 src/lib/atom.ts create mode 100644 src/lib/type.ts diff --git a/package.json b/package.json index 3565d53..9cc6aa1 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,11 @@ "react-icons": "^5.2.1", "react-twitter-widgets": "^1.11.0", "rehype-pretty-code": "^0.13.2", + "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.0", "sass": "^1.77.6", "shiki": "^1.6.4", + "tocbot": "^4.28.2", "typescript": "5.4.5", "usehooks-ts": "^3.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ec25c7..6150a27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: rehype-pretty-code: specifier: ^0.13.2 version: 0.13.2(shiki@1.6.4) + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 rehype-stringify: specifier: ^10.0.0 version: 10.0.0 @@ -53,6 +56,9 @@ dependencies: shiki: specifier: ^1.6.4 version: 1.6.4 + tocbot: + specifier: ^4.28.2 + version: 4.28.2 typescript: specifier: 5.4.5 version: 5.4.5 @@ -2964,6 +2970,10 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3170,6 +3180,12 @@ packages: web-namespaces: 2.0.1 dev: false + /hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + dependencies: + '@types/hast': 3.0.4 + dev: false + /hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} dependencies: @@ -5791,6 +5807,16 @@ packages: unist-util-visit: 5.0.0 dev: false + /rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.0 + unist-util-visit: 5.0.0 + dev: false + /rehype-stringify@10.0.0: resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} dependencies: @@ -6566,6 +6592,10 @@ packages: dependencies: is-number: 7.0.0 + /tocbot@4.28.2: + resolution: {integrity: sha512-/MaSa9xI6mIo84IxqqliSCtPlH0oy7sLcY9s26qPMyH/2CxtZ2vNAXYlIdEQ7kjAkCQnc0rbLygf//F5c663oQ==} + dev: false + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false diff --git a/src/app/(hayao)/blog/posts/[...slug]/page.tsx b/src/app/(hayao)/blog/posts/[...slug]/page.tsx index 88686b7..3a6f9c9 100644 --- a/src/app/(hayao)/blog/posts/[...slug]/page.tsx +++ b/src/app/(hayao)/blog/posts/[...slug]/page.tsx @@ -7,7 +7,9 @@ import { FaArrowLeft, FaArrowRight } from "react-icons/fa6"; import Breadcrumbs from "@/components/elements/Breadcrumbs"; import { BlogHeading } from "@/components/elements/Heading"; import { ShareCurrentURL } from "@/components/elements/ShareCurrentURL"; +import Toc from "@/components/elements/Toc"; import { PostList as PostListElement } from "@/components/layouts/blog/PostPreviewList"; +import useNoColonId from "@/hooks/useNoColonId"; import { BLOG_URL_FORMAT } from "@/lib/blog/config"; import { findPostFromUrl } from "@/lib/blog/fromurl"; import { fetchedBlogPostList } from "@/lib/blog/post"; @@ -88,6 +90,8 @@ const MostRecentPostPreview = ({ post, type }: { post: PostData | null; type: "b }; export default function PostPage({ params }: { params: { slug: string[] } }) { + const contentId = useNoColonId(); + // get post data const postData = findPostFromUrl(params.slug.join("/")); @@ -125,7 +129,10 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
{dateToString(postDate)}
-
{postData.parsed}
+ +
+ {postData.parsed} +
diff --git a/src/components/elements/Markdown.tsx b/src/components/elements/Markdown.tsx index 9d30bb3..a7e16e8 100644 --- a/src/components/elements/Markdown.tsx +++ b/src/components/elements/Markdown.tsx @@ -3,54 +3,78 @@ import "@/style/markdown.css"; import { MDXComponents } from "mdx/types"; import { MDXRemote } from "next-mdx-remote/rsc"; import { Link } from "next-view-transitions"; -import { ReactNode } from "react"; +import { ComponentPropsWithoutRef } from "react"; import rehypeCodeTitles from "rehype-code-titles"; import rehypePrettyCode from "rehype-pretty-code"; +import rehypeSlug from "rehype-slug"; import rehypeStringify from "rehype-stringify"; import remarkGfm from "remark-gfm"; +import { ComponentPropsWithoutRefAndClassName } from "@/lib/type"; + import { BlogHeading as Heading } from "./Heading"; import Tweet from "./Tweet"; export default async function Markdown({ content, basepath }: { content: string; basepath: string }) { + // PropsにidがないとrehypeSlugが動かない + // https://stackoverflow.com/questions/78294682/rehype-slug-is-not-adding-ids-to-headings const components: MDXComponents = { - h1: ({ children }) => { - return {children}; + h1: ({ children, id }) => { + return ( + + {children} + + ); }, - h2: ({ children }) => { - return {children}; + h2: ({ children, id }) => { + return ( + + {children} + + ); }, - h3: ({ children }) => { - return {children}; + h3: ({ children, id }) => { + return ( + + {children} + + ); }, - h4: ({ children }) => { - return {children}; + h4: ({ children, id }) => { + return ( + + {children} + + ); }, - h5: ({ children }) => { - return {children}; + h5: ({ children, id }) => { + return ( + + {children} + + ); }, - a: ({ href, children }) => { + a: ({ href, children, id }) => { if (!href) return {children}; return ( - + {children} ); }, - p: ({ children }) => ( + p: ({ children, id }) => (

{children}

), - Tweet: ({ id }: { id: string }) => { - return ; - }, + Tweet: Tweet, img: (props) => { let src = props.src; if (!src?.startsWith("http")) { @@ -62,31 +86,36 @@ export default async function Markdown({ content, basepath }: { content: string; }, //code: ({ children }) => {children}, - ul: ({ children }) =>
    {children}
, + ul: ({ children, id }) => ( +
    + {children} +
+ ), //pre: ({ children, className }) =>
{children}
, - Flex: ({ children }: { children: ReactNode }) => { - return
{children}
; + Flex: ({ children, id, ...props }: ComponentPropsWithoutRefAndClassName<"div">) => { + return ( +
+ {children} +
+ ); }, - Grid: ({ children, col }: { children: ReactNode; col: number }) => { + Grid: ({ children, col, id, ...props }: Omit, "className"> & { col: number }) => { return ( -
+
{children}
); }, }; - /* だるいエラーについて - - https://github.com/hashicorp/next-mdx-remote/issues/403 - - 現在next-mdx-remoteはremarkGfm 4.0.0をサポートしていないため、3.0.1を使う必要がある - - */ - return (
{ + contentSelector: string; +} + +const Toc = ({ contentSelector, ...props }: TocProps) => { + const id = useNoColonId(); + useEffect(() => { + // Tocbotの初期化 + tocbot.init({ + tocSelector: `#${id}`, // 目次の表示部分 + contentSelector: contentSelector, // 目次を生成する対象 + headingSelector: "h2, h3", // 目次に表示する見出しのタグ + }); + + // コンポーネントがアンマウントされたときにTocbotを破棄 + return () => tocbot.destroy(); + }, []); + + return
; +}; + +export default Toc; diff --git a/src/components/elements/Tweet.tsx b/src/components/elements/Tweet.tsx index bf0117e..79edfb3 100644 --- a/src/components/elements/Tweet.tsx +++ b/src/components/elements/Tweet.tsx @@ -1,5 +1,13 @@ "use client"; -import { Tweet as TweetWidget } from "react-twitter-widgets"; -export default function Tweet({ id }: { id: string }) { - return ; +import { ComponentPropsWithoutRef } from "react"; +import { Tweet as TweetWidget, TweetProps as LibTweetProps } from "react-twitter-widgets"; + +export type TweetProps = ComponentPropsWithoutRef<"div"> & LibTweetProps; + +export default function Tweet({ tweetId, options, onLoad, renderError, ...props }: TweetProps) { + return ( +
+ ; +
+ ); } diff --git a/src/components/layouts/Drawer/Drawer.tsx b/src/components/layouts/Drawer/Drawer.tsx index 09a3540..e9c9283 100644 --- a/src/components/layouts/Drawer/Drawer.tsx +++ b/src/components/layouts/Drawer/Drawer.tsx @@ -2,10 +2,9 @@ import classNames from "clsx"; import { motion, Variants } from "framer-motion"; -import { useAtom } from "jotai"; import { useEffect } from "react"; -import { drawerAtom } from "@/lib/atom"; +import useDrawerAtom from "@/hooks/useDrawerAtom"; export interface DrawerProps { open?: boolean; @@ -14,7 +13,7 @@ export interface DrawerProps { } export default function Drawer(props: DrawerProps) { - const [open, setOpen] = useAtom(drawerAtom); + const [open, setOpen] = useDrawerAtom(); // Toggle open state const toggle = () => { diff --git a/src/components/layouts/Drawer/DrawerToggle.tsx b/src/components/layouts/Drawer/DrawerToggle.tsx index 169e874..3afe3aa 100644 --- a/src/components/layouts/Drawer/DrawerToggle.tsx +++ b/src/components/layouts/Drawer/DrawerToggle.tsx @@ -1,12 +1,11 @@ "use client"; -import { useAtom } from "jotai"; import { FaBars } from "react-icons/fa6"; -import { drawerAtom } from "@/lib/atom"; +import useDrawerAtom from "@/hooks/useDrawerAtom"; export default function DrawerToggle() { - const [open, setOpen] = useAtom(drawerAtom); + const [open, setOpen] = useDrawerAtom(); const toggle = () => { console.log("toggle"); diff --git a/src/components/layouts/Drawer/index.ts b/src/components/layouts/Drawer/index.ts index 89531ec..9c7c648 100644 --- a/src/components/layouts/Drawer/index.ts +++ b/src/components/layouts/Drawer/index.ts @@ -1,6 +1,4 @@ -import { useAtom } from "jotai"; - -import { drawerAtom } from "@/lib/atom"; +import useDrawerAtom from "@/hooks/useDrawerAtom"; export { default } from "./Drawer"; //export { default as Side } from "./DrawerSide"; @@ -9,7 +7,7 @@ export { default as ToggleSwitch } from "./DrawerToggle"; export const useDrawer = (): [boolean, () => void] => { //const hoge = useState(false); - const [open, setOpen] = useAtom(drawerAtom); + const [open, setOpen] = useDrawerAtom(); const toggleDrawer = () => setOpen((prev) => !prev); return [open, toggleDrawer]; }; diff --git a/src/hooks/useDrawerAtom.ts b/src/hooks/useDrawerAtom.ts new file mode 100644 index 0000000..5ff2a4d --- /dev/null +++ b/src/hooks/useDrawerAtom.ts @@ -0,0 +1,6 @@ +import { atom, useAtom } from "jotai"; + +const drawerAtom = atom(false); + +const useDrawerAtom = () => useAtom(drawerAtom); +export default useDrawerAtom; diff --git a/src/hooks/useNoColonId.ts b/src/hooks/useNoColonId.ts new file mode 100644 index 0000000..e5db562 --- /dev/null +++ b/src/hooks/useNoColonId.ts @@ -0,0 +1,9 @@ +import { useId } from "react"; + +const useNoColonId = () => { + const id = useId(); + // 先頭と末尾のコロンを削除 + return id.slice(1, -1); +}; + +export default useNoColonId; diff --git a/src/lib/atom.ts b/src/lib/atom.ts deleted file mode 100644 index 91d19d4..0000000 --- a/src/lib/atom.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from "jotai"; - -export const drawerAtom = atom(false); diff --git a/src/lib/type.ts b/src/lib/type.ts new file mode 100644 index 0000000..29d73ac --- /dev/null +++ b/src/lib/type.ts @@ -0,0 +1,4 @@ +import { ComponentPropsWithoutRef, ElementType } from "react"; + +export type WithoutClassName = Omit; +export type ComponentPropsWithoutRefAndClassName = WithoutClassName>;