Skip to content

Commit

Permalink
Add: Add TOC
Browse files Browse the repository at this point in the history
  • Loading branch information
Hayao0819 committed Jul 7, 2024
1 parent 165b11c commit d95dcc3
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 47 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion src/app/(hayao)/blog/posts/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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("/"));

Expand Down Expand Up @@ -125,7 +129,10 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
</BlogHeading>
<div className="text-center">{dateToString(postDate)}</div>
</div>
<div className="grow">{postData.parsed}</div>
<Toc contentSelector={`#${contentId}`} className="border-y border-accent p-4" />
<div className="grow" id={contentId}>
{postData.parsed}
</div>

<div className="mt-4 h-fit border-t-2 border-secondary/15 pt-4">
<ShareCurrentURL text={postData.post.meta.title} />
Expand Down
95 changes: 65 additions & 30 deletions src/components/elements/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Heading level={1}>{children}</Heading>;
h1: ({ children, id }) => {
return (
<Heading id={id} level={1}>
{children}
</Heading>
);
},

h2: ({ children }) => {
return <Heading level={2}>{children}</Heading>;
h2: ({ children, id }) => {
return (
<Heading id={id} level={2}>
{children}
</Heading>
);
},
h3: ({ children }) => {
return <Heading level={3}>{children}</Heading>;
h3: ({ children, id }) => {
return (
<Heading id={id} level={3}>
{children}
</Heading>
);
},
h4: ({ children }) => {
return <Heading level={4}>{children}</Heading>;
h4: ({ children, id }) => {
return (
<Heading id={id} level={4}>
{children}
</Heading>
);
},
h5: ({ children }) => {
return <Heading level={5}>{children}</Heading>;
h5: ({ children, id }) => {
return (
<Heading id={id} level={5}>
{children}
</Heading>
);
},
a: ({ href, children }) => {
a: ({ href, children, id }) => {
if (!href) return <span>{children}</span>;
return (
<Link href={href} className=" text-blue-900">
<Link href={href} id={id} className=" text-blue-900">
{children}
</Link>
);
},
p: ({ children }) => (
p: ({ children, id }) => (
<p
// // @ts-expect-error word-breakでauto-phraseを使うための型定義がない
// style={{ wordBreak: "auto-phrase" }}
className="py-2 leading-6"
id={id}
>
{children}
</p>
),

Tweet: ({ id }: { id: string }) => {
return <Tweet id={id} />;
},
Tweet: Tweet,
img: (props) => {
let src = props.src;
if (!src?.startsWith("http")) {
Expand All @@ -62,31 +86,36 @@ export default async function Markdown({ content, basepath }: { content: string;
},
//code: ({ children }) => <code className="text-sky-600">{children}</code>,

ul: ({ children }) => <ul className="list-disc pl-8">{children}</ul>,
ul: ({ children, id }) => (
<ul id={id} className="list-disc pl-8">
{children}
</ul>
),

//pre: ({ children, className }) => <pre className={classNames(className, "p-2")}>{children}</pre>,

Flex: ({ children }: { children: ReactNode }) => {
return <div className="mx-auto flex flex-wrap justify-center">{children}</div>;
Flex: ({ children, id, ...props }: ComponentPropsWithoutRefAndClassName<"div">) => {
return (
<div id={id} {...props} className="mx-auto flex flex-wrap justify-center">
{children}
</div>
);
},

Grid: ({ children, col }: { children: ReactNode; col: number }) => {
Grid: ({ children, col, id, ...props }: Omit<ComponentPropsWithoutRef<"div">, "className"> & { col: number }) => {
return (
<div className="mx-auto grid justify-center" style={{ gridTemplateColumns: `repeat(${col}, minmax(0, 1fr))` }}>
<div
className="mx-auto grid justify-center"
id={id}
style={{ gridTemplateColumns: `repeat(${col}, minmax(0, 1fr))` }}
{...props}
>
{children}
</div>
);
},
};

/* だるいエラーについて
https://github.com/hashicorp/next-mdx-remote/issues/403
現在next-mdx-remoteはremarkGfm 4.0.0をサポートしていないため、3.0.1を使う必要がある
*/

return (
<div>
<MDXRemote
Expand All @@ -95,6 +124,12 @@ export default async function Markdown({ content, basepath }: { content: string;
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeSlug,
{
prefix: "",
},
],
rehypeCodeTitles,
[
rehypePrettyCode,
Expand Down
29 changes: 29 additions & 0 deletions src/components/elements/Toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { ComponentPropsWithoutRef, useEffect } from "react";
import * as tocbot from "tocbot";

import useNoColonId from "@/hooks/useNoColonId";

interface TocProps extends ComponentPropsWithoutRef<"div"> {
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 <div id={id} {...props}></div>;
};

export default Toc;
14 changes: 11 additions & 3 deletions src/components/elements/Tweet.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"use client";
import { Tweet as TweetWidget } from "react-twitter-widgets";
export default function Tweet({ id }: { id: string }) {
return <TweetWidget tweetId={id} />;
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 (
<div {...props}>
<TweetWidget tweetId={tweetId} options={options} onLoad={onLoad} renderError={renderError} />;
</div>
);
}
5 changes: 2 additions & 3 deletions src/components/layouts/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = () => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/layouts/Drawer/DrawerToggle.tsx
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
6 changes: 2 additions & 4 deletions src/components/layouts/Drawer/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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];
};
6 changes: 6 additions & 0 deletions src/hooks/useDrawerAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atom, useAtom } from "jotai";

const drawerAtom = atom(false);

const useDrawerAtom = () => useAtom(drawerAtom);
export default useDrawerAtom;
9 changes: 9 additions & 0 deletions src/hooks/useNoColonId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useId } from "react";

const useNoColonId = () => {
const id = useId();
// 先頭と末尾のコロンを削除
return id.slice(1, -1);
};

export default useNoColonId;
3 changes: 0 additions & 3 deletions src/lib/atom.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/lib/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ComponentPropsWithoutRef, ElementType } from "react";

export type WithoutClassName<T> = Omit<T, "className">;
export type ComponentPropsWithoutRefAndClassName<T extends ElementType> = WithoutClassName<ComponentPropsWithoutRef<T>>;

0 comments on commit d95dcc3

Please sign in to comment.