Skip to content

fix: improve anchors parsing in readmes#2486

Open
alexdln wants to merge 1 commit intonpmx-dev:mainfrom
alexdln:fix/anchor-headings
Open

fix: improve anchors parsing in readmes#2486
alexdln wants to merge 1 commit intonpmx-dev:mainfrom
alexdln:fix/anchor-headings

Conversation

@alexdln
Copy link
Copy Markdown
Member

@alexdln alexdln commented Apr 12, 2026

🧭 Context

Some packages (like nuxt) add anchors to headers themselves to control them in other services. This, coupled with the fact that we wrapped all header content in a link, caused hydration errors and wrong rendering

Added processing for a few common cases

  • if it's a title-anchor, then the user's link is ignored and ours is added;
  • if there was a link inside the title, our anchor will stand separately after;
  • if the link is empty, then we will show only content text

The class from the other file does not exist, but it caused warning with several thousand lines description and was terribly interfering

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 12, 2026 11:40am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 12, 2026 11:40am
npmx-lunaria Ignored Ignored Apr 12, 2026 11:40am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved markdown heading rendering to prevent hydration issues with nested anchor elements.
    • Enhanced link handling to gracefully manage unresolved references.
  • Style

    • Minor styling adjustments to package page display.

Walkthrough

This PR addresses two separate issues: removing a class binding from the PackageHeader component and refactoring README heading and link rendering logic to prevent anchor-within-anchor HTML hydration problems whilst handling anchor-wrapped headings.

Changes

Cohort / File(s) Summary
Package Header Styling
app/pages/package/[[org]]/[name].vue
Removed :class="$style.areaHeader" binding from PackageHeader component invocation.
README Rendering Logic
server/utils/readme.ts
Modified heading processing to detect existing <a> elements in displayHtml and avoid nested anchors; updated heading token parsing to strip anchor wrapper tokens; added fallback in link rendering to return original text when link resolution fails.

Possibly related PRs

Suggested reviewers

  • ghostdevv
  • danielroe
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main changes: improving anchor parsing in README files by handling edge cases with existing anchors.
Description check ✅ Passed The description is directly related to the changeset, explaining the context of hydration errors with existing anchors and detailing the three main processing cases implemented.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/utils/readme.ts`:
- Around line 552-555: The issue is that htmlAnchorRe (global /gi) is used with
.test() inside processHeading which mutates lastIndex and interferes with the
later .replace() that uses the same regex; fix by using a non-global regex when
testing or cloning the regex before use: inside processHeading, replace uses of
htmlAnchorRe.test(displayHtml) with testing against a non-global instance (e.g.
new RegExp(htmlAnchorRe.source, htmlAnchorRe.flags.replace('g','')) or simply
create /i-only version) or otherwise ensure lastIndex is reset
(htmlAnchorRe.lastIndex = 0) before the subsequent .replace(); keep the
.replace() usage unchanged but ensure it receives an unmutated/global-intended
regex.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de042ff2-6a07-479e-aae8-49e64494082d

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb6bc6 and a89e54c.

📒 Files selected for processing (2)
  • app/pages/package/[[org]]/[name].vue
  • server/utils/readme.ts
💤 Files with no reviewable changes (1)
  • app/pages/package/[[org]]/[name].vue

Copy link
Copy Markdown
Member

@serhalp serhalp left a comment

Choose a reason for hiding this comment

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

would it be possible to add some test coverage for this? it's pretty complex parsing logic!

// The browser doesn't support anchors within anchors and automatically extracts them from each other,
// causing a hydration error. To prevent this from happening in such cases, we use the anchor separately
if (htmlAnchorRe.test(displayHtml)) {
return `<h${semanticLevel} id="${id}" data-level="${depth}"${preservedAttrs}>${displayHtml}<a href="#${id}"></a></h${semanticLevel}>\n`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we just always do it this way rather than conditionally? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Probably more UX question - is it appropriate for us to only be able to click the anchor by hovering over that side area, not the entire header? I didn't want to break the current UX, so I just added a condition for this rare, specific case with anchor inside heading


renderer.heading = function ({ tokens, depth }: Tokens.Heading) {
const displayHtml = this.parser.parseInline(tokens)
const anchorTokenRegex = /^<a(\s.+)?\/?>$/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

perf nit: we could define this constant regex outside this renderer.heading so it doesn't get recompiled on every heading call.

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.

2 participants