Skip to content

Add support for cross-page anchor links#160

Merged
adrianschmidt merged 2 commits intojgroth:mainfrom
adrianschmidt-bot:feat/cross-page-anchor-links
Feb 11, 2026
Merged

Add support for cross-page anchor links#160
adrianschmidt merged 2 commits intojgroth:mainfrom
adrianschmidt-bot:feat/cross-page-anchor-links

Conversation

@adrianschmidt-bot
Copy link
Contributor

@adrianschmidt-bot adrianschmidt-bot commented Feb 9, 2026

Summary

This PR adds support for cross-page anchor links in kompendium documentation, addressing the issue where anchor links don't work due to asynchronous page loading in the SPA.

Changes

1. Auto-generate heading IDs (rehype-slug)

All headings (h1-h6) in markdown content now automatically get an id attribute based on their text content:

  • # Getting Started<h1 id="getting-started">Getting Started</h1>
  • ## What's New in v2.0?<h2 id="whats-new-in-v20">What's New in v2.0?</h2>

2. Scroll to anchor after content renders

The kompendium-markdown component now:

  • Checks for a URL hash after markdown content is rendered
  • Scrolls the matching element into view with smooth scrolling
  • Listens for hashchange events for same-page anchor navigation

Usage

This enables links like:

  • /guide/changelog#v2-features
  • /guide/getting-started#installation

to work correctly even though content loads asynchronously.

Related

fix: #60

Summary by CodeRabbit

  • New Features

    • Headings automatically generate unique, URL-friendly IDs for direct linking
    • Improved in-page navigation with enhanced anchor scrolling for shadow DOM components
    • Hash-based navigation now reliably scrolls to referenced sections after render
  • Tests

    • Added tests for automatic heading ID generation, including handling of special characters

adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 9, 2026
@adrianschmidt

This comment was marked as resolved.

@adrianschmidt-bot adrianschmidt-bot force-pushed the feat/cross-page-anchor-links branch from 6e46e02 to 7b63bba Compare February 9, 2026 14:41
@adrianschmidt-bot

This comment was marked as resolved.

@adrianschmidt

This comment was marked as resolved.

@adrianschmidt-bot

This comment was marked as resolved.

adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 9, 2026
adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 9, 2026
@adrianschmidt adrianschmidt force-pushed the feat/cross-page-anchor-links branch from 60002f0 to ee78408 Compare February 9, 2026 16:01
@adrianschmidt-bot

This comment was marked as resolved.

@adrianschmidt adrianschmidt requested a review from jgroth February 9, 2026 16:44
adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 9, 2026
adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 10, 2026
@adrianschmidt
Copy link
Collaborator

I've updated the test-PR, so the latest version of this PR can now be tested at https://lundalogik.github.io/lime-elements/versions/PR-3843/

Why not try this deep-link to the Reduced Presence example for limel-button?

@adrianschmidt adrianschmidt force-pushed the feat/cross-page-anchor-links branch 2 times, most recently from 70dc1a1 to 3e7810d Compare February 10, 2026 10:40
@adrianschmidt adrianschmidt changed the title feat(markdown): add support for cross-page anchor links Add support for cross-page anchor links Feb 10, 2026
This adds two features to enable cross-page anchor links:

1. Auto-generate heading IDs using rehype-slug
   - All headings (h1-h6) now get an id attribute based on their text
   - IDs are URL-safe slugs (e.g., 'Getting Started' → 'getting-started')

2. Scroll to anchor after content renders
   - After markdown content is rendered, check for URL hash
   - If an anchor is present, scroll the matching element into view
   - Also handles hash changes for same-page navigation

This enables links like /guide/changelog#v2-features to work correctly
in the SPA, where content loads asynchronously.
@adrianschmidt-bot adrianschmidt-bot force-pushed the feat/cross-page-anchor-links branch from 3e7810d to b680725 Compare February 11, 2026 13:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

This PR adds heading slug generation and anchor scrolling: it installs rehype-slug, integrates slugging into the markdown pipeline, introduces shadow-DOM-aware anchor scroll utilities, and wires hashchange-driven scrolling into Markdown and component lifecycle hooks.

Changes

Cohort / File(s) Summary
Dependency
package.json
Added rehype-slug (^4.0.1) to enable automatic id generation for headings.
Anchor scroll utilities
src/components/anchor-scroll.ts
New module providing getRoute, getAnchorId, scrollToAnchor, and scrollToElement for scheduled scrolling inside shadow roots.
Component integration
src/components/component/component.tsx
Replaced internal route/scroll logic with imports from anchor-scroll; removed legacy private helpers and updated route handling to use getRoute().
Markdown component
src/components/markdown/markdown.tsx
Added constructor, render sequencing, hashchange handler registration/unregistration, componentDidUpdate, and calls to scrollToAnchor to scroll after render.
Markdown pipeline
src/kompendium/markdown.ts
Inserted rehypeSlug() into the rehype pipeline (after rehype-raw) to generate heading id attributes.
Tests
src/kompendium/test/markdown.spec.ts
Updated expectations to include heading id attributes and added tests verifying slug generation and normalization for headings.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant Markdown as Markdown Component
    participant Anchor as AnchorScroll Utils
    participant Shadow as Shadow DOM

    User->>Browser: Click link or change hash
    Browser->>Markdown: emit "hashchange"
    Markdown->>Markdown: handleHashChange / render
    Markdown->>Anchor: scrollToAnchor(shadowRoot)
    Anchor->>Anchor: getAnchorId() parse hash
    Anchor->>Browser: requestAnimationFrame
    Browser->>Shadow: querySelector by id in shadowRoot
    Shadow->>Shadow: element.scrollIntoView()
    Shadow->>User: view jumps to anchor
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through slugs and hashes bright,
Anchors now land just right,
From markdown head to shadowed view,
Links find their homes — hip hop, woo! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding support for cross-page anchor links, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR successfully addresses issue #60 by implementing auto-generated heading IDs for all markdown headings using rehype-slug and adding anchor scrolling functionality to enable deep-linking to specific examples within documentation pages.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing anchor link support: adding rehype-slug dependency, creating anchor-scroll utilities, updating component and markdown rendering, and adding tests. No unrelated or out-of-scope changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/components/component/component.tsx`:
- Around line 65-77: Route/hash fragments (e.g., `#/component/x`#heading) are
being passed through getRoute() into scroll targets and IDs, causing mismatches;
update usages in componentDidLoad, componentDidUpdate, and handleRouteChange to
strip any anchor fragment from the string returned by getRoute() before
assigning to scrollToOnNextUpdate or passing to scrollToElement (use a simple
split on '#' or URL parsing), so scrollToElement(this.host.shadowRoot, ...) and
assignments to this.scrollToOnNextUpdate always receive route-only values
without trailing fragments.

In `@src/components/markdown/markdown.tsx`:
- Around line 49-56: renderMarkdown() can produce stale UI because
markdownToHtml(this.text) is awaited and older calls may finish after newer
ones; fix by capturing a render token or snapshot of this.text before awaiting
and aborting update if it no longer matches the latest. Concretely, add a
monotonic renderId or store a local const currentText = this.text at the start
of renderMarkdown(), incrementing or comparing against a this._latestRenderId,
then after awaiting markdownToHtml(this.text, types) verify the
token/currentText still matches this._latestRenderId/this.text; only then assign
to host.shadowRoot.querySelector('#root').innerHTML and call
scrollToAnchor(this.host.shadowRoot). Ensure renderId/_latestRenderId is updated
whenever text changes so outdated async results are ignored.

Extract common anchor scrolling functionality into anchor-scroll.ts:
- getRoute(): get route from hash for hash-based routing
- getAnchorId(): extract anchor fragment from URL
- scrollToAnchor(): scroll to anchor in shadow root
- scrollToElement(): scroll to element by ID in shadow root

Both kompendium-markdown and kompendium-component now use
this shared utility instead of duplicating the logic.
@adrianschmidt-bot adrianschmidt-bot force-pushed the feat/cross-page-anchor-links branch from b680725 to 0716316 Compare February 11, 2026 14:15
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/component/component.tsx`:
- Around line 78-80: handleRouteChange sets the private field
scrollToOnNextUpdate but because that field isn't reactive the component won't
re-render and componentDidUpdate won't run; either mark scrollToOnNextUpdate
with the `@State`() decorator (so assignments inside handleRouteChange trigger a
re-render) or call this.forceUpdate() at the end of handleRouteChange so
componentDidUpdate (and the scroll logic) executes—update the declaration of
scrollToOnNextUpdate or add the forceUpdate call in handleRouteChange
accordingly (refer to scrollToOnNextUpdate, handleRouteChange, and
componentDidUpdate).
🧹 Nitpick comments (2)
src/components/markdown/markdown.tsx (2)

43-45: componentDidUpdate triggers renderMarkdown unconditionally on every update.

This will re-render markdown even when unrelated state changes occur. Consider adding a check to only re-render when this.text actually changes, or rely on Stencil's @Watch decorator for the text prop instead.

♻️ Suggested approach using `@Watch`
+import { Component, h, Prop, Element, Watch } from '@stencil/core';
-import { Component, h, Prop, Element } from '@stencil/core';
...
-    protected componentDidUpdate(): void {
-        this.renderMarkdown();
-    }
+    `@Watch`('text')
+    protected onTextChange(): void {
+        this.renderMarkdown();
+    }

62-66: Potential null reference if #root element is not found.

If querySelector('#root') returns null, setting .innerHTML will throw. While unlikely given the component's render() method, a defensive check would be safer.

🛡️ Optional defensive check
-        this.host.shadowRoot.querySelector('#root').innerHTML =
-            file?.toString();
+        const root = this.host.shadowRoot.querySelector('#root');
+        if (root) {
+            root.innerHTML = file?.toString();
+        }

adrianschmidt added a commit to Lundalogik/lime-elements that referenced this pull request Feb 11, 2026
Copy link
Collaborator

@adrianschmidt adrianschmidt left a comment

Choose a reason for hiding this comment

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

This seems to work as expected. The anchor links from the navigation menu to Properties, Events, Methods, and Styles are not perfect. On long pages they they don't scroll the page far enough down. But it's exactly the same on main, and the new anchor support in the markdown component works perfectly as far as I've been able to test.

@adrianschmidt adrianschmidt merged commit e91aa28 into jgroth:main Feb 11, 2026
5 checks passed
@jgroth
Copy link
Owner

jgroth commented Feb 11, 2026

🎉 This PR is included in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: give examples anchors, to make it possible to link to a specific example

3 participants