Skip to content

Blocks: Add innerContent support for static inner blocks, adopt it in the HTML block#79115

Merged
youknowriad merged 15 commits into
trunkfrom
claude/eloquent-pare-5f261c
Jun 29, 2026
Merged

Blocks: Add innerContent support for static inner blocks, adopt it in the HTML block#79115
youknowriad merged 15 commits into
trunkfrom
claude/eloquent-pare-5f261c

Conversation

@youknowriad

@youknowriad youknowriad commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What?

Introduces a new block support, innerContent, that lets a block keep static HTML fragments interleaved with inner blocks as the canonical source of its own markup — and adopts it unconditionally in the Custom HTML block. This makes it possible to have editable blocks (a paragraph, an image…) at arbitrary positions deep inside an otherwise static HTML structure, without them being movable or removable like regular inner blocks.

<!-- wp:html -->
<div class="banner"><h1>Static heading</h1><!-- wp:paragraph -->
<p>Editable paragraph</p>
<!-- /wp:paragraph --><footer>Static footer</footer></div>
<!-- /wp:html -->

Why?

Complex hand-written HTML structures currently force a choice: either everything is static (HTML block) or everything is blocks. There's no way to mark just parts of an HTML structure as editable. The block grammar has always supported interleaved HTML and blocks (innerContent with null placeholders), but the in-memory model, serializer, and editor never exposed it for valid registered blocks.

How?

@wordpress/blocks — the data layer:

  • The parser retains the grammar's innerContent array on parsed blocks with the innerContent support and skips save-based validation: serializing the parsed content reproduces the input by construction.
  • The serializer emits these blocks by interleaving the static fragments with their serialized inner blocks instead of calling save (inner blocks without a matching placeholder are defensively appended so content is never lost).
  • createBlock() accepts an optional fourth innerContent argument; getBlockFromExample() passes it through.
  • On the front end, WP_Block::render() already interleaves inner_content with rendered inner blocks, so no PHP changes are needed.

@wordpress/block-editor — a new private InnerContent component:

  • Renders the static markup inert (assigning innerHTML never executes scripts; inline on* handlers are stripped) and portals each inner block's BlockListBlock into a <wp-inner-block-slot> placeholder at its position within the static markup.
  • Inner blocks are editable in place but locked (templateLock: 'all'): no moving, removing, or inserting.
  • Provides a layout context with no alignments — arbitrary static markup offers no layout to align against, so alignment controls are unavailable on the inner blocks.

Custom HTML block:

  • Always runs in innerContent mode. The content attribute (source: raw) is removed — serialized output is byte-identical for existing content, so no deprecation or migration is needed.
  • The canvas SandBox iframe is replaced by InnerContent. Note: scripts no longer execute in the canvas preview (styles still apply); the modal's preview keeps the SandBox.
  • The code modal re-parses on update: <!-- wp:* --> delimited segments in the HTML tab become editable inner blocks.
  • listView support is enabled so the inner blocks tree shows in the inspector, acting as a navigation aid for the editable parts (appender/reorder/delete stay disabled via the locking).
  • All core/html creation sites updated (paste handler, the various convert-to-HTML flows, legacy widget transform); the widget editors' freeform-handler path parses and serializes back unchanged.

Testing Instructions

  1. npm run dev and start wp-env.
  2. Insert a Custom HTML block, click "Edit HTML", and enter static HTML with a block delimited inside it, e.g.:
    <div class="banner"><h1>Static</h1><!-- wp:paragraph --><p>Editable</p><!-- /wp:paragraph --><footer>Footer</footer></div>
  3. Click Update. The paragraph should render at its position inside the static markup and be editable in place.
  4. Verify the paragraph cannot be moved or removed (no movers, no Delete in its options menu) and offers no alignment controls.
  5. Verify the inspector shows the inner blocks list, with spacing between the tabs and the "Edit code" button.
  6. Save the post and reload: the saved markup is identical to what was entered; the paragraph edit persists at its position. Check the front end renders the full structure.
  7. Regression-check existing HTML blocks (plain static content): create, edit, save — markup should be unchanged (note the canvas preview is no longer sandboxed, so scripts don't run there).

Testing Instructions for Keyboard

  1. Insert a Custom HTML block, then use Tab/Enter to activate "Edit HTML", type content including a paragraph block delimiter, Tab to "Update", press Enter.
  2. Use arrow keys/Tab in the canvas writing flow to reach the inner paragraph and type — editing should work without a pointer.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 11, 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.

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

Co-authored-by: youknowriad <[email protected]>
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: mcsf <[email protected]>
Co-authored-by: mtias <[email protected]>
Co-authored-by: tyxla <[email protected]>
Co-authored-by: priethor <[email protected]>
Co-authored-by: fabiankaegy <[email protected]>

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

@github-actions github-actions Bot added [Package] Blocks /packages/blocks [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels Jun 11, 2026
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

Size Change: +1.28 kB (+0.02%)

Total Size: 7.51 MB

📦 View Changed
Filename Size Change
build/scripts/block-directory/index.min.js 43.7 kB +4 B (+0.01%)
build/scripts/block-editor/index.min.js 381 kB +437 B (+0.11%)
build/scripts/block-library/index.min.js 325 kB +417 B (+0.13%)
build/scripts/blocks/index.min.js 45 kB +216 B (+0.48%)
build/scripts/widgets/index.min.js 7.82 kB +25 B (+0.32%)
build/styles/block-library/editor-rtl.css 12.5 kB +18 B (+0.14%)
build/styles/block-library/editor-rtl.min.css 10.3 kB +16 B (+0.16%)
build/styles/block-library/editor.css 12.5 kB +18 B (+0.14%)
build/styles/block-library/editor.min.css 10.3 kB +16 B (+0.16%)
build/styles/block-library/html/editor-rtl.css 1.32 kB +28 B (+2.17%)
build/styles/block-library/html/editor-rtl.min.css 495 B +28 B (+6%) 🔍
build/styles/block-library/html/editor.css 1.33 kB +30 B (+2.3%)
build/styles/block-library/html/editor.min.css 495 B +27 B (+5.77%) 🔍

compressed-size-action

Comment thread packages/blocks/README.md Outdated
- _name_ `string`: Block name.
- _attributes_ `Record< string, unknown >`: Block attributes.
- _innerBlocks_ `Block[]`: Nested blocks.
- _innerContent_ `Array< string | null >`: Static HTML fragments interleaved with inner blocks, where `null` entries mark inner block positions. Only applies to blocks with the `innerContent` support.

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.

Is innerBlocks useful when innerContent is true? Can't we just replace that instead of doing a new arg?

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.

It is useful and in fact it help us keep all the things working properly: things like list views for instance.
innerContent is just the extra HTML fragments.

@mtias mtias Jun 12, 2026

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.

This name doesn't seem very self-evident for what it does (block islands), can we think of something else? Perhaps something like innerBlockIslands or innerBlockSlots

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.

Happy to change the name, but it's not just the slots, it's also the other unrelated HTML content. The slots are represented with null values.

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.

One argument for keeping innerContent is that this is basically an information that we already had in the "raw parser" and the property is named innerContent there too.

Comment thread tools/build-scripts/dev.mjs Outdated
Comment thread packages/blocks/src/api/parser/test/index.js Outdated
Comment thread packages/block-library/src/html/modal.js
import BlockListBlock from '../block-list/block';
import { LayoutProvider } from '../block-list/layout';

const SLOT_TAG_NAME = 'wp-inner-block-slot';

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.

heh, clever :)

```js
supports: {
// Serialize from static HTML fragments interleaved with inner blocks.
innerContent: true

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.

Where do you see this being used outside the html block?

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.

I don't but didn't really want to hard code the HTML block in the parser... potentially someone can build. custom HTML block even if the value is very low.

@mtias

mtias commented Jun 12, 2026

Copy link
Copy Markdown
Member

Thanks for running with this idea. I think it is quite nice. The mental model it imposes is the same as a locked / content-only pattern from the perspective of what is editable and what block tools are available: designated blocks nodes can be edited but they cannot be moved, etc.

We got there in two ways — by locking down everything that is not deemed editable, or by starting with a non-editable block (html) and marking parts of it editable. There are probable some nuances we are missing, including how basic tools like "word count", outline, etc, might operate when there are chunks of content that are not visible as block tree nodes.

So I wouldn't say this replaces those mechanisms entirely, but it gives a lot of flexibility to build content without depending on a top-down block tree.

@mtias mtias added the [Feature] Block API API that allows to express the block paradigm. label Jun 12, 2026
youknowriad and others added 5 commits June 12, 2026 12:29
… the HTML block

Introduce a new block support, innerContent, that lets a block keep
static HTML fragments interleaved with inner blocks as the canonical
source of its own markup:

- The parser retains the grammar's innerContent array (static fragments
  with null placeholders marking inner block positions) on the parsed
  block, and skips save-based validation since the content round-trips
  by construction.
- The serializer emits opted-in blocks by interleaving the fragments
  with their serialized inner blocks instead of calling save.
- A new private InnerContent block-editor component renders the static
  markup inert (no script execution, inline handlers stripped) and
  portals each inner block into its placeholder position, where it is
  editable in place but locked against moving, removing, and inserting.
  No alignment options are offered within static markup.

The Custom HTML block adopts the support unconditionally: the content
attribute is removed (serialized output is byte-identical for existing
content, so no deprecation is needed), the canvas SandBox is replaced
with InnerContent, the code modal re-parses on update so wp:* delimited
segments become editable inner blocks, and listView support surfaces
those inner blocks in the inspector.

Co-Authored-By: Claude Fable 5 <[email protected]>
Replace the templateLock 'all' that InnerContent set on the parent's
block list settings with innerContent-aware checks in canRemoveBlock,
canMoveBlock, and canInsertBlockType. Template locks inherit downward,
which wrongly blocked insertion inside containers (e.g. a Group block)
nested within an HTML block. The constraint now applies only to the
direct children, which are fixed at their placeholder positions; deeper
descendants edit normally.

Co-Authored-By: Claude Fable 5 <[email protected]>
The button borrowed the block inspector's edit-contents class names for
styling, but those are owned by the framework's EditContents component,
which renders above the inspector tabs where a zeroed top margin is
intentional. Style the button in the block's own editor.scss instead,
with spacing on all sides since it renders inside the tabs panel, and
revert the scoped override added to the block inspector stylesheet.

Co-Authored-By: Claude Fable 5 <[email protected]>
The code block's FROM HTML transform test seeded the HTML block via the
removed content attribute, producing an empty block; seed it through
serialized markup instead. Also deflake the new HTML inner blocks test:
wait for the paragraph to be focused before typing, and assert the lock
through the options menu and toolbar instead of a swallowed click.

Co-Authored-By: Claude Fable 5 <[email protected]>
Bail out of the modal update when the content is unchanged, and keep
the existing inner blocks (and thereby their client IDs and selection)
when only the surrounding static HTML was edited, so the mounted
blocks aren't needlessly replaced.

Co-Authored-By: Claude Fable 5 <[email protected]>
@youknowriad youknowriad force-pushed the claude/eloquent-pare-5f261c branch from 6427bd8 to 6362673 Compare June 12, 2026 10:30
@youknowriad youknowriad added [Type] Enhancement A suggestion for improvement. Needs Dev Note Requires a developer note for a major WordPress release cycle [Type] Feature New feature to highlight in changelogs. and removed [Type] Enhancement A suggestion for improvement. labels Jun 12, 2026
@youknowriad

Copy link
Copy Markdown
Contributor Author

I think this is ready for a final review/approval if we want to move forward with it.

@youknowriad youknowriad requested review from ellatrix, mtias and tyxla June 22, 2026 13:13
The HTML block's markup now lives in innerContent rather than the
content attribute, so update the collaboration gauntlet to insert and
edit it through innerContent and assert on it via the full block data.
Add crdt-blocks unit tests confirming innerContent round-trips through
the CRDT merge (it flows through the generic property handling).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@youknowriad youknowriad requested a review from nerrad as a code owner June 22, 2026 13:28
@github-actions github-actions Bot added the [Package] Core data /packages/core-data label Jun 22, 2026
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Flaky tests detected in 6caef11.
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/27963377728
📝 Reported issues:

Comment thread packages/blocks/src/api/parser/index.ts
Comment thread packages/block-library/src/code/transforms.js
Comment thread packages/block-library/src/html/edit.js
Comment thread packages/blocks/src/api/factory.ts Outdated
youknowriad and others added 5 commits June 22, 2026 15:39
The code-to-HTML transform dumped the entire source into a single
static inner content fragment, so any block delimiters became inert
comment text rather than editable inner blocks. This broke the
html -> code -> html round trip. Re-parse the source so delimiters
become inner blocks at their positions, mirroring the code modal.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
parse() already yields a fully-formed core/html block with the right
inner blocks and inner content, so wrapping it in another createBlock
is redundant.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Passing innerContent for a block that lacks the innerContent support
silently dropped it, which wasn't obvious. Emit a development warning
via @wordpress/warning so the dependency on the block support is clear.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the generic innerContent block support with a hardcoded
core/html check in the parser, serializer, createBlock, and the
block-editor locking selectors. Other blocks could otherwise opt into
bypassing validation and deprecations, which mtias and tyxla flagged as
undesirable. Remove the support from block.json, the BlockSupports type,
the JSON schema, and the docs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The crdt-blocks tests assign innerContent on Block literals, so add the
field to the local Block interface. Fixes the TypeScript build.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@youknowriad

Copy link
Copy Markdown
Contributor Author

So should we give this a try?

@youknowriad

Copy link
Copy Markdown
Contributor Author

I would love an approval here, I don't want to miss the 7.1 deadline haha

@youknowriad

Copy link
Copy Markdown
Contributor Author

I'm merging this soon unless there are any blockers that are raised.

@priethor

Copy link
Copy Markdown
Contributor

Catching up with this; thanks for the exploration, @youknowriad!

I'm picking up on your "for now" regarding restricting this to the HTML block: I've been exploring PHP-only blocks for #79330 on top of this, so the core/html hardcode closes that path.

By hardcoding core/html, a block registered in PHP can no longer use this. Is that the final call, or could PHP-registered blocks also opt into the same behavior, even if only later? Should I explore a similar approach without depending on this PR?

@youknowriad

youknowriad commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

By hardcoding core/html, a block registered in PHP can no longer use this.

Why would a block registered in PHP want to use this? I don't understand the relationship?

On the other hand I think what we should allow after this PR, is that users can just register a variation of an HTML block where you actually provider the innerContent. I'm not sure the current variations API allows this yet.

@priethor

priethor commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Why would a block registered in PHP want to use this? I don't understand the relationship?

I was thinking of the PHP-only follow-up in #79330 and the goal of making them editable in the canvas rather than using a render_ballback, with one open question being whether we could leverage this directly instead of building on top of patterns.

So far, the pattern approach looks good and is simpler (although both work well), so I'm not sure this alternative approach needs to solve that, but it was worth mentioning.

@fabiankaegy

Copy link
Copy Markdown
Member

@youknowriad I'm in favor of this. I'm just not sure about the timing stuff... When you say

So should we give this a try?

I'm very much on board and think we should ship it....

When you then say you want to merge it now so that it gets into the beta cutoff for 7.1 which gets released in basically a month I'm less sure.

This is a major change in how the concept of html parsing / validation etc is handled in the editor and there is no way back from here when we ship it in core.

So ideally I'd want us all to be really sure that this is "ready" and I'm not sure we will get that feedback in 2 weeks 🤔

But I won't block merging here. Just sharing my thoughts :)

@youknowriad

Copy link
Copy Markdown
Contributor Author

We have two weeks in Gutenberg and whole beta/RC cycle. It is meaningful for sure but conceptually it has proven to be a logical continuation of what we had, so I don't think it's a huge change really.

@youknowriad

Copy link
Copy Markdown
Contributor Author

So far, #79598 looks good and is simpler (although both work well), so I'm not sure this alternative approach needs to solve that, but it was worth mentioning.

I think that indeed they allow a similar behavior (editable islands in a middle of a locked "block") but conceptually they are very different. the pattern for instance can't be LLM friendly while the HTML block support for things like global styles... are not given. So each has its pros and cons but I don't see one using the other.

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

Labels

[Feature] Block API API that allows to express the block paradigm. Needs Dev Note Requires a developer note for a major WordPress release cycle [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Blocks /packages/blocks [Package] Core data /packages/core-data [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants