Skip to content

fix(content): strip link affordance from self-referencing links#16935

Open
ethangui wants to merge 1 commit into
PostHog:masterfrom
ethangui:fix/strip-self-referencing-links-in-docs-content
Open

fix(content): strip link affordance from self-referencing links#16935
ethangui wants to merge 1 commit into
PostHog:masterfrom
ethangui:fix/strip-self-referencing-links-in-docs-content

Conversation

@ethangui
Copy link
Copy Markdown

@ethangui ethangui commented May 20, 2026

The user experience

A reader on /docs/migrate/managed-migrations opens the page. The first thing they see is a warning callout. The last sentence in step 3 reads: "This is automated if you are running a managed migration." That phrase is underlined and styled like every other link. They click it. Nothing happens — the link points to the page they're already on.

The same thing happens in /blog/posthog-vs-heap at the FAQ accordion ("Heap" → /blog/posthog-vs-heap), in /handbook/brand/art-requests mid-paragraph ("request one from the design team" → /handbook/brand/art-requests), and in dozens of other places.

The problem

Self-referencing links — <a href> values that resolve to the current page — appear in ~13 authored places across the site plus another ~110 emitted by one component, from four distinct root causes:

  • Class A (4) — Shared MDX snippets contain absolute links to their canonical doc. The link is useful when the snippet is imported on another page; on the canonical page itself it becomes a self-link.
  • Class B (7) — Blog comparison and "best alternatives" articles contain rollup bullets at the bottom where the author pasted the current article's URL instead of the intended sibling URL.
  • Class C (2) — Direct prose self-links in docs/handbook content. Plain authoring oversight.
  • Class D (~110 SSR HTML occurrences)ProductComparisonTable detects the current page from window.location.pathname. SSR has no window, so during static render the "compare" link for the current product ships in the HTML and gets removed only after client hydration. Visible to non-JS consumers (Slack/Discord unfurlers, Bing crawler, archive.org, the pre-hydration flash on slow connections).

All four look identical to the reader: a clickable-looking link that does nothing.

Reference: how Wikipedia handles this

MediaWiki has had a convention for ~20 years (Wikipedia:Self link): when wikitext like [[Heap]] appears on the Heap article, the platform automatically renders it as bold body text, not a link. The reader sees that the phrase is meaningful (bold) but un-clickable (no underline, no pointer cursor) — which signals "you are here" without requiring the reader to recognize a custom convention.

Wikipedia's Manual of Style additionally discourages authors from writing self-links on purpose, but the auto-strip exists as a safety net for accidental ones.

The fix

Two changes that together remove both the visual and the keyboard affordance of self-referencing links inside .reader-content-container (the wrapper around all MDX content in the Reader app).

1. CSS rule in src/styles/global.css

Strips the visual link affordance from anchors that have aria-current="page". The bold weight stays because it's already inherited from surrounding markup (<strong> wrappers in markdown bullets, container-level [&_a]:font-semibold rules in blockquotes).

.reader-content-container a[aria-current="page"] {
    text-decoration: none;
    pointer-events: none;
    color: inherit;
    cursor: default;
}

Why this works without JavaScript or template changes: Gatsby's Link wraps @gatsbyjs/reach-router's Link, which automatically sets aria-current="page" on links whose href matches the current pathname. The attribute is already in the SSR HTML — we just style against it.

2. Keyboard a11y useEffect in src/components/Wrapper

CSS alone can hide a link visually but can't neutralize the underlying <a href>. Without this companion, keyboard users would tab to the now-invisible link and could activate it via Enter — a WCAG 2.4.7 (focus visible) regression for the population this PR aims to help.

The effect runs after every Wrapper render and sets tabindex="-1" and aria-disabled="true" on each .reader-content-container a[aria-current="page"]. Idempotent set-attribute calls; cheap.

Effect on each class

  • Class A (4) — Fixed. Snippet still imports correctly on its non-canonical hosts (where the link is useful); on the canonical page it renders as plain bold text.
  • Class B (7) — Each authored slip renders as plain text instead of a dead link. A follow-up content PR can route specific ones to their intended sibling URL (e.g., [Heap] should likely point to /blog/best-heap-alternatives). Once the URL is corrected, the link no longer matches aria-current="page" and this rule stops applying — the link works normally again.
  • Class C (2) — Same as B.
  • Class D (~110) — The <a> tags still ship in SSR HTML, but they render as plain text. Post-hydration the component removes them entirely. Pre/post-hydration are now visually identical.

False positives correctly preserved

  • In-page anchors/docs/surveys/installation/flutter contains [PostHogObserver](/docs/surveys/installation/flutter#step-two-install-posthogobserver). The #anchor makes the href != pathname so aria-current="page" is not set; the link stays clickable.
  • Child-page links/docs/advanced/proxy contains [self-hosted proxy reference](/docs/advanced/proxy/proxy-reference). Different path; no aria-current="page"; preserved.

What this doesn't touch

  • Nav, breadcrumb, sidebar, footer, and any chrome outside .reader-content-container. The site's existing active-link styling in those components is unchanged.
  • DOM semantics. Screen readers still announce the anchor as "current page link" — the W3C-recommended pattern. Only the visual link affordance and keyboard interaction are removed.

Review feedback addressed

  • Codex bot review (keyboard focus regression): addressed by the Wrapper useEffect.
  • Local code review (safelist line was likely a no-op): empirically verified with a clean rebuild — the rule survives without the safelist entry. The safelist change has been removed.
  • Local comment review (5 nits on the CSS comment): comment rewritten to reference @gatsbyjs/reach-router, name the scope precisely, flag the pointer-events: none side effects on context menu / glossary / hover prefetch, soften the anchor/child-page guarantee to a tripwire for future router changes, and cross-reference the Wrapper effect.

Verification

Ran pnpm build:minimal and inspected the rendered HTML for:

  • All four Class A canonical pages → self-link has aria-current="page", renders as plain text in browser
  • Two representative Class B pages → same
  • Both Class C pages → same
  • /blog/posthog-vs-heap with JS disabled → Class D "compare" links present in HTML but render as plain text
  • /docs/surveys/installation/flutter → in-page anchor link preserved
  • /docs/advanced/proxy → child-page link preserved

Also rebuilt from a cleaned cache without the safelist entry to confirm the CSS rule survives Tailwind's PurgeCSS pass on its own.

Checklist

  • I've read the docs style guide
  • American English
  • Relative URLs (n/a — CSS / TSX only)
  • Vercel preview verified
  • Redirect added if moved (n/a)

@ethangui ethangui force-pushed the fix/strip-self-referencing-links-in-docs-content branch from 37809f8 to d4f01d8 Compare May 20, 2026 15:42
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37809f8840

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/styles/global.css
@ethangui ethangui force-pushed the fix/strip-self-referencing-links-in-docs-content branch from d4f01d8 to 561cff9 Compare May 20, 2026 17:36
Inside the Reader app's content container (the .reader-content-container
scope), any link whose href resolves to the current page now renders as
plain text instead of an underlined click target.

Mechanism:
- Gatsby Link (via @gatsbyjs/reach-router) auto-sets aria-current="page"
  on anchors whose href matches the current pathname.
- A CSS rule in global.css strips the link affordance from those anchors.
- A companion useEffect in components/Wrapper sets tabindex="-1" and
  aria-disabled="true" so keyboard users don't land on invisible focus
  stops or activate the no-op link via Enter.

In-page anchor links (/foo#section on /foo) and child-page links
(/foo/bar on /foo) don't receive aria-current, so they're preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ethangui ethangui force-pushed the fix/strip-self-referencing-links-in-docs-content branch from 561cff9 to fc8f7db Compare May 20, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant