Make comment headings into anchor links

main
ef65cbcfix: support double-dash --ref and --version flags in remote-bash CLI (#134)
This post might have stale content, as main is 5 commits ahead.

make it so headings in comments' content be anchor links. watch out for common mistakes:

  1. 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
  2. watch out for the comment headers being sticky
Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

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):

  1. extractText() — Recursively extracts plain text from React children nodes (handles strings, numbers, nested elements, arrays)
  2. slugify() — Converts heading text to URL-safe slugs (Getting Startedgetting-started)
  3. Heading component — Updated to:
    • Accept an id prop for the anchor
    • Render heading content wrapped in an <a> link when id is present
    • Added scroll-mt-10 class to offset past the sticky comment header when scrolling to anchor
    • Show a # indicator on hover (via group-hover/heading with opacity transition)
    • On click: update URL hash via replaceState + smooth scroll to element
  4. 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 SlugCounter map: first → 2-title, second → 2-title-1, etc.
  5. useStreamdownComponents() — Hook replacing the static streamdownComponents object:
    • Uses useRef for the slug counter so it persists across renders
    • Clears the counter on every render for deterministic IDs
    • Returns memoized components keyed on commentNumber

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 commentNumber is 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")
Avatar of julianbenegas
Julian Benegas
commented

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:   31 |     )
17:59:28.244 forums:build:   32 |   }
17:59:28.244 forums:build: > 33 |   if (Array.isArray(node)) return node.map(extractText).join("")
17:59:28.244 forums:build:      |                                            ^
17:59:28.244 forums:build:   34 |   const arr: ReactNode[] = []
17:59:28.244 forums:build:   35 |   Children.forEach(node, (child: ReactNode) => arr.push(child))
17:59:28.244 forums:build:   36 |   return arr.map(extractText).join("")
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
Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

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[].

Avatar of julianbenegas
Julian Benegas
commented

don't add the additional "#" - and make the existing ## also clickable. underline on hover is enough

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

Pushed. Changes:

  • Removed the extra # indicator that appeared on hover
  • Made the ## prefix clickable — it's now an <a> tag with hover:underline instead 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-id fallback keeps the original static <span> rendering
Avatar of julianbenegas
Julian Benegas
commented

merge it!

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

Merged! ✅


END OF POST

Make comment headings into anchor links — basehub-ai/forums