WordPress.org

WordPress Developer Blog

Creating a custom block that stores post meta

Post meta is one of the most used tools in a WordPress developer’s toolbox. Up until WordPress 5.0 and the release of the block editor, it was the only way of storing data meant to be associated with a post instance. A developer would add a meta box in the post editor to allow input of the data and then access that stored data in a theme or plugin template to display.

Now with the ability to create custom blocks and block patterns, you don’t always need to store that data in post meta, you can use a block attribute and store it as part of the post content. The philosophical question of when to use post meta or post content is a bit beyond the scope of this article but at a high level the answer most likely lies in whether the data being stored will be queried against or be shown outside of the single template of the post type it’s associated with.

A real-world use-case

For the purposes of this tutorial, assume you have a client that has a website with an existing custom post type called Product. They want to be able to enter a testimonial for products and then display the testimonials for the five most recent Products with testimonials on the homepage of the website. The testimonial also needs to store the author’s name and website URL, but they are only displayed on the single product template.

Defining our approach

Based on the specifications of the request, you know that you need to store three pieces of data:

  1. The testimonial content.
  2. The author’s name. 
  3. The author’s website URL.

You also know that the site owner wants to show the content of the testimonial for the five most recent Products that have testimonials, so based on that it can be inferred that the testimonial content needs to be stored in post meta because the intent is to query against it. The author’s name and URL can be stored with the post content as part of a block because they are never queried against and are only displayed on the single project template.

Now that you know where you’re storing the items, it’s time to determine what you will store them in. 

Because a testimonial is just some quoted text, the Quote, and Pullquote blocks are good possible candidates for use here. You could even extend them to store the author’s email and website URL using block filters. However in this case, you need to have the block save the testimonial content to post meta and these blocks store their content in the post content. Additionally, you’ll need to retrieve the testimonial content directly from WordPress using the get_post_meta() function so you’ll need to use a dynamic block rather than a static block.

It looks like the solution is to create a custom, dynamic block.

Setting up the Post Meta

Your fictional client has a Product post type already registered on their site with the following snippet.

register_post_type(
	'product',
	array(
		'labels'       => array(
			'name'          => __( 'Products', 'tutorial' ),
			'singular_name' => __( 'Product', 'tutorial' ),
		),
		'public'       => true,
		'has_archive'  => true,
		'show_in_rest' => true,
		'supports'     => array(
			'title',
			'editor',
			'thumbnail',
			'excerpt',
			'custom-fields',
		),
	)
);

In order to access any post meta in the Post Editor, it needs to be exposed to the REST API. This can be done using the register_post_meta function making sure to set show_in_rest to true. The snippet below would register the testimonial post meta for the product custom post type and ensure that the data saved is sanitized for the database by passing the wp_kses_post function to the sanitize_callback argument.

register_post_meta(
	'product',
	'testimonial',
	array(
		'show_in_rest'       => true,
		'single'             => true,
		'type'               => 'string',
		'sanitize_callback'  => 'wp_kses_post',
	)
);

Scaffolding the Block

Now that the post meta has been registered for the existing post type, you need to create a block that will interact with it. You can use the @wordpress/create-block tool to create a plugin that will register the block. Please note that it is entirely possible to register a block from inside a theme, and in some cases that approach may be a better choice. For the purposes of this tutorial, the code will be contained in a plugin to reduce the amount of setup needed.

Run the following command in the wp-content/plugins directory to scaffold a new plugin that contains a single dynamic block. 

npx @wordpress/create-block post-meta-testimonial --variant dynamic --namespace tutorial --category text --short-description "A block quote style testimonial that saves to post meta."

You are using the --variant flag here to tell the tool that you want to generate a dynamic block in order to render the front end in PHP and not save the block out to the post content. The other flags are defining some other properties for the block such as the category is should appear under, the description that is displayed in the block editor, and the namespace that is used when registering the block

For more information on the create-block package and it’s options, please refer to the official documentation.

It is also possible to only scaffold the block files by using the --no-plugin flag. This is a great option for adding a new block to existing plugins. Just be sure to run the command from inside a src directory in your plugin.

Once the command has finished running, there will be a new directory called post-meta-tutorial in the plugins directory with a working dynamic block. Be sure to go to the Plugins page and activate it before continuing.

In the terminal of your choice, start the build process for the block by running npm run start from inside the wp-content/plugins/post-meta-tutorial directory.

The start command is one of many commands provided by the @wordpress/scripts package that is used to build blocks.

This command will watch the files in the src directory for changes and recompile them as needed.

You can now insert the scaffolded block into the editor.

Modifying the block.json

Now that you have the block scaffolded and the build process running, you can start adding your customizations to what has been scaffolded.

Start by defining the two attributes to store the authors name and email; authorName and authorURL. Both attributes are of type string and have no default value.

"attributes": {
	"authorName": {
		"type": "string"
	},
	"authorURL": {
		"type": "string"
	}
},

For more information on block attributes, please refer to the official documentation.

Note: There is technically a way of connecting a block attribute to post meta via block.json, however, it is deprecated and should not be used.

Next, you’ll need to be able to get information about where the block is located. More specifically, you will need to get access to the current post ID and type. This can be done with the usesContext property.

This property allows a block to access information provided by an ancestor, or parent, of the block. In this case, we’re accessing information that is provided by the Block editor itself and is available to any block. Go ahead and add postId and postType to the usesContext property.

"usesContext": ["postId", "postType"]

For more information on usesContext, please refer to the official documentation.

Next, let’s change the icon that is associated with this block to something that better represents its usage. We can make that change in the icon property.

"icon": "format-quote"

As this block accesses a single piece of meta for the entire post, there should only be a single instance of this block allow in a post. You can manage that in the supports property by setting multiple to false

"supports": {
	"html": false,
	"multiple": false
},

If you want to see a preview of the block in the block inserter, you can add an example property as well.

"example": {
	"attributes": {
		"authorName": "WordPress",
		"authorURL": "developer.wordpress.org/news"
	}
},

Feel free to tweak any of the other properties in the block.json file to your liking but at this point, you should have a block.json file whose contents look something like this.

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "tutorial/post-meta-testimonial",
	"version": "0.1.0",
	"title": "Post Meta Testimonial",
	"category": "text",
	"icon": "format-quote",
	"description": "A block quote style testimonial that saves to post meta.",
	"supports": {
		"html": false,
		"multiple": false
	},
	"example": {
		"attributes": {
			"authorName": "WordPress",
			"authorURL": "developer.wordpress.org/news"
		}
	},
	"attributes": {
		"authorName": {
			"type": "string"
		},
		"authorURL": {
			"type": "string"
		}
	},
	"usesContext": [ "postId", "postType" ],
	"textdomain": "post-meta-testimonial",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php"
}

Creating the UI in edit.js

Now that block.json has been updated, you can start working on how the block functions in the Block editor. Open the scaffolded edit.js file. Typically this file is heavily commented, but for the purposes of this tutorial, they have been simplified so the file now looks like this.

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';

/**
 * Internal dependencies
 */
import './editor.scss';

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @return {WPElement} Element to render.
 */
export default function Edit() {
	return (
		<p { ...useBlockProps() }>
			{ __(
				'Post Meta Testimonial – hello from the editor!',
				'post-meta-testimonial'
			) }
		</p>
	);
}

Updating the markup and styles

Start by updating the JSX to use a blockquote tag that contains a p and cite tag. The cite tag will contain two span tags that will contain the author’s name and author’s URL, with the latter being wrapped in an a tag. This will be the same markup that will be displayed on the front end.

export default function Edit() {
	return (
		<blockquote { ...useBlockProps() }>
			<p>Testimonial will go here</p>
			<cite>
				<span>Author Name</span>
				<br />
				<span>
					<a href="#">Author URL</a>
				</span>
			</cite>
		</blockquote>
	);
}

Next, add the following CSS to the style.scss file to give your block the look of a quote.

.wp-block-tutorial-post-meta-testimonial {
	border-left: 0.25em solid;
	margin: 0 0 1.75em;
	padding-left: 1em;
	border-width: 1px;
}

You shoud now have a block that looks like this:

Adding the components and interactivity.

Now that the markup and styles are in place, you can start add some interactivity. First, update the Edit component to access the items passed to it. The Edit component receives a props object that you can extract items from using object destructuring. You’ll need access to the attributes object, the setAttributes function, and the contents of the context object.

export default function Edit( {
	attributes,
	setAttributes,
	context: { postType, postId },
} ) {
	return (
		<blockquote { ...useBlockProps() }>
			<p>Testimonial will go here</p>
			<cite>
				<span>Author Name</span>
				<br />
				<span>
					<a href="#">Author URL</a>
				</span>
			</cite>
		</blockquote>
	);
}

Next, you’ll need to provide some places for the user to enter the information. While you could use some input field components such as TextControl, the RichText component is a great option here as it provides a great WYSIWYG experience and provides some basic formatting controls.

Import the RichText component from the @wordpress/block-editor package.

import { useBlockProps, RichText } from '@wordpress/block-editor';

The RichText component supports a number of props but you will only need the following six for this tutorial:

  • tagName: The tag name of the editable element.
  • value: The content of the field
  • onChange:  Called when the value changes.
  • placeholder: Placeholder text to show when the field is empty
  • allowedFormats: The list of allowed formats such as bold, italics, etc.
  • disableLineBreaks: Whether the instance allows line breaks.

You will need three instances of the RichText component to manage the information that this block manages. One for the testimonial content and one for each of the attributes that will store the author information.

Update the contents of the cite tag with the following

export default function Edit( {
	attributes: { authorName, authorURL },
	setAttributes,
	context: { postType, postId },
} ) {
	return (
		<blockquote { ...useBlockProps() }>
			<p>Testimonial goes here</p>
			<cite>
				<RichText
					tagName="span"
					placeholder={ __( 'Author name', 'tutorial' ) }
					allowedFormats={ [] }
					disableLineBreaks
					value={ authorName }
					onChange={ ( newAuthorName ) =>
						setAttributes( { authorName: newAuthorName } )
					}
				/>
				<br />
				<span>
					<RichText
						tagName="a"
						placeholder={ __( 'Author URL', 'tutorial' ) }
						allowedFormats={ [] }
						disableLineBreaks
						value={ authorURL }
						onChange={ ( newAuthorURL ) =>
							setAttributes( { authorURL: newAuthorURL } )
						}
					/>
				</span>
			</cite>
		</blockquote>
	);
}

For more information on the RichText component, see the source code.

Retrieving and updating the post meta

Before you can populate props for the RichText component that will be used to write the testimonial content, you’ll need to get access to the post meta for the current post. To do that, import the useEntityProp hook from the @wordpress/core-data package.

import { useEntityProp } from '@wordpress/core-data';

This hook accepts four parameters:

  • kind: The entity kind.
  • name: The entity name.
  • prop: The property name.
  • id: An entity ID to use instead of the context-provided one.

In this case, you’ll set the kind parameter to postType, the name parameter to product to  access properties for the Product post type, and the prop parameter to meta.

Finally, you can pass the postId that the block has received from the context object as the id parameter to ensure that you retrieve the specific post meta for the post instance being referred to. This is important so that the block can retrieve the correct post meta values regardless of whether it was placed in the post content, the Site editor, or even into a Query loop block template.

useEntityProp returns an array that contains the data that was queried in the first index and a function to update the data in the second. For those that have used the useState hook before, this will look familiar.

You can use array destructuring to extract the items and give them names that correspond to their function; in this case meta and updateMeta respectively.

Lastly, the meta variable is an object containing all of the post meta registered for this post type. You only need to work with the testimonial meta that was registered and can again use object destructuring to access that as well.

As the testimonial meta is only registered to the Product post type, you will run into errors with the block using it on other content types.

export default function Edit( {
	attributes: { authorName, authorURL },
	setAttributes,
	context: { postType, postId },
} ) {
	const [ meta, updateMeta ] = useEntityProp(
		'postType',
		'product',
		'meta',
		postId
	);

	const { testimonial } = meta;
	return (
		<blockquote { ...useBlockProps() }>
			<p>Testimonial goes here</p>
			<cite>
				<RichText
					tagName="span"
					placeholder={ __( 'Author name', 'tutorial' ) }
					allowedFormats={ [] }
					disableLineBreaks
					value={ authorName }
					onChange={ ( newAuthorName ) =>
						setAttributes( { authorName: newAuthorName } )
					}
				/>
				<br />
				<span>
					<RichText
						tagName="a"
						placeholder={ __( 'Author URL', 'tutorial' ) }
						allowedFormats={ [] }
						disableLineBreaks
						value={ authorURL }
						onChange={ ( newAuthorURL ) =>
							setAttributes( { authorURL: newAuthorURL } )
						}
					/>
				</span>
			</cite>
		</blockquote>
	);
}

Now that you have the items it needs, you can populate the RichText component that will be used for the testimonial content. Set the value property to use the testimonial variable and inside of the onChange add a call to the updateMeta function. This function expects an object of meta and you want to ensure that you don’t affect any other meta when updating testimonial.

This can be achieved by using the spread operator to add all of the meta to the new object. By adding the testimonial property and setting the value afterwards, it has the effect of overriding any items in the meta object with the same key. In effect, you are creating a new object with all of the existing meta attributes unchanged with the exception of testimonial

export default function Edit( {
	attributes: { authorName, authorURL },
	setAttributes,
	context: { postType, postId },
} ) {
	const [ meta, updateMeta ] = useEntityProp(
		'postType',
		'product',
		'meta',
		postId
	);

	const { testimonial } = meta;
	return (
		<blockquote { ...useBlockProps() }>
			<RichText
				placeholder={ __( 'Testimonial goes here', 'tutorial' ) }
				tagName="p"
				value={ testimonial }
				onChange={ ( newTestimonialContent ) =>
					updateMeta( {
						...meta,
						testimonial: newTestimonialContent,
					} )
				}
			/>
			<cite>
				<RichText
					tagName="span"
					placeholder={ __( 'Author name', 'tutorial' ) }
					allowedFormats={ [] }
					disableLineBreaks
					value={ authorName }
					onChange={ ( newAuthorName ) =>
						setAttributes( { authorName: newAuthorName } )
					}
				/>
				<br />
				<span>
					<RichText
						tagName="a"
						placeholder={ __( 'Author URL', 'tutorial' ) }
						allowedFormats={ [] }
						disableLineBreaks
						value={ authorURL }
						onChange={ ( newAuthorURL ) =>
							setAttributes( { authorURL: newAuthorURL } )
						}
					/>
				</span>
			</cite>
		</blockquote>
	);
}

At this point, you should have a working block that displays and allows for updating the Testimonial content. You should notice that when you update the Testimonial content, that the post meta is not immediately saved but rather, the changes will take effect in the database once the post is saved.

Updating the front end in the render.php

To display the testimonial content on the front end, you’ll need to make some changes to the render.php file that the dynamic block will use to render the markup of the block.

This file should be thought of as a template for the front end of the block and as such should only contain markup that is meant to be displayed, similar to templates used with the get_template_part function, and is not the best place for defining functions or classes in PHP.

Open the scaffolded render.php file which should look like this

<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php esc_html_e( 'Post Meta Testimonial – hello from a dynamic block!', 'post-meta-testimonial' ); ?>
</p>

Next, update the markup to match what was done in JSX.

<blockquote <?php echo get_block_wrapper_attributes(); ?>>
	<p>Testimonial will go here</p>
	<cite>
		<span>Author Name</span>
		<br />
		<span>
			<a href="#">Author URL</a>
		</span>
	</cite>
</blockquote>

The render.php has access to three variable related to the block:

  1. $attributes: An array containing all of the block attributes
  2. $content: An inner block content. 
  3. $block: A reference the block instances

For the purposes of this tutorial, you’ll need to use $attributes to retrieve the authorName and authorEmail information and $block to retrieve the post ID that is being passed via usesContext

$author_name = $attributes['authorName'];
$author_url  = $attributes['authorURL'];

For the content of the testimonial, you’ll use the get_post_meta() function to retrieve the testimonial post meta. To do this, you will need to get the current post ID.

Get the post ID from the $block->context array, which is populated with the items you added in the usesContext property of block.json, and then call the get_post_meta() function passing the post ID, the name of the key and true to indicate that only a single piece of post meta should be returned.

$testimonial = get_post_meta( $block->context['postId'], 'testimonial', true );
$author_name = $attributes['authorName'];

The markup for this block is very simple but now you have a working block that can display post meta on the front end.

<?php
global $post;
$testimonial = get_post_meta( $post->ID, 'testimonial', true );
$author_name = $attributes['authorName'];
$author_url  = $attributes['authorURL'];
?>
<blockquote <?php echo get_block_wrapper_attributes(); ?>>
	<p><?php echo esc_html( $testimonial ); ?></p>
	<cite>
		<span><?php echo esc_html( $author_name ); ?></span>
		<br />
		<span>
			<a href="<?php echo esc_url( $author_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $author_url ); ?></a>
		</span>
	</cite>
</blockquote>

Working with blocks that support post meta has many levels and use cases but hopefully this tutorial helps to provide a good baseline.

What post meta related topics would you like to see in future tutorials?

Thank you to @bph, @greenshady, and @fabiankaegy for reviewing this post.

18 responses to “Creating a custom block that stores post meta”

  1. bikramchettri Avatar

    Can we create also create PluginSidebar using wordpress/create-block package?

    1. Ryan Welcher Avatar

      Out of the box, no. There is no direct support for scaffolding PluginSidebar ( or any of the available SlotFill ). It is possible to set this up manually but it will require a non-trivial amount of work.

      What might be a great idea is creating an external project template for create-block (https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/packages-create-block-external-template ) that can set this up.

      1. bikramchettri Avatar

        const { get } = require(“lodash”);
        const path = require(“path”);
        const glob = require(“glob”);
        const defaultConfig = require(“wordpress/scripts/config/webpack.config”);
        const { getWebpackEntryPoints } = require(“wordpress/scripts/utils/config”);

        module.exports = {
        …defaultConfig,
        entry: {
        …getWebpackEntryPoints(),
        …getSidebarEntries(),
        },
        output: {
        …get(defaultConfig, “output”, {}),
        filename: “[name].js”,
        path: path.resolve(process.cwd(), “build”),
        },
        };

        function getSidebarEntries() {
        const basePath = path.join(__dirname, “src/sidebars/”);
        const pattern = “**/index.js”;
        const files = glob.sync(pattern, { cwd: basePath });
        return files.reduce((entries, file) => {
        const name = path.dirname(file);
        entries[name] = path.join(basePath, file);
        return entries;
        }, {});
        }

        1. bikramchettri Avatar

          Change entries[name] to entries[`sidebars/${name}/index`]
          function getSidebarEntries() {
          const basePath = path.join(__dirname, “src”, “sidebars”);
          const pattern = “**/index.js”;
          const files = glob.sync(pattern, { cwd: basePath });
          return files.reduce((entries, file) => {
          const name = path.dirname(file);
          entries[`sidebars/${name}/index`] = path.join(basePath, file);
          return entries;
          }, {});
          }

  2. Sören Wrede Avatar

    Thanks for this article.
    Where do we use the context postType?

    1. Ryan Welcher Avatar

      The postType context can be used in place of where we hardcoded the product post type in the useEntityProp hook.

  3. Dominique Pijnenburg Avatar

    If I add a bold, italic, etc. word in the `testimonial`, upon saving it’s being stripped from that styling. When I modify the block to save the `testimonial` as an attribute, that styling isn’t stripped

    Why is this happening and what can be done to preserve the styling?

    1. Justin Tadlock Avatar

      I haven’t tested this against the code, but it’s likely sanitize_textarea_field in the post meta registration that strips it. You could replace this with something like wp_kses_post or another function that allows the HTML that you want.

      1. Dominique Pijnenburg Avatar

        You are correct, if I apply “`wp_kses_post“` on the registered post_meta and in the render template, it’s working. Thank you. 😊

        1. Ryan Welcher Avatar

          Thank you for caling this out! My choice of sanitization function was a little too strict it seems! I’ll update the post accordingly!

  4. victorkane Avatar

    I really want to thank you for this tutorial/article. It literally brought everything together for me, and is just what I need right now to get started with my projects (using “no-plugin but my own” plus pure block theme created by WP plugin create block theme). If you’re trying to empower people with your teachings, you are achieving that wholly with me!

  5. Deryck Avatar

    This is a great tutorial. I have to say I feel more comfortable working with dynamic blocks. I think it gives us a lot more control and is more future proof.

    It’s easier to break a standard block but of course if we don’t need dynamic data that should be the way to go.

  6. George Avatar

    Thank you for this tutorial. If I understand correctly, WP provides the context automatically? Is it possible to provide our own arbitrary postId from a parent custom block?

    1. George Avatar

      I just realised that we can use a query loop for that matter. Thanks!

  7. David Lewis Avatar
    David Lewis

    Console keeps telling me right side of assignment cannot be destructured i.e.

    const { testimonial } = meta;

    I’m guessing the post meta isn’t being registered correctly? But I’ve tried “everything”

  8. David Lewis Avatar
    David Lewis

    Don’t forget to declare support for custom-fields when registering the post type. I was using a post type I already had registered so I skipped the very first step and missed that.

  9. Margarita Avatar
    Margarita

    Thank you for this guide. It’s very useful!

    One thing I didn’t really get: in which case we use the $block->context for the get_post_meta function and in which case the ID from the global $post?

    1. twobyte Avatar

      That got me too. This is what I used for render.php in the end:

      if(!function_exists('post_meta_render_callback')) {
          function post_meta_render_callback( $attributes, $content, $block ) {
              if ( ! isset( $block-&gt;context['postId'] ) ) {
                  return '';
              }
              $testimonial = get_post_meta( $block-&gt;context['postId'], 'testimonial', true );
              $author_name = $attributes['authorName'];
      	    $author_url  = $attributes['authorURL'];
          ?&gt;
          &lt;blockquote &gt;
              
              <cite>
                  
                  
                  
                      &lt;a href=&quot;" target="_blank" rel="noopener noreferrer"&gt;</a>
                  
              </cite>
          </blockquote>
          &lt;?php
          }
      }
      echo post_meta_render_callback($attributes, $content, $block );
      

Leave a Reply

Your email address will not be published. Required fields are marked *