Skip to content

Add block data to REST API - #2649

Open
adamsilverstein wants to merge 80 commits into
WordPress:trunkfrom
adamsilverstein:add-block-rest-endpoints
Open

adamsilverstein wants to merge 80 commits into
WordPress:trunkfrom
adamsilverstein:add-block-rest-endpoints

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Sep 2, 2017 •

Copy link
Copy Markdown
Member

This pull request adds Gutenberg block data to the post endpoints

This provides a way to get the data and content of each block of a Gutenberg-built page from the front end. This would be very useful for building component based front ends, because the components could map one-to-one with gutenberg blocks. With these endpoints, an App could easily get the data it needs to render each component. This might also provide a patch for a standard component library matching Gutenberg blocks.

Brought up to date with Claude Code in November 2026, its summary of where this landed:

Adds content.blocks to REST responses for every REST-enabled post type, and to their revisions and autosaves: the post's blocks as parsed from the content. Keys match the public properties of WP_Block (name, attributes, inner_blocks, inner_html, inner_content), applied all the way down the nested blocks.

  • Edit context only. The blocks carry the raw stored markup, so they follow the same rules as content.raw: the edit context, where the controller only returns posts the user can edit (collections included). This keeps markup that plugins hide at render time (block visibility, paywalls) out of public responses.
  • Same tree as content.raw. Posts get the same Block Hooks as content.raw before parsing, so hooked blocks show up in both. Revisions and autosaves return the stored content, like their content.raw.
  • Opt-in. The request has to name content.blocks in _fields, so existing clients, including the editor's own context=edit loads, don't pay for a second parse and a doubled payload.
  • Whitespace-only freeform blocks between top-level blocks are dropped, an empty top-level attributes encodes as {} (nested empty objects inside it still come back as [], from parse_blocks()), and content.blocks is described in the item schema.
  • The code lives in lib/compat/wordpress-7.3/rest-blocks.php and steps aside when core already returns content.blocks.

attributes are only the ones stored in the block comment delimiter. Sourced attributes (eg. an image src that lives in the HTML) are not extracted; see the discussion on https://core.trac.wordpress.org/ticket/53603.

Core backport: WordPress/wordpress-develop#15835
Trac: https://core.trac.wordpress.org/ticket/53603

Testing Instructions

Test in WordPress Playground

  1. Log in as an editor or admin and open a post with some nested blocks (eg. a Group with a Paragraph inside).
  2. Visit /wp-json/wp/v2/posts/<id>?context=edit&_fields=id,content.blocks.
  3. content.blocks holds the block tree, nested blocks under inner_blocks.
  4. Remove _fields, or switch to context=view, and content.blocks is no longer returned.
  5. As a contributor, request /wp-json/wp/v2/posts?context=edit&status=draft,publish&_fields=id,content.blocks. Only the contributor's own posts come back, each with blocks.
  6. Request /wp-json/wp/v2/posts/<id>/revisions/<revision id>?context=edit&_fields=id,content.blocks and the revision's blocks come back too.
  7. Run the tests: npm run test:php -- --filter Gutenberg_REST_Blocks_Test

AI Use

The original code is mine from 2017-2020. Claude Code did this round: the trunk merge, the review fixes, the tests, the core backport, and a second pass for Block Hooks, revisions and test gaps, plus this description. I will review and test.

🤖 Generated with Claude Code

https://claude.ai/code/session_018vwYjWL8VuAH7kt7zZdZi4

@BE-Webdesign

Copy link
Copy Markdown
Contributor

#2503 is similar. They are addressing two different problems but, it would be good to have coordination between this and #2503 .

@adamsilverstein

Copy link
Copy Markdown
Member Author

@BE-Webdesign thanks for pointing out #2503 - I searched for a similar PR and couldn't find one.

As you pointed out, these PRs are pretty different; from a brief view it looks like #2503 is an API for (reusable) block objects themselves, while this PR is focused on exposing a (gutenberg built) post's blocks (and their data)

@rmccue rmccue 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 rather than creating separate endpoints, we should build this into the existing endpoints instead. In particular .content on the post type is intentionally an object to allow adding extra properties.

(I'll follow this up in a minute; GH's review comment UI isn't great for leaving long comments.)

Comment thread lib/class-wp-rest-gutenberg-blocks.php Resolved
Comment thread lib/class-wp-rest-gutenberg-blocks.php Resolved
Comment thread lib/class-wp-rest-gutenberg-blocks.php Resolved
Comment thread lib/class-wp-rest-gutenberg-blocks.php Resolved
@rmccue

rmccue commented Sep 4, 2017 •

Copy link
Copy Markdown
Contributor

In the post object, content contains the various representations of the same piece of data: the post content. As I understand it, with Gutenberg, the blocks are simply another representation of that same piece of data. (We also intentionally designed content for this purpose; although Gutenberg wasn't in sight at the time, other page builders were, and making content an object gave us more extensibility there.)

It also seems like (maybe) Gutenberg distinguishes between rendered and raw block content, based on this PR. (I could be wrong there.) Given that, I'd probably add a new sub-field under content called blocks, which could also contain its own raw and rendered fields (based on the provided context).

Something like this:

{
    "content": {
        "raw": "<!-- gutenberg comments --> interspersed with HTML",
        "rendered": "<div class='block-foo'><p>bar</p></div><p>interspersed with HTML</p>",
        "blocks": [
            {
                "type": "foo",
                "attributes": {
                    "value": "bar"
                },
                "rendered": "<p>bar</p>"
            },
            {
                "type": "html",
                "attributes": {
                    "content": "interspersed with HTML"
                },
                "rendered": "<p>interspersed with HTML</p>"
            }
        ]
    }
}

(Disclaimer: I only know the data model in very broad strokes here.)

@noisysocks

Copy link
Copy Markdown
Member

As you pointed out, these PRs are pretty different; from a brief view it looks like #2503 is an API for (reusable) block objects themselves, while this PR is focused on exposing a (gutenberg built) post's blocks (and their data)

While the two endpoints address different use cases, it'd be highly useful to have both endpoints represent a block in a consistent way, e.g.

{
    "type": "core/paragraph",
    "attributes": {
        "content": "How are you?",
        "dropCap": true
    },
    "content": "<!-- wp:core/paragraph {\"dropCap\":true} -->\n<p class=\"has-drop-cap\">How are you?</p>\n<!-- /wp:core/paragraph -->",
    "rendered": "<p class=\"has-drop-cap\">How are you?</p>"
}

In the post object, content contains the various representations of the same piece of data: the post content. As I understand it, with Gutenberg, the blocks are simply another representation of that same piece of data.

+1

@adamsilverstein

Copy link
Copy Markdown
Member Author

While the two endpoints address different use cases, it'd be highly useful to have both endpoints represent a block in a consistent way.

Good point @noisysocks, I was simply passing the data back as provided by gutenberg_parse_blocks(). looks like I need to transform the naming anyway so I'll try to match the block object format from #2503.

Thanks for the feedback @rmccue - I like your suggestion of adding this data onto the regular post endpoint content object - my only concern would be the overhead introduced in block parsing the post content, even if it hasn't been edited in gutenberg, seems like we need a performat has_been_edited_in_gutenberg() helper, if there is one I couldn't find it.

@adamsilverstein

Copy link
Copy Markdown
Member Author

I remapped the response fields to match #2503 and also moved the data to the post object response as content->blocks.

Here is what the response from the demo page provided by my install of gutenberg looks like:

https://gist.github.com/adamsilverstein/5369160faca6097c5e22e0256e380962

@adamsilverstein

Copy link
Copy Markdown
Member Author

@noisysocks & @rmccue can you please take another look after my recent changes where I have addressed your suggestions.

@rmccue

rmccue commented Sep 5, 2017

Copy link
Copy Markdown
Contributor

my only concern would be the overhead introduced in block parsing the post content, even if it hasn't been edited in gutenberg

Won't this point become moot at the point where Gutenberg is the default editor anyway? Also, we could choose to only expose all the block pieces for context=edit if it's a frontend concern.

rmccue
rmccue previously requested changes Sep 5, 2017
Comment thread lib/blocks.php Outdated

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.

This can be $block_name instead

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.

fixed in 5937d1a

Comment thread lib/blocks.php Outdated

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.

This should be inverted with a continue instead:

if ( null === $block_name ) {
    continue;
}

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.

fixed in d990979

Comment thread lib/blocks.php Outdated

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.

This should be $attributes not $block['attrs']. Also, properties should always be set if they're exposed in a given context to avoid index errors.

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.

fixed in 862bccc

Comment thread lib/blocks.php Outdated

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.

$block['rawContent'] -> $raw_content

Also, should this only be exposed for $context === 'edit'?

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.

This is perhaps misnamed when it is returned from the parser - its not really the raw content and doesn't include the html markers for example. This really represents the rendered html and is returned as content for a block (matching naming from #2503). As far as I can see, rendered is only used for a couple of block types where their content is dynamic, for example a recent post list.

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.

Can you review the data thats output, it seems public? we could show blocks only in edit context, but that would make my idea of using them for a front end data source less useful.

Comment thread lib/blocks.php Outdated

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.

Maybe gutenberg_add_blocks_to_post_resource()? This won't be used apart from the filter, so should probably be a prefixed name rather than a convenient one.

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.

fixed in 36f1000

Comment thread lib/blocks.php Resolved
Comment thread lib/blocks.php Outdated

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.

The blocks field should always be set, even if it's null.

Also, this should probably use ->set_data() instead of reaching into the object.

@adamsilverstein adamsilverstein Sep 7, 2017 •

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.

Addressed in 957dc7e & bc5f31e

@joemcgill

Copy link
Copy Markdown
Member

+1 for this being a part of the regular content endpoint(s) and glad to see that #1516 was mentioned.

Architecturally, we should be thinking about more than just default post content here too since the blocks concept will likely be used in other scenarios (i.e. widgets and menus) in the future.

Base automatically changed from master to trunk March 1, 2021 15:42
@gziolo

gziolo commented Jul 6, 2021

Copy link
Copy Markdown
Member

I filed two tickets in WordPress core hoping we can move forward efforts for this POC:

@gziolo gziolo added the [Feature] Block API API that allows to express the block paradigm. label Feb 11, 2023
@gziolo gziolo mentioned this pull request Feb 13, 2023
14 of 70 tasks
@youknowriad

Copy link
Copy Markdown
Contributor

Doing some cleanup in the repository. I was wondering if this PR still worth keeping open. Probably needs to be redone entirely if we ever get to it. What do you think?

@mtias

mtias commented Aug 3, 2024

Copy link
Copy Markdown
Member

I still think this is a good addition to the API surface, would be nice to pick it up

@adamsilverstein

Copy link
Copy Markdown
Member Author

I still feel this would be a valuable addition although I'm not sure when I will have time to pick it up again. I do see some recent updates on the core tickets though.

@gziolo
gziolo removed their request for review October 7, 2026 11:15
Initialize the per-block item array, drop an unused variable and fix
spacing so PHPCS passes after the trunk merge.
Also drops the unused block registry variable, which was the intent.
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @BE-Webdesign, @loremipson, @khromov.

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: BE-Webdesign, loremipson, khromov.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: rmccue <rmccue@git.wordpress.org>
Co-authored-by: noisysocks <noisysocks@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: joemcgill <joemcgill@git.wordpress.org>
Co-authored-by: kadamwhite <kadamwhite@git.wordpress.org>
Co-authored-by: mtias <matveb@git.wordpress.org>
Co-authored-by: lkraav <lkraav@git.wordpress.org>
Co-authored-by: TimothyBJacobs <timothyblynjacobs@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

adamsilverstein and others added 5 commits November 7, 2026 20:08
Rename block keys to match WP_Block properties (name, attributes,
inner_blocks, inner_html, inner_content) and apply them recursively,
as requested in review. Register the filters on rest_api_init for every
REST-enabled post type that supports the editor, so the opt-out filter
can actually run. Hide blocks for password-protected posts the same way
content.rendered is hidden, honor _fields, drop whitespace-only freeform
blocks, and describe content.blocks in the item schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjWL8VuAH7kt7zZdZi4
The blocks carry the raw stored markup, so expose them under the same
rules as content.raw: the edit context and permission to edit the post.
Make the field opt-in through _fields so existing clients, including the
editor's own edit context loads, don't pay for parsing and a doubled
payload. Drop the opt-out filter, skip posts that already have blocks,
and describe the nested items in the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjWL8VuAH7kt7zZdZi4
Hooking registered_post_type picks up post types registered at any
point, including late on rest_api_init, instead of only the ones that
existed when rest_api_init first ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjWL8VuAH7kt7zZdZi4
@adamsilverstein

adamsilverstein commented Nov 8, 2026 •

Copy link
Copy Markdown
Member Author

Picking this one back up, six years later! Thanks @mtias for keeping it alive.

Claude went back through the old review threads, here is what changed:

  • @noisysocks the keys now match WP_Block: name, attributes, inner_blocks, inner_html and inner_content, applied recursively so nested blocks use them too. The parsed array is mapped directly rather than building WP_Block instances, so attributes stay exactly what is stored in the delimiter, with no defaults filled in.
  • @TimothyBJacobs on parsing twice per request: content.blocks is now opt-in. It is only returned in the edit context when the request names it in _fields, so the extra parse only happens for clients that ask for the blocks. Memoizing parse_blocks() still seems like a good separate ticket.
  • The edit-only rule is new. The blocks carry the raw stored markup, so returning them in the view context would expose content that plugins hide at render time, and the content of password-protected posts.
  • The code moved to lib/compat/wordpress-7.3/rest-blocks.php, with PHPUnit tests, and the core backport is up at REST API: Add parsed blocks to the post content field. wordpress-develop#15835 for https://core.trac.wordpress.org/ticket/53603.

Sourced attributes (from the Trac discussion with @spacedmonkey and @gziolo) are still out of scope; this returns what parse_blocks() returns.

Apply Block Hooks before parsing so content.blocks and content.raw
describe the same tree, and add blocks to revision and autosave
responses, whose schemas already inherit the field from the parent.

Drop the per-post edit_post check: in the edit context the posts
controller already leaves out posts the user cannot edit, so the
plugin now behaves exactly like the core patch.

Let the schema callback accept a non-array schema from another filter
and keep a blocks definition core already provides.

Cover freeform content, _fields as an array, custom post types, post
types without content, hooked blocks, revisions and autosaves, and
reset the REST server after each test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjV3Gi53g5ct8BDL5JE
@adamsilverstein adamsilverstein added the [Type] Feature New feature to highlight in changelogs. label Nov 11, 2026

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

Core REST API Task Task for Core REST API efforts [Feature] Block API API that allows to express the block paradigm. REST API Interaction Related to REST API [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.