Skip to content

Stop saving Table of Contents output - #80404

Merged
scruffian merged 19 commits into
trunkfrom
update/remove-headings-nav-markup
Aug 17, 2026
Merged

Stop saving Table of Contents output#80404
scruffian merged 19 commits into
trunkfrom
update/remove-headings-nav-markup

Conversation

@getdave

@getdave getdave commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Related to #41031 and #42229.
Addresses #80782

This changes the experimental Table of Contents block so generated heading data and generated <nav> markup are no longer saved into post content.

Existing blocks that already have saved Table of Contents markup continue to render that saved markup on the front of site until the post is edited and saved.

Why

The Table of Contents now renders on the front of site from the current post headings. Persisting generated heading data and saved navigation markup makes the saved content stale and duplicates data that can be derived at render time.

This is also part of the Table of Contents stabilization work in #42229: keeping saved markup minimal reduces the future compatibility burden while dynamic rendering continues to evolve.

Although the block is experimental, it has been available in the Gutenberg plugin for long enough that existing content needs a compatibility path. This PR includes a deprecation that recognizes the old saved <nav> form, drops the serialized headings data, and preserves persistent settings while migrating to the new dynamic form.

How

The block now follows the existing dynamic block pattern used by other server-rendered core blocks with custom editor UI: it relies on server rendering and does not provide a save implementation.

The headings attribute remains available for the editor preview, but is marked with role: "local" so it is not serialized. Persistent settings such as ordered, maxLevel, and onlyIncludeCurrentPage continue to serialize as block comment attributes.

The render callback preserves saved legacy markup when a block still has saved inner content. This keeps existing front of site output stable for posts that have not been edited since the old block format was saved.

The deprecated block definition snapshots the old saved output so legacy content can validate in the editor. When the post is saved, the block migrates to the new dynamic serialization shape while keeping persistent settings.

Fixtures, docs, and e2e helpers now use self-closing Table of Contents block markup, and a deprecated fixture covers old saved content migrating to the new serialization shape.

Testing Instructions

  1. Insert a Table of Contents block and several Heading blocks.
  2. Confirm the editor preview updates as headings are added, removed, reordered, and when list/max-level settings change.
  3. Save or publish the post.
  4. Confirm post content stores the Table of Contents as a self-closing block comment without headings or saved <nav> markup.
  5. Confirm the front of site renders the Table of Contents from the current post headings.
  6. Create or load content where a Table of Contents block already has saved <nav> markup.
  7. Confirm the front of site renders the saved Table of Contents markup before the post is edited.
  8. Open that post in the editor, confirm the Table of Contents shows current headings, then save.
  9. Confirm the saved content migrates to the self-closing block form and the front of site renders current headings.

Tested with:

  • npm run test:unit -- test/integration/full-content/full-content.test.js --runInBand --testNamePattern=table-of-contents
    • --runInBand keeps the generated full-content fixture run in one Jest worker for this targeted check.
  • npm run test:unit:php:base -- --filter Table_Of_Contents
  • npm run test:e2e -- test/e2e/specs/editor/blocks/table-of-contents.spec.js

@getdave getdave self-assigned this Jul 17, 2026
@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Jul 17, 2026
@getdave
getdave requested a review from Copilot July 17, 2026 09:38
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Size Change: +279 B (0%)

Total Size: 7.73 MB

📦 View Changed
Filename Size Change
build/scripts/block-library/index.min.js 358 kB +279 B (+0.08%)

compressed-size-action

@getdave getdave added [Type] Code Quality Issues or PRs that relate to code quality [Block] Table of contents (experimental) Affects the Table of contents Block labels Jul 17, 2026
@getdave
getdave force-pushed the update/remove-headings-nav-markup branch from 69487e1 to 7afc154 Compare July 17, 2026 09:42
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
const { serializeRawBlock } = require( '../../../../../packages/blocks' );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot is this a good approach? If not, based on other cases in this repo, what would you suggest?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No, it's not a good approach — importing serializeRawBlock via a relative path to packages/blocks is not used anywhere else in the e2e suite. Every other test file constructs block markup with raw template literals.

Replaced all three helpers (headingBlock, htmlBlock, tableOfContentsBlock) with equivalent template literals in commit Replace serializeRawBlock import with raw template literals in e2e test. The tableOfContentsBlock helper now serializes attributes conditionally and returns a self-closing comment: <!-- wp:table-of-contents /--> (or <!-- wp:table-of-contents {"ordered":false} /--> etc.), matching the format used in the fixtures and postContentWithTocAndHeadings.

@getdave
getdave marked this pull request as ready for review July 17, 2026 09:44
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @Copilot.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: Copilot.

Co-authored-by: getdave <get_dave@git.wordpress.org>
Co-authored-by: scruffian <scruffian@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@getdave
getdave force-pushed the update/remove-headings-nav-markup branch from 7afc154 to e36258a Compare July 17, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the experimental core/table-of-contents block to follow a fully dynamic (server-rendered) pattern so it no longer serializes generated headings data or saved <nav> markup into post content, preventing stale/duplicated ToC output.

Changes:

  • Remove the block’s client save implementation so the block serializes as a self-closing comment and relies on server rendering.
  • Mark the headings attribute as role: "local" so it remains usable for editor preview without being persisted.
  • Update integration fixtures, docs, and e2e helpers to use self-closing Table of Contents block markup.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/integration/fixtures/blocks/core__table-of-contents.serialized.html Updates serialized fixture to self-closing ToC block with persisted settings only.
test/integration/fixtures/blocks/core__table-of-contents.parsed.json Updates parsed fixture expectations for empty inner HTML/content and persisted attrs.
test/integration/fixtures/blocks/core__table-of-contents.json Updates block fixture attributes to reflect non-serialized headings and persisted settings.
test/integration/fixtures/blocks/core__table-of-contents.html Updates HTML fixture to self-closing ToC block comment.
test/e2e/specs/editor/blocks/table-of-contents.spec.js Updates e2e helpers/fixtures to stop relying on saved <nav> markup.
packages/block-library/src/table-of-contents/save.js Removes static saved markup implementation.
packages/block-library/src/table-of-contents/README.md Updates documentation to describe the block as dynamic and show new stored markup form.
packages/block-library/src/table-of-contents/index.js Stops registering a custom save, aligning client registration with dynamic behavior.
packages/block-library/src/table-of-contents/block.json Marks headings attribute as role: "local" to prevent serialization.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Flaky tests detected in c1d8adc.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/32021190972
📝 Reported tests:

two users concurrently move list items (RTC WebSocket) in /test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts, passed after 1 failed attempt.
Error: expect(received).toBeGreaterThan(expected)

Expected: > 2
Received:   1

Call Log:
- Timeout 10000ms exceeded while waiting on the predicate
    at assertMovedOrder (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts:646:8)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts:650:10

@Mamaduka

Copy link
Copy Markdown
Member

I think we can drop the headings attribute, but it will likely require a larger refactoring. Not a blocker here. I have some ideas and could do it as a follow-up.

@t-hamano

Copy link
Copy Markdown
Contributor

Because this is an experimental block, this PR intentionally does not include a deprecation to migrate existing saved blocks to the new dynamic form. Existing experimental content may lose old saved Table of Contents markup when resaved.

I believe the deprecation is absolutely necessary. While this block does have "experimental":true applied in block.json, I don't think this field strictly denotes whether a block is experimental. This field is simply meant to prevent the block from shipping to core, and it's a block that is available by default if the Gutenberg plugin is enabled.

Since this block has been part of the Gutenberg plugin for a long time, not adding a deprecation could affect a significant number of sites.

image

@getdave

getdave commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Since this block has been part of the Gutenberg plugin for a long time, not adding a deprecation could affect a significant number of sites.

Yes I wondering whether that would be the concern. That said, playing devil's advocate, does it set the wrong precedent? We don't commit to backwards compatibility until something has been stabilised. AFAIK this block would only register if you enable the experimental blocks feature, at which point you are actively opting in to something that could be unstable.

In this case, it's relatively simple to provide a basic deprecation which just migrates the block and honestly I think we should just add it. However, I think we might need to be clearer about the contract of these "experimental" blocks to avoid other situations where it could be a lot more complicated to provide a migration.

@t-hamano

Copy link
Copy Markdown
Contributor

AFAIK this block would only register if you enable the experimental blocks feature

I think this is incorrect. The Table of Contents block has been available for several years, even without enabling the experimental block feature.

As a side note, now that the Playlist and Tabs blocks are stable, the only remaining experimental block family is Forms.

if ( window?.__experimentalEnableFormBlocks ) {
blocks.push( form );
blocks.push( formInput );
blocks.push( formSubmitButton );
blocks.push( formSubmissionNotification );
}
if ( window?.__experimentalEnableBlockExperiments ) {
// Blocks added here are only registered when the "Block experiments"
// option is enabled in the Gutenberg > Experiments settings page.
}

I think we might need to be clearer about the contract of these "experimental" blocks to avoid other situations where it could be a lot more complicated to provide a migration.

Regarding this matter, I have previously clarified various states of the block and posted my thoughts.

#71228

However, adding deprecations is indeed cumbersome, so I hope a block validation level will be introduced.

@getdave

getdave commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@t-hamano Ok I was incorrect. Thanks for clarifying.

So we're agreed that a deprecation is required, especially as validation levels isn't a thing yet.

@t-hamano

Copy link
Copy Markdown
Contributor

So we're agreed that a deprecation is required, especially as validation levels isn't a thing yet.

My understanding is this. It's particularly bothersome how the fixture files keep accumulating, but I don't see any other way around it for now.

@getdave
getdave force-pushed the update/remove-headings-nav-markup branch from c65b625 to 4e2de40 Compare July 29, 2026 09:14
@getdave

getdave commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

As I understand it you are recommending the following

  • For already-published old posts that nobody edits, keep rendering the saved static ToC markup so the front end does not unexpectedly change or disappear.
  • For old posts opened in the editor, use a deprecation so the block validates cleanly, then let it migrate to the new dynamic format when the post is saved.
  • For new or migrated posts, the block should behave dynamically: no saved <nav>, no persisted generated headings -the front end derives the ToC from the current post content dynamically.

Did I understand that correctly?

If so I'll need to make sure the first point is addressed in this PR.

@Mamaduka

Copy link
Copy Markdown
Member

That's correct, and as you said, it only requires addressing the first point in this PR.

@getdave

getdave commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@Mamaduka Great. All done. Thanks for picking up on this btw - it was a important piece of the implementation 🙇

Comment thread test/e2e/specs/editor/blocks/table-of-contents.spec.js Outdated
@scruffian

This comment was marked as resolved.

@scruffian

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Co-authored-by: scruffian <275961+scruffian@users.noreply.github.com>

Copilot AI commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merge conflicts resolved:

  • packages/block-library/CHANGELOG.md: kept both the ToC bug-fix entry from this branch and the Navigation Overlay entry from trunk.
  • packages/block-library/src/table-of-contents/save.js: trunk had modified the file but this PR intentionally deletes it; resolved by removing the file as intended.

Comment thread test/e2e/specs/editor/blocks/table-of-contents.spec.js Outdated

@scruffian scruffian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should bring this in and keep iterating.

@scruffian
scruffian merged commit a2556e7 into trunk Aug 17, 2026
45 checks passed
@scruffian
scruffian deleted the update/remove-headings-nav-markup branch August 17, 2026 11:46
@github-actions github-actions Bot added this to the Gutenberg 23.9 milestone Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Block] Table of contents (experimental) Affects the Table of contents Block [Package] Block library /packages/block-library [Type] Code Quality Issues or PRs that relate to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants