Skip to content

Fix the Table of Contents links of headings with inline formatting - #8424

Open
Somilg11 wants to merge 1 commit into
plone:mainfrom
Somilg11:fix-8340-toc-anchors-inline-formatting
Open

Somilg11 wants to merge 1 commit into
plone:mainfrom
Somilg11:fix-8340-toc-anchors-inline-formatting

Conversation

@Somilg11

@Somilg11 Somilg11 commented Sep 10, 2026

Copy link
Copy Markdown
  • I signed and returned the Plone Contributor Agreement, and received and accepted an invitation to join a team in the Plone GitHub organization.
  • I verified there aren't other open pull requests for the same change.
  • I followed the guidelines in Contributing to Volto.
  • I successfully ran code linting checks on my changes locally.
  • I successfully ran unit tests on my changes locally.
  • I successfully ran acceptance tests on my changes locally.
  • If needed, I added new tests for my changes.
  • If needed, I added documentation for my changes, either in the Storybook or narrative documentation.
  • I included a change log entry in my commits.

Closes #8340

What happens

A Table of Contents block entry stops pointing to the correct heading when the heading contains inline formatting or a link. Clicking the entry does nothing because the link target does not match the id of the heading it should scroll to.

Examples reported in the issue:

Heading Broken anchor Expected heading id
Subtitle 1: everything is okay #kay subtitle-1-everything-is-okay
Subtitle 2: everything is *not* okay #ot-okay subtitle-2-everything-is-not-okay
Subtitle 3: everything is [okay](/some-page) #kay- subtitle-3-everything-is-okay

Headings without inline formatting are not affected.

Why

The heading id and the Table of Contents link were generated from two different representations of the same heading.

The heading id was generated by serializeNodesToText in TextBlockView, while the Table of Contents link was generated by slugging the stored plaintext of the block. Both paths used serializeNodesToText, which is intended for indexing and trims whitespace around individual leaves and nodes, as described in its docstring.

When a heading is split into multiple inline nodes, the two representations can differ. Applied to individual leaves, whitespace around formatted text is removed. Applied to the whole block, empty text nodes around inline elements can result in extra separators in the generated slug.

As a result, the heading and its Table of Contents entry end up with different anchors whenever the heading contains multiple inline nodes.

The fix

The anchor is now generated from the heading text as it is rendered, and the block containing the heading reports the anchor it actually uses.

  • Added getAnchor, getAnchorText and the required serialization helpers in packages/volto-slate/src/utils/toc.js. These concatenate the text from inline children without adding or removing whitespace, then collapse and trim the result before generating the anchor.
  • TextBlockView now renders getAnchor(node) as the heading id.
  • The text block's tocEntry now reports the third element of the entry, [level, title, anchor], calculated from the current value instead of the stored plaintext.
  • The ToC block passes the anchor through to its variations. Blocks that do not report an anchor fall back to the previous behavior.

The tocEntry change is backwards compatible. Add-on blocks that continue to return the usual [level, title] format work exactly as before.

serializeNodesToText and the stored plaintext are unchanged.

Anchors of headings without inline formatting remain unchanged, so existing working URLs are not affected. Only anchors that were already broken are changed.

Before and after

Clicking the Table of Contents entry for a heading containing an italic word:

comparison-italic-heading.png

comparison-italic-heading

Clicking the Table of Contents entry for a heading containing a link:

comparison-heading-with-link.png

comparison-heading-with-link

Tests

  • Added packages/volto-slate/src/utils/toc.test.js, covering plain, italic, partially formatted and linked headings.
  • Added tests in packages/volto-slate/src/blocks/Text/TextBlockView.test.js for the rendered id and verifying that it matches the anchor used by the Table of Contents link.
  • Added tests in packages/volto/src/components/manage/Blocks/ToC for passing the anchor through the entries, including blocks that do not report one.
  • Added anchor assertions to both ToC variations.
  • Added a case to the block-anchors.js acceptance test that creates a heading with an italic word through the editor and follows its Table of Contents entry. It fails on main with:
h2[id="title-1-is-not-okay"] em

Local test runs

  • Unit tests: 347 files, 1566 passed, 1 skipped
  • block-anchors.js acceptance spec: 3 passing, including the new case
  • Full core acceptance suite: development failures in recurrence-widget, blocks-search, and a11y/content reproduce identically on main in the same environment. Every other spec that failed during the full run passes when run individually.

@boring-cyborg

boring-cyborg Bot commented Sep 10, 2026

Copy link
Copy Markdown

Caution

The Volto Team has suspended its review of new pull requests from first-time contributors until the release of Plone 7, which is preliminarily scheduled for the second quarter of 2026.
Read details.

Thanks for submitting your first pull request! You are awesome! 🤗

If you haven't done so already, read
Plone's Code of Conduct,
Contributing to Plone,
First-time contributors, and
Contributing to Volto,
as this will greatly help the review process.

Welcome to the Plone community! 🎉

@mister-roboto

Copy link
Copy Markdown

@Somilg11 you need to sign the Plone Contributor Agreement to merge this pull request.

Learn about the Plone Contributor Agreement: https://plone.org/foundation/contributors-agreement

If you have already signed the agreement, please allow a week for your agreement to be processed.
Once it is processed, you will receive an email invitation to join the plone GitHub organization as a Contributor.

If after a week you have not received an invitation, then please contact agreements@plone.org.

The anchor of a heading was built from the text returned by
`serializeNodesToText`, which trims every leaf of the node and joins them
with whitespace, because it is meant for indexing. A heading that Slate
splits into several leafs, either by inline formatting or by an inline
link, therefore produced an anchor that did not match the one the Table
of Contents block linked to, and the entry led nowhere.

Build the anchor from the text of the heading as it is rendered instead,
and report it as the third element of the `tocEntry` of the block, so
that the Table of Contents block links to the anchor that the block
actually renders, instead of slugging its stored plaintext. Blocks that
report no anchor keep the previous behavior.

Closes plone#8340
@Somilg11
Somilg11 force-pushed the fix-8340-toc-anchors-inline-formatting branch from 7a94673 to 3d11fb9 Compare September 10, 2026 12:38
@mister-roboto

Copy link
Copy Markdown

@Somilg11 you need to sign the Plone Contributor Agreement to merge this pull request.

Learn about the Plone Contributor Agreement: https://plone.org/foundation/contributors-agreement

If you have already signed the agreement, please allow a week for your agreement to be processed.
Once it is processed, you will receive an email invitation to join the plone GitHub organization as a Contributor.

If after a week you have not received an invitation, then please contact agreements@plone.org.

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.

Table of Content block link breaks, if a subheading contains cursive words or links

2 participants