Skip to content

fix(publisher): emit one merged rel on links and buttons instead of two - #570

Open
tommy230 wants to merge 1 commit into
CoreBunch:mainfrom
tommy230:fix/anchor-rel-single-attribute
Open

tommy230 wants to merge 1 commit into
CoreBunch:mainfrom
tommy230:fix/anchor-rel-single-attribute

Conversation

@tommy230

@tommy230 tommy230 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A link or button anchor that carries its own rel (typed into the Attributes view, or brought in by the HTML importer) is published with two rel attributes: the author's from the htmlAttributes bag, then the module's noopener noreferrer for target="_blank". A duplicate attribute is a parse error and the browser keeps the first one, so the new-tab guard is silently gone. The canvas does not show this: React lets the module's rel win there, so the editor and the published page disagree.

<!-- stored: href=https://e.com, target=_blank, htmlAttributes={ rel: "nofollow" } -->
<!-- published today -->
<a rel="nofollow" href="https://e.com" target="_blank" rel="noopener noreferrer">Go</a>
<!-- what the browser keeps -->
<a rel="nofollow" href="https://e.com" target="_blank">Go</a>

Modern browsers imply noopener for target="_blank", so in practice the loss is noreferrer plus the editor/page mismatch. This is a hardening fix, not a vulnerability report.

On import the opposite happens: walkAndMap.ts lists rel as module-generated for base.link and base.button, so every source rel (nofollow, sponsored, next/prev, me) is dropped, even though the module only ever regenerates the _blank guard. A theme that styles its pagination with a[rel="next"] loses that styling after import.

The cause is that render() and the *Editor.tsx of both modules serialise anchorRel(target) next to the untouched bag instead of combining them.

The fix:

  • mergeAnchorRel(authoredRel, target) in src/modules/base/shared/anchorTarget.ts returns one token list: the author's tokens, then the security rel, each once. anchorHtmlAttributes(bag, target) splits rel out of the bag and merges it; both anchor modules use it on the publisher and canvas paths, so exactly one rel is emitted and it is the same in both.
  • A _blank anchor always carries noopener noreferrer, whatever the author wrote.
  • The importer keeps rel on links and button anchors in htmlAttributes.
  • The authored rel value is HTML-escaped on the publisher path like every other bag attribute.

Rich text and markdown links already emit a single rel (sanitize.ts replaces it, renderMarkdown.ts writes a fixed one), so only these two modules had the duplicate.

Anchors without an authored rel publish byte-identical markup (the render goldens are unchanged). Existing stored nodes with a rel in the bag start publishing the merged value on their next publish.

Overlaps with #575 on shared/anchorTarget.ts and docs/features/modules.md; whichever lands second gets a small rebase.

Verification

  • bun run build
  • bun run lint
  • bun test: 7060 pass, 0 fail
  • Docker/deployment check, if relevant: not relevant

New and updated tests, all failing on main and passing with this change:

  • src/__tests__/base-modules-shared-render.test.ts: 4 render tests. They cannot load on main because mergeAnchorRel does not exist there.
  • src/__tests__/base-modules-shared-render.editor.test.tsx: 2 editor tests.
  • src/__tests__/htmlImport/mapping.test.ts: 2 importer tests, one of them the existing attribute-preservation test, which now keeps rel="nofollow".

Checklist

  • Tests cover behavior changes.
  • Docs were updated when behavior, config, deployment, or public surfaces changed. (docs/features/modules.md and docs/features/html-import.md.)
  • No compatibility shim was added for old pre-release behavior.
  • No secrets, local databases, uploads, or generated artifacts are included.

🤖 Generated with Claude Code

base.link and base.button emitted the author's rel from the htmlAttributes
bag and their own security rel as two separate attributes. Browsers keep the
first, so a nofollow new-tab link published without noopener noreferrer, and
the canvas showed a different rel than the page. The HTML importer also
dropped every source rel on anchors as if the module regenerated it.

The two anchor modules now merge the authored tokens with the security rel
through one shared helper on both render paths, and the importer keeps rel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tommy230
tommy230 force-pushed the fix/anchor-rel-single-attribute branch from 2bde07a to b67f1a9 Compare September 29, 2026 04:45
@tommy230 tommy230 changed the title fix(modules): emit one rel on links and buttons and keep the authored tokens fix(publisher): emit one merged rel on links and buttons instead of two Sep 29, 2026
@tommy230
tommy230 marked this pull request as ready for review September 29, 2026 04:47

This branch has not been deployed

No deployments
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