Skip to content

Latest commit

 

History

History
855 lines (666 loc) · 30.1 KB

README.md

File metadata and controls

855 lines (666 loc) · 30.1 KB

⚠️ This plugin is currently in Beta. It is designed to run on WordPress VIP. This beta release is not intended for use on a production environment.


VIP Block Data API (Beta)

VIP Block Data API attribute sourcing animation

The Block Data API is a REST API for retrieving block editor posts structured as JSON data. While primarily designed for use in decoupled WordPress, the Block Data API can be used anywhere you want to represent block markup as structured data.

This plugin is currently developed for use on WordPress sites hosted on the VIP Platform.

Quickstart

You can get started with the Block Data API in 3 steps.

  1. Install the plugin by adding it to the plugins/ directory of the site's GitHub repository. We recommend using git subtree for adding the plugin.
  2. Activate the plugin in the "Plugins" screen of the site's WordPress admin dashboard.
  3. Make a request to /wp-json/vip-block-data-api/v1/posts/<post_id>/blocks (replacing <post_id> with a valid post ID of your site).

Other installation options, examples, and helpful filters for customizing the API are outlined below.

Table of contents

Installation

The latest version of the VIP Block Data API plugin is available in the default trunk branch of this repository.

Install via git subtree

We recommend installing the latest plugin version via git subtree within your site's repository:

# Enter your project's root directory:
cd my-site-repo/

# Add a subtree for the trunk branch:
git subtree add --prefix plugins/vip-block-data-api [email protected]:Automattic/vip-block-data-api.git trunk --squash

To deploy the plugin to a remote branch, git push the committed subtree.

The trunk branch will stay up to date with the latest version of the plugin. Use this command to pull the latest trunk branch changes:

git subtree pull --prefix plugins/vip-block-data-api [email protected]:Automattic/vip-block-data-api.git trunk --squash

BETA: We anticipate frequent updates to the VIP Block Data API plugin during beta testing. Ensure that the plugin is up-to-date by pulling changes often.

Note: We do not recommend using git submodule. Submodules on WPVIP that require authentication will fail to deploy.

Install via ZIP file

The latest version of the plugin can be downloaded from the repository's Releases page. Unzip the downloaded plugin and add it to the plugins/ directory of your site's GitHub repository.

Plugin activation

Usually VIP recommends activating plugins with code. In this case, we are recommending activating the plugin in the WordPress Admin dashboard. This will allow the plugin to be more easily enabled and disabled during testing.

To activate the installed plugin:

  1. Navigate to the WordPress Admin dashboard as a logged-in user.

  2. Select Plugins from the lefthand navigation menu.

  3. Locate the "VIP Block Data API" plugin in the list and select the "Activate" link located below it.

    Plugin activation

Usage

The VIP Block Data API plugin provides a REST endpoint for reading post block data as JSON. The REST URL is located at:

/wp-json/vip-block-data-api/v1/posts/<post_id>/blocks

// e.g. https://my-site.com/wp-json/vip-block-data-api/v1/posts/139/blocks

This public endpoint will return editor block metadata as structured JSON for any published post, page, or published WP_Post object.

Review these Filters to learn more about limiting access to the REST endpoint:

The Block Data API uses server-side registered blocks to determine block attributes. Refer to the Client-side blocks section for more information about client-side block support limitations.

Block Data API examples

Examples of WordPress block markup and the associated data structure returned by the Block Data API.

Example: Basic text blocks: core/heading and core/paragraph

Heading and paragraph block in editor

Block Markup Block Data API
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Block Data API</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Blocks as JSON.</p>
<!-- /wp:paragraph -->
[{
  "name": "core/heading",
  "attributes": {
    "level": 3,
    "content": "Block Data API"
  }
},
{
  "name": "core/paragraph",
  "attributes": {
    "content": "Blocks as JSON."
  }
}]

Example: Text attributes in core/pullquote

Pullquote block in editor

Block Markup Block Data API
<!-- wp:pullquote -->
<figure class="wp-block-pullquote">
    <blockquote>
        <p>From markup -> props</p>
        <cite>~ WPVIP</cite>
    </blockquote>
</figure>
<!-- /wp:pullquote -->
[{
  "name": "core/pullquote",
  "attributes": {
    "value": "From markup -> props",
    "citation": "~ WPVIP"
  }
}]

Example: Nested blocks in core/media-text

Media-text block containing heading in editor

Block Markup Block Data API
<!-- wp:media-text {"mediaId":256,
  "mediaType":"image"} -->
<div class="wp-block-media-text">
  <figure class="wp-block-media-text__media">
    <img src="http://my.site/api.jpg"
      class="wp-image-256 size-full" />
  </figure>
  <div class="wp-block-media-text__content">
    <!-- wp:heading -->
    <h2 class="wp-block-heading">
      REST API
    </h2>
    <!-- /wp:heading -->
  </div>
</div>
<!-- /wp:media-text -->
[{
  "name": "core/media-text",
  "attributes": {
    "mediaId": 256,
    "mediaType": "image",
    "mediaPosition": "left",
    "mediaUrl": "http://my.site/api.jpg",
    "mediaWidth": 50
  },
  "innerBlocks": [
    {
      "name": "core/heading",
      "attributes": {
        "content": "REST API",
        "level": 2
      }
    }
  ]
}]

Preact example

An example Preact app app that queries for block data and maps it into customized components.

The example post being queried contains a core/media-text element with an image on the left and core/heading and core/paragraph blocks on the right side:

Screenshot of example media-text post content

The following code uses the REST API to retrieve post and block metadata and map each block onto a custom component.

<!DOCTYPE html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>VIP Block Data API Preact example</title>
</head>

<body></body>

<script type="module">
  import { h, render } from 'https://esm.sh/preact';

  renderPost('https://gutenberg-content-api-test.go-vip.net/wp-json', 55);

  async function renderPost(restUrl, postId) {
    const postResponse = await fetch(`${restUrl}/wp/v2/posts/${postId}`);
    const postTitle = (await postResponse.json())?.title?.rendered;

    const blocksResponse = await fetch(`${restUrl}/vip-block-data-api/v1/posts/${postId}/blocks`);
    const blocks = (await blocksResponse.json())?.blocks;

    const App = Post(postTitle, blocks);
    render(App, document.body);
  }

  function mapBlockToComponent(block) {
    if (block.name === 'core/heading') {
      return Heading(block);
    } else if (block.name === 'core/paragraph') {
      return Paragraph(block);
    } else if (block.name === 'core/media-text') {
      return MediaText(block);
    } else {
      return null;
    }
  }

  /* Components */

  function Post(title, blocks) {
    return h('div', { className: 'post' },
      h('h1', null, title),
      blocks.map(mapBlockToComponent),
    );
  }

  function Heading(props) {
    // Use dangerouslySetInnerHTML for rich text formatting
    return h('h2', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
  }

  function Paragraph(props) {
    // Use dangerouslySetInnerHTML for rich text formatting
    return h('p', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
  }

  function MediaText(props) {
    return h('div', { className: 'media-text' },
      h('div', { className: 'media' },
        h('img', { src: props.attributes.mediaUrl })
      ),
      h('div', { className: 'text' },
        props.innerBlocks ? props.innerBlocks.map(mapBlockToComponent) : null,
      ),
    )
  }
</script>
</html>

The code above produces this HTML from post data:

<div class="post">
  <h1>Post with a media-text</h1>

  <div class="media-text">
    <div class="media">
      <img src="https://gutenberg-content-api-test.go-vip.net/.../api.webp?w=1024">
    </div>

    <div class="text">
      <h2>Heading content</h2>
      <p>Paragraph content</p>
    </div>
  </div>
</div>

Limitations

Client-side blocks

The Block Data API relies on server-side registered blocks to source attributes from HTML. Custom blocks that register via register_block_type() and block.json will automatically be available in the Block Data API. All Gutenberg core blocks are registered server-side.

Modern blocks are likely to be registered server-side and work immediately with the Block Data API. However, some custom blocks may only use registerBlockType() in JavaScript and will not provide server-side registration. For these blocks, some attribute data may be missing. To address this issue, we recommend:

  • Creating a block.json file for each of your site's custom blocks.
  • Using register_block_type() with the block.json file to expose the block information to the server.

For more information on using block.json to enhance block capabilities, read this WordPress core post.

Client-side example

For legacy block content or third-party blocks that are not registered server-side, some attributes may still be available through the Block Data API. For example, this is a hero block that is registered only in JavaScript:

blocks.registerBlockType('wpvip/hero-block', {
    title: __('Hero Block', 'wpvip'),
    icon: 'index-card',
    category: 'text',
    attributes: {
        title: {
            type: 'string',
            source: 'html',
            selector: 'h2',
        },
        mediaURL: {
            type: 'string',
            source: 'attribute',
            selector: 'img',
            attribute: 'src',
        },
        content: {
            type: 'string',
            source: 'html',
            selector: '.hero-text',
        },
        mediaID: {
            type: 'number',
        }
    }
});

The block's output markup will render like this:

<!-- wp:wpvip/hero-block {"mediaID":9} -->
<div class="wp-block-wpvip-hero-block">
    <h2>Hero title</h2>
    <div class="hero-image">
        <img src="http://my.site/uploads/hero-image.png" />
    </div>
    <p class="hero-text">Hero summary</p>
</div>
<!-- /wp:wpvip/hero-block -->

Because the block is not registered server-side, the server is unaware of the block's sourced attributes like title and mediaURL. The Block Data API can only return a subset of the block's attributes:

[{
  "name": "wpvip/hero-block",
  "attributes": {
    "mediaID": 9
  }
}]

mediaID is stored directly in the block's delimiter (<!-- wp:wpvip/hero-block {"mediaID":9} -->), and will be available in the Block Data API. Any other sourced attributes will be missing.

Registering client-side blocks

The example above shows block attributes missing on a client-side block. To fix this problem, the block can be changed to register with a block.json via register_block_type():

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 2,
  "name": "wpvip/hero-block",
  "title": "Hero block",
  "icon": "index-card",
  "category": "text",
  "attributes": {
    "title": {
      "type": "string",
      "source": "html",
      "selector": "h2"
    },
    "mediaURL": {
      "type": "string",
      "source": "attribute",
      "selector": "img",
      "attribute": "src"
    },
    "content": {
      "type": "string",
      "source": "html",
      "selector": ".hero-text"
    },
    "mediaID": {
      "type": "number"
    }
  }
}

The block.json file is used to register the block both server-side and client-side:

In PHP plugin code:

register_block_type( __DIR__ . '/block.json' );

In JavaScript:

import metadata from './block.json';

registerBlockType( metadata, {
    edit: Edit,
    // ...other client-side settings
} );

After server-side registration, the block's full structure is available via the Block Data API:

[{
  "name": "wpvip/hero-block",
  "attributes": {
    "mediaID": 9,
    "title": "Hero title",
    "mediaURL": "http://my.site/uploads/hero-image.png",
    "content": "Hero summary"
  }
}]

Rich text support

Blocks with html-sourced attributes can contain HTML rich-text formatting, but that may not always be apparent. For example, this is an image with a basic plain-text caption:

Image with plain-text caption

The image is saved in WordPress with this markup:

<!-- wp:image {"id":17,"sizeSlug":"large","linkDestination":"media"} -->
<figure class="wp-block-image size-large">
  <a href="https://my.site/wp-content/wpvip.jpg">
    <img src="https://my.site/wp-content/wpvip.jpg" alt="" class="wp-image-17"/>
  </a>

  <figcaption class="wp-element-caption">This is a center-aligned image with a caption</figcaption>
</figure>
<!-- /wp:image -->

The Block Data API uses the caption property definition from core/image's block.json file:

"attributes": {
  "caption": {
    "type": "string",
    "source": "html",
    "selector": "figcaption",
    /* ... */
  },
}

The sourced caption is returned in the Block Data API:

{
  "name": "core/image",
  "attributes": {
    /* ... */
    "caption": "This is a center-aligned image with a caption",
  }
}

Because the caption property in this example is , it seems possible to print the caption to the page safely (e.g. without using innerHTML or React's dangerouslySetInnerHTML). However, this is not the case and may result in incorrect rendering.

Attributes with the html source like the image block caption attribute above can contain plain-text as well as markup.

Image with rich-text caption

Retrieving the caption through the Block Data API yields this result:

{
  "name": "core/image",
  "attributes": {
    /* ... */
    "caption": "This is a caption with <strong>bold text</strong> <a href=\"https://wpvip.com/\">and a link</a>.",
  }
}

caption now contains inline HTML. In order to view rich-text formatting in a decoupled component, direct HTML usage with innerHTML or dangerouslySetInnerHTML is necessary. You could also use the vip_block_data_api__sourced_block_result filter to remove HTML from attributes. Formatting would be removed as well, but the resulting data may be more flexible.

In the future we are considering providing a rich-text data format so that no direct HTML is required to render blocks correctly. This would improve the flexibility of the Block Data API in non-browser locations such as in native mobile applications. For now, however, some direct HTML is still required to render blocks with rich formatting.

Deprecated blocks

When core or custom editor blocks are updated to a new version, block attributes can change. This can result in the Block Data API returning a different block structure for the same block type depending on when the post containing a block was authored.

For example, the core/list block was updated in 2022 from storing list items in the values attribute to use innerBlocks instead. Before this change, a list with two items was structured like this:

<!-- wp:list -->
<ul>
  <li>List item 1</li>
  <li>List item 2</li>
</ul>
<!-- /wp:list -->

The resulting attributes for a core/list block pulled from the Block Data API would be structured like this:

{
  "name": "core/list",
  "attributes": {
    "ordered": false,
    "values": "<li>List item 1</li><li>List item 2</li>"
  }
}

List items are stored as HTML in the values attribute, which is not an ideal structure for mapping to custom components. After the core/list block was updated in WordPress, the same two-item list block is represented this way in HTML:

<!-- wp:list -->
<ul>
  <!-- wp:list-item -->
  <li>List item 1</li>
  <!-- /wp:list-item -->

  <!-- wp:list-item -->
  <li>List item 2</li>
  <!-- /wp:list-item -->
</ul>
<!-- /wp:list -->

The resulting core/list item from the Block Data API parses the list items as core/list-item children in innerBlocks:

{
  "name": "core/list",
  "attributes": {
    "ordered": false,
    "values": ""
  },
  "innerBlocks": [
    {
      "name": "core/list-item",
      "attributes": {
        "content": "List item 1"
      }
    },
    {
      "name": "core/list-item",
      "attributes": {
        "content": "List item 2"
      }
    }
  ]
}

Deprecated blocks can be a tricky problem when using the Block Data API to render multiple versions of the same block. A core/list block from a post in 2021 has a different data shape than a core/list block created in 2023. Consumers of the API need to be aware of legacy block structures in order to implement custom frontend components. This issue applies to custom blocks as well; if a block has legacy markup saved in the database, this can result in legacy block representation in the Block Data API.

We are considering ways to mitigate this problem for consumers of the API, such as implementing server-side block deprecation rules or providing type structures to represent legacy block data shapes. For now, ensure that Block Data API consumers test against older content to ensure that legacy block versions used in content are covered by code.

Filters

Block Data API filters can be applied to limit access to the REST API and modify the output of parsed blocks.


vip_block_data_api__rest_validate_post_id

Limit which post IDs are valid in the REST API. By default, all posts with post_status set to publish are valid.

/**
 * Validates that a post can be queried via the Block Data API REST endpoint.
 * Return false to disable access to a post.
 *
 * @param boolean $is_valid Whether the post ID is valid for querying.
 *                          Defaults to true when post status is 'publish'.
 * @param int     $post_id  The queried post ID.
 */
return apply_filters( 'vip_block_data_api__rest_validate_post_id', $is_valid, $post_id );

For example, this filter can be used to allow only pages that are published to be available:

add_filter( 'vip_block_data_api__rest_validate_post_id', function( $is_valid, $post_id ) {
    // Only allow published pages
    return 'page' === get_post_type( $post_id ) && 'publish' === get_post_status( $post_id );
}, 10, 2);

vip_block_data_api__rest_permission_callback

Limit Block Data API access to specific users or roles.

/**
 * Validates that a request can access the Block Data API. This filter can be used to
 * limit access to authenticated users.
 * Return false to disable access.
 *
 * @param boolean $is_permitted Whether the request is permitted. Defaults to true.
 */
return apply_filters( 'vip_block_data_api__rest_permission_callback', true );

Warning: Authenticated requests to the Block Data API will bypass WPVIP's built-in REST API caching. Review Caching on WPVIP for more information.

By default no authentication is required, as posts must be published to be available on the Block Data API. If limited access is desired (e.g. via Application Password credentials) this filter can be used to check user permissions:

add_filter( 'vip_block_data_api__rest_permission_callback', function( $is_permitted ) {
    // Require authenticated user access with 'publish_posts' permission
    return current_user_can( 'publish_posts' );
});

vip_block_data_api__sourced_block_result

Modify or add attributes to a block's output in the Block Data API.

/**
 * Filters a block when parsing is complete.
 *
 * @param array  $sourced_block An associative array of parsed block data with keys 'name' and 'attribute'.
 * @param string $block_name    The name of the parsed block, e.g. 'core/paragraph'.
 * @param string $post_id       The post ID associated with the parsed block.
 * @param string $block         The result of parse_blocks() for this block.
 *                              Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys.
 */
$sourced_block = apply_filters( 'vip_block_data_api__sourced_block_result', $sourced_block, $block_name, $post_id, $block );

This is useful when block rendering requires attributes stored in post metadata or outside of a block's markup. This filter can be used to add attributes to any core or custom block. For example:

add_filter( 'vip_block_data_api__sourced_block_result', 'add_custom_block_metadata', 10, 4 );

function add_custom_block_metadata( $sourced_block, $block_name, $post_id, $block ) {
    if ( 'wpvip/my-custom-block' !== $block_name ) {
        return $sourced_block;
    }

    // Add custom attribute to REST API result
    $sourced_block['attributes']['custom-attribute-name'] = 'custom-attribute-value';

    return $sourced_block;
}

Direct block HTML can be accessed through $block['innerHTML']. This may be useful if manual HTML parsing is necessary to gather data from a block.

For another example of how this filter can be used to extend block data, we have implemented a default image block filter in src/parser/block-additions/core-image.php. This filter is automatically called on core/image blocks to add width and height to image attributes.

Caching on WPVIP

All requests to the Block Data API on WPVIP will automatically be cached for 1 minute. Be aware that authenticated requests will bypass this cache, so be very cautious when using the REST permissions filter.

More information about WPVIP's caching can be found here.

Errors and Warnings

Error: vip-block-data-api-no-blocks

The VIP Block Data API is designed to parse structured block data, and can not read content from WordPress before the release of Gutenberg in WordPress 5.0 or created using the classic editor plugin. If the parser encounters post content that does not contain block data, this error will be returned with an HTTP 400 response code:

{
  "code": "vip-block-data-api-no-blocks",
  "message": "Error parsing post ID ...: This post does not appear to contain block content. The VIP Block Data API is designed to parse Gutenberg blocks and can not read classic editor content."
}

Error: vip-block-data-api-parser-error

If any unexpected errors are encountered during block parsing, the block API will return error data with an HTTP 500 response code:

{
  "code": "vip-block-data-api-parser-error",
  "message": "..."
}

The full stack trace for the error will be available in the site's logs:

[29-Mar-2023 07:42:58 UTC] PHP Warning: vip-block-data-api (<version>): Exception: ...
Stack trace:
#0 ...

If you encounter an error, we would really appreciate it if you could create a bug report so that we can understand and fix the issue.

Warning: Unregistered block type

The Block Data API requires blocks to be server-side registered in order to return full block attributes. When the plugin encounters post content containing a block that is not registered, a warning will be returned with block data:

{
  "blocks": [{
    "name": "wpvip/client-side-block",
    "attributes": { /* ... */ }
  }],
  "warnings": [
      "Block type 'wpvip/client-side-block' is not server-side registered. Sourced block attributes will not be available."
  ]
}

These warnings indicate blocks that are missing from the server-side registry. Review the Client-side blocks section for information about this limitation, which attributes will be accessible in client-side blocks, and recommendations for registering custom blocks server-side.

Development

Tests

Run tests locally with wp-env and Docker:

wp-env start
composer install
composer run test