Add anchor link support to markdown headers

This commit is contained in:
Jared Schoeny 2025-10-27 20:32:44 -10:00
parent bbd2f56797
commit 27f920b905
7 changed files with 80 additions and 4 deletions

50
package-lock.json generated
View File

@ -22,6 +22,7 @@
"react-dom": "19.1.0",
"react-icons": "^5.5.0",
"react-markdown": "9.0.3",
"rehype-slug": "^6.0.0",
"remark-gfm": "4.0.0",
"rom-patcher-js": "github:marcrobledo/RomPatcher.js#91e522e247f709e894761157ccba3189004d0859"
},
@ -2115,6 +2116,12 @@
"node": ">= 0.4"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -2194,6 +2201,19 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-heading-rank": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz",
"integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@ -2221,6 +2241,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-string": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
"integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@ -4092,6 +4125,23 @@
"node": ">= 6"
}
},
"node_modules/rehype-slug": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz",
"integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"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"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz",

View File

@ -23,6 +23,7 @@
"react-dom": "19.1.0",
"react-icons": "^5.5.0",
"react-markdown": "9.0.3",
"rehype-slug": "^6.0.0",
"remark-gfm": "4.0.0",
"rom-patcher-js": "github:marcrobledo/RomPatcher.js#91e522e247f709e894761157ccba3189004d0859"
},

View File

@ -75,6 +75,22 @@ body {
font-family: Arial, Helvetica, sans-serif;
}
/* Smooth scrolling and anchor offset for sticky header */
html { scroll-behavior: smooth; }
:root { --anchor-offset: 38px; }
@media (min-width: 768px) { :root { --anchor-offset: 72px; } }
/* Modern browsers honor scroll-padding-top on the scroll container */
html { scroll-padding-top: var(--anchor-offset); }
/* Fallback for elements targeted directly (especially headings) */
.prose h1[id],
.prose h2[id],
.prose h3[id],
.prose h4[id],
.prose h5[id],
.prose h6[id] {
scroll-margin-top: var(--anchor-offset);
}
.bg-grid { background-image: radial-gradient(var(--grid-dot-color) 1px, transparent 1px); background-size: 22px 22px; }
.card {

View File

@ -4,6 +4,7 @@ import Gallery from "@/components/Hack/Gallery";
import HackActions from "@/components/Hack/HackActions";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import Image from "next/image";
import { FaDiscord, FaTwitter } from "react-icons/fa6";
import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon";
@ -127,7 +128,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
<div className="card p-5">
<h2 className="text-xl font-semibold tracking-tight">About this hack</h2>
<div className="prose prose-sm mt-3 max-w-none text-foreground/80">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{hack.description}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{hack.description}</ReactMarkdown>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import TagSelector from "@/components/Submit/TagSelector";
import { baseRoms } from "@/data/baseRoms";
import Image from "next/image";
@ -301,7 +302,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
/>
) : (
<div className={`prose max-w-none rounded-md min-h-[14rem] px-3 py-2 ring-1 ring-inset ${descriptionChanged ? 'ring-[var(--ring)] bg-[var(--surface-2)]' : 'bg-[var(--surface-2)] ring-[var(--border)]'} ${description ? "" : "text-foreground/60 text-sm"}`}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{description || "Nothing to preview yet."}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Nothing to preview yet."}</ReactMarkdown>
</div>
)}
</div>

View File

@ -11,6 +11,7 @@ import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy }
import { CSS } from "@dnd-kit/utilities";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import { RxDragHandleDots2 } from "react-icons/rx";
import { useAuthContext } from "@/contexts/AuthContext";
import { useBaseRoms } from "@/contexts/BaseRomContext";
@ -723,7 +724,7 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
</div>
{isDummy ? (
<div className="prose max-w-none h-36 rounded-md bg-[var(--surface-2)] px-3 py-2 ring-1 ring-inset ring-[var(--border)] text-foreground/60 select-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{description || "Write a longer markdown description here."}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Write a longer markdown description here."}</ReactMarkdown>
</div>
) : !showMdPreview ? (
<textarea
@ -735,7 +736,7 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
/>
) : (
<div className={`prose max-w-none rounded-md bg-[var(--surface-2)] min-h-[14rem] px-3 py-2 ring-1 ring-inset ring-[var(--border)] ${description ? "" : "text-foreground/60 text-sm"}`}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{description || "Nothing to preview yet."}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Nothing to preview yet."}</ReactMarkdown>
</div>
)}
</div>

View File

@ -3,6 +3,7 @@ declare module "react-markdown" {
export interface ReactMarkdownProps {
children?: React.ReactNode;
remarkPlugins?: any[];
rehypePlugins?: any[];
className?: string;
[key: string]: any;
}
@ -15,4 +16,9 @@ declare module "remark-gfm" {
export default remarkGfm;
}
declare module "rehype-slug" {
const rehypeSlug: any;
export default rehypeSlug;
}