Skip to content

feat: heading anchor links (permalink icons on hover) #20

@engineervix

Description

@engineervix

Summary

Headings in rendered pages already receive id attributes via goldmark's parser.WithAutoHeadingID(), so fragment URLs like /guide/configuration/#build-options already work. What's missing is the UX affordance: a clickable anchor link (the # icon) that appears when hovering over a heading, letting readers copy or share a direct link to any section.

Desired behaviour

  • On hover, a # (or link icon) appears inline with h2 and h3 headings in the page body
  • Clicking it navigates to #<heading-id> and can be copied from the address bar
  • Should also apply to h4 if present
  • Implementation should be pure CSS — no JS required

Implementation sketch

CSS approach (no JS, no template changes needed)

.prose h2,
.prose h3,
.prose h4 {
  position: relative;
}

.prose h2 > a.heading-anchor,
.prose h3 > a.heading-anchor,
.prose h4 > a.heading-anchor {
  opacity: 0;
  margin-left: 0.4em;
  font-weight: 400;
  color: var(--color-muted);
  text-decoration: none;
  transition: opacity 0.15s;
}

.prose h2:hover > a.heading-anchor,
.prose h3:hover > a.heading-anchor,
.prose h4:hover > a.heading-anchor {
  opacity: 1;
}

Go: custom goldmark renderer

The anchor <a> element needs to be injected into the rendered HTML. The cleanest approach is a custom goldmark NodeRenderer that wraps the default heading renderer and appends the anchor element. Something like:

// In internal/parser/anchors.go
// HeadingAnchorRenderer wraps default heading rendering and appends
// <a class="heading-anchor" href="#<id>">#</a> to h2–h4 elements.

Alternatively, a post-render string replacement pass on the HTML output would work but is less elegant.

Acceptance criteria

  • h2, h3, h4 headings in .prose get <a class="heading-anchor" href="#<id>">#</a> injected
  • Anchor is hidden by default, visible on heading hover (CSS only)
  • Anchor uses the same id already generated by WithAutoHeadingID()
  • h1 is excluded (the page title heading is not a fragment target)
  • Works in both light and dark mode
  • No JS required

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions