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:
- The testimonial content.
- The author’s name.
- 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 fieldonChange
: Called when the value changes.placeholder
: Placeholder text to show when the field is emptyallowedFormats
: 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:
$attributes
: An array containing all of the block attributes$content
: An inner block content.$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.
Leave a Reply