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); +## 목차 + +## 추상화하기 위한 요구조건 + 우리는 **모든** 상품 목록을 가져오고 싶습니다. 그리고 여기, 상품 목록을 가져오는 API가 있습니다. 이 API의 사용법은 다음과 같습니다. - 한 번에 최대 100개까지만 가져올 수 있습니다. (`limit=100` 으로 고정이라 가정합니다) diff --git a/src/common/globals.css b/src/common/globals.css index 9e271e03..46a1a2d4 100644 --- a/src/common/globals.css +++ b/src/common/globals.css @@ -145,3 +145,23 @@ .ch-scrollycoding-step-content { @apply border-gray-200; } + +article .heading-anchor { + @apply absolute text-gray-300 -left-6 sm:-left-7 top-1 w-6 h-6 p-1; +} + +h2:has(.heading-anchor), +h3:has(.heading-anchor), +h4:has(.heading-anchor), +h5:has(.heading-anchor), +h6:has(.heading-anchor) { + @apply relative ml-2 sm:ml-0; +} + +article .heading-anchor:hover { + @apply text-inherit; +} + +article .heading-anchor svg { + @apply w-4 h-4; +} diff --git a/src/modules/article/block-components.tsx b/src/modules/article/block-components.tsx index e15991dd..8d90dd9f 100644 --- a/src/modules/article/block-components.tsx +++ b/src/modules/article/block-components.tsx @@ -1,35 +1,61 @@ +import type { ComponentProps } from "react"; +import { twMerge } from "tailwind-merge"; import style from "./style.module.css"; -export function Blockquote({ children }: { children?: React.ReactNode }) { - return

{children}

; -} - -export function Heading2({ children }: { children?: React.ReactNode }) { - return

{children}

; +export function Heading2({ children, ...props }: ComponentProps<"h2">) { + return ( +

+ {children} +

+ ); } -export function Heading3({ children }: { children?: React.ReactNode }) { - return

{children}

; +export function Heading3({ children, ...props }: ComponentProps<"h3">) { + return ( +

+ {children} +

+ ); } -export function Heading4({ children }: { children?: React.ReactNode }) { - return

{children}

; +export function Heading4({ children, ...props }: ComponentProps<"h4">) { + return ( +

+ {children} +

+ ); } -export function Heading5({ children }: { children?: React.ReactNode }) { - return
{children}
; +export function Heading5({ children, ...props }: ComponentProps<"h5">) { + return ( +
+ {children} +
+ ); } -export function Heading6({ children }: { children?: React.ReactNode }) { - return
{children}
; +export function Heading6({ children, ...props }: ComponentProps<"h6">) { + return ( +
+ {children} +
+ ); } -export function Paragraph({ children }: { children?: React.ReactNode }) { - return

{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 ( +
    + {children} +
+ ); } -export function UnorderdList({ children }: { children?: React.ReactNode }) { - return ; +export function UnorderdList({ + children, + ...props +}: React.ComponentProps<"ul">) { + return ( + + ); } -export function ListItem({ children }: { children?: React.ReactNode }) { - return
  • {children}
  • ; +export function ListItem({ children, ...props }: React.ComponentProps<"li">) { + return ( +
  • + {children} +
  • + ); } diff --git a/src/modules/article/style.module.css b/src/modules/article/style.module.css index 82f1b32c..8d36a4a1 100644 --- a/src/modules/article/style.module.css +++ b/src/modules/article/style.module.css @@ -44,7 +44,7 @@ } .heading { - @apply font-semibold mt-[1.5em] mb-[0.5em]; + @apply font-semibold mt-[1.5em] mb-[0.5em] relative; } .heading1 {