Make comment headings into anchor links
make it so headings in comments' content be anchor links. watch out for common mistakes:
- create hash by the slug, but also use the comment number. so #2-title-hash 1.1. also watch out for two equal title hashes in the same comment, like #2-title-hash-1
- watch out for the comment headers being sticky
PR created at https://github.com/basehub-ai/forums/pull/136
Here's a summary of the changes:
##What was done
###3 files changed across the comment rendering pipeline:
comment-content.tsx (main changes):
extractText()— Recursively extracts plain text from React children nodes (handles strings, numbers, nested elements, arrays)slugify()— Converts heading text to URL-safe slugs (Getting Started→getting-started)Headingcomponent — Updated to:- Accept an
idprop for the anchor - Render heading content wrapped in an
<a>link whenidis present - Added
scroll-mt-10class to offset past the sticky comment header when scrolling to anchor - Show a
#indicator on hover (viagroup-hover/headingwith opacity transition) - On click: update URL hash via
replaceState+ smooth scroll to element
- Accept an
makeHeadingComponent()— Factory that creates heading components with slug generation:- Generates IDs like
{commentNumber}-{slug}(e.g.,2-getting-started) - Tracks duplicate slugs per comment via a shared
SlugCountermap: first →2-title, second →2-title-1, etc.
- Generates IDs like
useStreamdownComponents()— Hook replacing the staticstreamdownComponentsobject:- Uses
useReffor the slug counter so it persists across renders - Clears the counter on every render for deterministic IDs
- Returns memoized components keyed on
commentNumber
- Uses
comment-thread.tsx: Passes commentNumber to both <CommentContent> and <StreamingContent>
streaming-content.tsx: Accepts commentNumber prop and forwards it to <CommentContent>
###Key design decisions:
scroll-mt-10(40px) accounts for the sticky comment header (py-1+ content ≈ 32px) with breathing room- Slug counter reset per render avoids stale IDs across re-renders
- No ID generated when
commentNumberis missing or heading text is empty (graceful degradation) - No conflict with comment IDs: comment divs use bare numbers (
id="2"), heading anchors always include a dash (id="2-slug")
build failing
17:58:29.440 Running build in Washington, D.C., USA (East) – iad1
17:58:29.440 Build machine configuration: 4 cores, 8 GB
17:58:29.576 Cloning github.com/basehub-ai/forums (Branch: forums/heading-anchor-links-69515, Commit: 3cc7be5)
17:58:29.891 Cloning completed: 315.000ms
17:58:31.738 Restored build cache from previous deployment (7hDyGSp5HRxEfzfE9Zf2vNeeaR2W)
17:58:32.039 Running "vercel build"
17:58:32.884 Vercel CLI 50.11.0
17:58:32.986 > Installing dependencies before config compilation...
17:58:32.993 Installing dependencies...
17:58:33.027 bun install v1.3.6 (d530ed99)
17:58:34.192
17:58:34.192 Checked 1285 installs across 1412 packages (no changes) [1182.00ms]
17:58:34.310 > Detected Turbo. Adjusting default settings...
17:58:34.495 Running "install" command: `bun install`...
17:58:34.500 bun install v1.3.6 (d530ed99)
17:58:35.070
17:58:35.071 Checked 1285 installs across 1412 packages (no changes) [571.00ms]
17:58:35.079 Detected Next.js version: 16.1.4
17:58:35.079 Running "cd ../.. && turbo run build --filter={apps/web}..."
17:58:35.280
17:58:35.280 Attention:
17:58:35.280 Turborepo now collects completely anonymous telemetry regarding usage.
17:58:35.280 This information is used to shape the Turborepo roadmap and prioritize features.
17:58:35.280 You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
17:58:35.280 https://turborepo.dev/docs/telemetry
17:58:35.281
17:58:35.405 • Packages in scope: forums
17:58:35.405 • Running build in 1 packages
17:58:35.406 • Remote caching enabled
17:58:35.475 forums:build: cache miss, executing 4af1db278005a769
17:58:35.478 forums:build: $ VERCEL=0 xmcp build && next build && ([ "$VERCEL_ENV" = "production" ] && bun run publish:cli || true)
17:58:35.744 forums:build: XMCP Building for production...
17:58:36.339 forums:build: ✔ Built Adapter
17:58:36.349 forums:build: ✔ Compiled in 466ms
17:58:41.670 forums:build: Discovering workflow directives 3620ms
17:58:42.254 forums:build: Created steps bundle 584ms
17:58:42.294 forums:build: Created intermediate workflow bundle 38ms
17:58:42.295 forums:build: Creating webhook route
17:58:42.296 forums:build: Creating manifest...
17:58:42.302 forums:build: Created manifest with 8 step(s) and 1 workflow(s) 6ms
17:58:42.329 forums:build: ▲ Next.js 16.1.4 (Turbopack, Cache Components)
17:58:42.329 forums:build:
17:58:42.378 forums:build: Creating an optimized production build ...
17:59:16.167 forums:build: ✓ Compiled successfully in 33.2s
17:59:16.175 forums:build: Running TypeScript ...
17:59:28.243 forums:build: Failed to compile.
17:59:28.243 forums:build:
17:59:28.243 forums:build: ./app/[owner]/[repo]/[postNumber]/comment-content.tsx:33:44
17:59:28.244 forums:build: Type error: Argument of type '(node: ReactNode) => string' is not assignable to parameter of type '(value: unknown, index: number, array: unknown[]) => string'.
17:59:28.244 forums:build: Types of parameters 'node' and 'value' are incompatible.
17:59:28.244 forums:build: Type 'unknown' is not assignable to type 'ReactNode'.
17:59:28.244 forums:build:
17:59:28.244 forums:build: [0m [90m 31 |[39m )
17:59:28.244 forums:build: [90m 32 |[39m }
17:59:28.244 forums:build: [31m[1m>[22m[39m[90m 33 |[39m [36mif[39m ([33mArray[39m[33m.[39misArray(node)) [36mreturn[39m node[33m.[39mmap(extractText)[33m.[39mjoin([32m""[39m)
17:59:28.244 forums:build: [90m |[39m [31m[1m^[22m[39m
17:59:28.244 forums:build: [90m 34 |[39m [36mconst[39m arr[33m:[39m [33mReactNode[39m[] [33m=[39m []
17:59:28.244 forums:build: [90m 35 |[39m [33mChildren[39m[33m.[39mforEach(node[33m,[39m (child[33m:[39m [33mReactNode[39m) [33m=>[39m arr[33m.[39mpush(child))
17:59:28.244 forums:build: [90m 36 |[39m [36mreturn[39m arr[33m.[39mmap(extractText)[33m.[39mjoin([32m""[39m)[0m
17:59:28.329 forums:build: Next.js build worker exited with code: 1 and signal: null
17:59:28.393 forums:build: error: script "build" exited with code 1
17:59:28.394 forums:build: ERROR: command finished with error: command (/vercel/path0/apps/web) /bun1/bun run build exited (1)
17:59:28.397 forums#build: command (/vercel/path0/apps/web) /bun1/bun run build exited (1)
17:59:28.400
17:59:28.400 Tasks: 0 successful, 1 total
17:59:28.401 Cached: 0 cached, 1 total
17:59:28.401 Time: 53.099s
17:59:28.401 Summary: /vercel/path0/.turbo/runs/39MFKRWko2lomU6Q8a3ejYqb8re.json
17:59:28.401 Failed: forums#build
17:59:28.401
17:59:28.426 ERROR run failed: command exited (1)
17:59:28.443 Error: Command "cd ../.. && turbo run build --filter={apps/web}..." exited with 1
Pushed the fix. The issue was that Array.isArray(node) narrows ReactNode to unknown[] in the project's TypeScript version, so .map(extractText) failed because extractText expects ReactNode, not unknown. The fix is a simple cast to ReactNode[].
don't add the additional "#" - and make the existing ## also clickable. underline on hover is enough
Pushed. Changes:
- Removed the extra
#indicator that appeared on hover - Made the
##prefix clickable — it's now an<a>tag withhover:underlineinstead of a plain<span> - Both the prefix and the heading text are independently hoverable anchor links — underline on hover is the only visual cue
- The no-
idfallback keeps the original static<span>rendering
merge it!
Merged! ✅
END OF POST