diff --git a/next.config.mjs b/next.config.mjs
index 56fdff73..4ce75b1f 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -2,7 +2,10 @@ import remarkCustomContainer from "@echoja/remark-custom-container";
import bundleAnalyzer from "@next/bundle-analyzer";
import mdx from "@next/mdx";
import { remarkCodeHike } from "codehike/mdx";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
+import remarkToc from "remark-toc";
/** @type {import('codehike/mdx').CodeHikeConfig} */
const chConfig = {
@@ -106,10 +109,57 @@ const customContainerOptions = {
const withMDX = mdx({
options: {
jsx: true,
+
remarkPlugins: [
[remarkCustomContainer, customContainerOptions],
remarkGfm,
[remarkCodeHike, chConfig],
+ [remarkToc, { heading: "목차" }],
+ ],
+ rehypePlugins: [
+ rehypeSlug,
+ [
+ rehypeAutolinkHeadings,
+ {
+ properties: {
+ ariaHidden: true,
+ tabIndex: -1,
+ className: "heading-anchor",
+ },
+ content: {
+ type: "element",
+ tagName: "svg",
+ properties: {
+ className: "icon-link",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "24",
+ height: "24",
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ "stroke-width": "2",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "path",
+ properties: {
+ d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
+ },
+ },
+ {
+ type: "element",
+ tagName: "path",
+ properties: {
+ d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
+ },
+ },
+ ],
+ },
+ },
+ ],
],
},
});
diff --git a/package.json b/package.json
index 45ce564c..6df48b49 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,10 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-wrap-balancer": "^1.1.1",
+ "rehype-autolink-headings": "^7.1.0",
+ "rehype-slug": "^6.0.0",
"remark-gfm": "4.0.0",
+ "remark-toc": "^9.0.0",
"sharp": "^0.33.5",
"tailwind-merge": "^2.6.0",
"vite": "^6.0.6"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0464c872..86986392 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -74,9 +74,18 @@ importers:
react-wrap-balancer:
specifier: ^1.1.1
version: 1.1.1(react@19.0.0)
+ rehype-autolink-headings:
+ specifier: ^7.1.0
+ version: 7.1.0
+ rehype-slug:
+ specifier: ^6.0.0
+ version: 6.0.0
remark-gfm:
specifier: 4.0.0
version: 4.0.0
+ remark-toc:
+ specifier: ^9.0.0
+ version: 9.0.0
sharp:
specifier: ^0.33.5
version: 0.33.5
@@ -1997,6 +2006,9 @@ packages:
'@types/supports-color@8.1.3':
resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==}
+ '@types/ungap__structured-clone@1.2.0':
+ resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
+
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -2927,6 +2939,9 @@ packages:
get-tsconfig@4.8.1:
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -3002,12 +3017,21 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ hast-util-heading-rank@3.0.0:
+ resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
+
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
hast-util-to-estree@3.1.0:
resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==}
hast-util-to-jsx-runtime@2.3.2:
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
+ hast-util-to-string@3.0.1:
+ resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
+
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
@@ -3424,6 +3448,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ mdast-util-toc@7.1.0:
+ resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -3917,9 +3944,15 @@ packages:
resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==}
engines: {node: '>= 0.4'}
+ rehype-autolink-headings@7.1.0:
+ resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
+
rehype-recma@1.0.0:
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+ rehype-slug@6.0.0:
+ resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
+
remark-gfm@4.0.0:
resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==}
@@ -3935,6 +3968,9 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+ remark-toc@9.0.0:
+ resolution: {integrity: sha512-KJ9txbo33GjDAV1baHFze7ij4G8c7SGYoY8Kzsm2gzFpbhL/bSoVpMMzGa3vrNDSWASNd/3ppAqL7cP2zD6JIA==}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -7178,6 +7214,8 @@ snapshots:
'@types/supports-color@8.1.3': {}
+ '@types/ungap__structured-clone@1.2.0': {}
+
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
@@ -8459,6 +8497,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
+ github-slugger@2.0.0: {}
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -8536,6 +8576,14 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-heading-rank@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
hast-util-to-estree@3.1.0:
dependencies:
'@types/estree': 1.0.6
@@ -8577,6 +8625,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ hast-util-to-string@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -9080,6 +9132,16 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ mdast-util-toc@7.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/ungap__structured-clone': 1.2.0
+ '@ungap/structured-clone': 1.2.1
+ github-slugger: 2.0.0
+ mdast-util-to-string: 4.0.0
+ unist-util-is: 6.0.0
+ unist-util-visit: 5.0.0
+
merge-stream@2.0.0:
optional: true
@@ -9752,6 +9814,15 @@ snapshots:
es-errors: 1.3.0
set-function-name: 2.0.2
+ rehype-autolink-headings@7.1.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@ungap/structured-clone': 1.2.1
+ hast-util-heading-rank: 3.0.0
+ hast-util-is-element: 3.0.0
+ unified: 11.0.5
+ unist-util-visit: 5.0.0
+
rehype-recma@1.0.0:
dependencies:
'@types/estree': 1.0.6
@@ -9760,6 +9831,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rehype-slug@6.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ github-slugger: 2.0.0
+ hast-util-heading-rank: 3.0.0
+ hast-util-to-string: 3.0.1
+ unist-util-visit: 5.0.0
+
remark-gfm@4.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -9801,6 +9880,11 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
+ remark-toc@9.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-toc: 7.1.0
+
require-from-string@2.0.2:
optional: true
diff --git a/src/app/article/2024-12/functional/page.mdx b/src/app/article/2024-12/functional/page.mdx
index 9b7e88df..8feeed4e 100644
--- a/src/app/article/2024-12/functional/page.mdx
+++ b/src/app/article/2024-12/functional/page.mdx
@@ -17,6 +17,10 @@ export const metadata = getArticleMetadata(item);
{children}
; +export function Paragraph({ children, ...props }: ComponentProps<"p">) { + return ( ++ {children} +
+ ); } -export function Quote({ children }: { children?: React.ReactNode }) { - return{children}; +export function Quote({ children, ...props }: ComponentProps<"blockquote">) { + return ( +
+ {children} ++ ); } export function Table({ children }: { children?: React.ReactNode }) { diff --git a/src/modules/article/format-components.tsx b/src/modules/article/format-components.tsx index ddc8213b..21cf8ddf 100644 --- a/src/modules/article/format-components.tsx +++ b/src/modules/article/format-components.tsx @@ -136,15 +136,14 @@ export function Strong({ children }: { children?: React.ReactNode }) { export function Anchor({ children, href, -}: { - children?: React.ReactNode; - href?: string; -}) { - const isInternal = href?.startsWith("/article"); + ...restProps +}: React.ComponentProps<"a">) { + const isInternal = href?.startsWith("/article") || href?.startsWith("#"); return ( {children}; +export function OrderedList({ + children, + ...props +}: React.ComponentProps<"ol">) { + return ( +