In this post, we will build a section of case/portfolio showcases. Generally, a post-title block in a query loop block can link to a single post. However, we want to link up to the actual project link.
So, there are two sub-tasks to develop this kind of section.
- A metabox to store the actual project link. Previously, in the classic editor, there was a long procedure to store post meta. However, the Gutenberg page editor offers a JavaScript-based workaround, making it easier than the traditional PHP method.
- Modify the output of the post-title block in the query loop block. As a reputable developer, you want to have control over this modified output. To achieve this added control, we will introduce attributes and a controller.
Ready the environment
I prefer to implement all functionalities within a separate plugin. Please refer to this introductory article to understand my reasoning.
A WordPress plugin is a package of code that can be installed on a WordPress website to add new features or functionality.
Since the plugin will include some block elements, I prefer to use the @wordpress/create-block package.
First, check npm and node version. This package requires node version 20.10.0+ and npm version 10.2.3+
Open the terminal and navigate to the wp-content/plugins folder. Then, enter the following command:
npx @wordpress/create-block@latest wpdevagent
Then, navigate to wpdevagent
folder and run npm run start
command.
I recommend using @wordpress/eslint-plugin and @wordpress/prettier-config for linting and formatting JavaScript code according to WordPress standards.
Register post type
Open the wpdevagent.php
file and add the following lines to register the post type after the block initialization hook. There are various options for the register_post_type function, but for simplicity, I will outline the minimal implementation.
if ( ! function_exists( 'wpdevagent_post_types' ) ) {
/**
* Register post types for WPDevAgent
*
* @since 1.0.0
*/
function wpdevagent_post_types() {
register_post_type(
'wpdevagent_case',
array(
'labels' => array(
'name' => _x( 'Cases', 'Post type general name', 'wpdevagent' ),
),
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
'public' => true,
)
);
}
}
add_action( 'init', 'wpdevagent_post_types' );
Custom field support is necessary since post meta is a type of custom field.
Register post meta
Once we have registered the custom post type, the next step is to register the post meta.
if ( ! function_exists( 'wpdevagent_case_post_meta' ) ) {
/**
* Custom post meta
*/
function wpdevagent_case_post_meta() {
register_post_meta(
'wpdevagent_case',
'_wpdevagent_case_link',
array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'label' => __( 'Case link', 'case' ),
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
'sanitize_callback' => 'esc_url_raw',
)
);
}
}
add_action( 'init', 'wpdevagent_case_post_meta' );
We declare that the name of the post meta is _wpdevagent_case_link. This post meta is only available when the post type is wpdevagent_case.
- show_in_rest need to set true for that, this meta is available in gutenberg editor.
- Currently, the meta field stores a single object value. We hope to enable the storage of an array of object values in post-meta in future versions.
- For this task, we store strings. In addition, we can store booleans, integers, numbers, arrays, and objects.
- Only users with the
edit_posts
permission can store post meta, as this is necessary for managing private metadata.
Add metabox
We want to create a meta box using JavaScript. However, we won’t need edit.js, editor.scss, save.js, style.scss, or view.js. Also, please remove the relevant lines from block.json.
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
We do not need the example code for the register block type, so please clean up the index.js file.
In Gutenberg, register component in the sidebar means register plugin. To use this utility, we required @wordpress/plugins package.
npm install @wordpress/plugins --save
In the index.js
file, we write down these codes.
import { registerPlugin } from '@wordpress/plugins';
import CASE_LINK from './case-link-meta-field';
registerPlugin( 'case-link', {
render() {
return <CASE_LINK />;
},
} );
- In the
index.js
file, import theregisterPlugin
function from the@wordpress/plugins
package. - In the
registerPlugin
function, the first argument specifies the plugin’s namespace. The second argument is an object that will render a custom component calledCASE_LINK
. - I want to separate the problem. I will use another file to define
CASE_LINK
.CASE_LINK
is imported from a file namedcase-link-meta-file.js
. We must create this file before writing the component.
In case-link-meta-field.js file, we will use
- Internationalization ( __ ) function from @wordpress/i18n package.
- I will define compose function to using HOCs ( Higher Order Components ). For that, I am going to install @wordpress/compose package.
- I will use withSelect function to select the post meta and post type. Beside that, I will use withDispatch function to dispatch or send the changed data to store. These will come from @wordpress/data package.
- PluginDocumentSettingPanel is used to create a panel on sidebar. This utility comes from @wordpress/editor package.
- To use the gutenberg like meta box, I am going to use PanelRow and TextControl. This utilities are defined in @wordpress/componets package.
/**
* Internationalization utilities for client-side localization.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
/**
* The compose package is a collection of handy Hooks and Higher Order Components (HOCs) you can use to wrap your WordPress components.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-compose/
*/
import { compose } from '@wordpress/compose';
/**
* WordPress’ data module serves as a hub to manage application state for both plugins and
* WordPress itself, providing tools to manage data within and between distinct modules.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/
*/
import { withSelect, withDispatch } from '@wordpress/data';
/**
* Having an awareness of the concept of a WordPress post,
* it associates the loading and saving mechanism of the value
* representing blocks to a post and its content.
*
* @see https://www.npmjs.com/package/@wordpress/editor
*/
import { PluginDocumentSettingPanel } from '@wordpress/editor';
/**
* This package includes a library of generic WordPress components
* to be used for creating common UI elements shared between screens and features of the WordPress dashboard.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/components/
*/
import { TextControl, PanelRow } from '@wordpress/components';
const CASE_LINK = ( { postType, postMeta, setPostMeta } ) => {
if ( 'wpdevagent_case' !== postType ) {
return null;
}
return (
<PluginDocumentSettingPanel
title={ __( 'Case Link', 'wpdevagent' ) }
initialOpen="true"
>
<PanelRow>
<TextControl
label={ __( 'Write case url', 'wpdevagent' ) }
value={ postMeta._wpdevagent_case_link }
onChange={ ( value ) =>
setPostMeta( { _wpdevagent_case_link: value } )
}
help={
__( 'Meta Field', 'case' ) + ': _wpdevagent_case_link'
}
/>
</PanelRow>
</PluginDocumentSettingPanel>
);
};
export default compose( [
withSelect( ( select ) => {
return {
postMeta: select( 'core/editor' ).getEditedPostAttribute( 'meta' ),
postType: select( 'core/editor' ).getCurrentPostType(),
};
} ),
withDispatch( ( dispatch ) => {
return {
setPostMeta( newMeta ) {
dispatch( 'core/editor' ).editPost( { meta: newMeta } );
},
};
} ),
] )( CASE_LINK );
Here, the complex functionaly lies on compose(). This tell that, defaltly export CASE_LINK constant where some arguments ( postMeta, postType and setPostMeta ) used.
- postMeta: withSelect() function is used to select wp library’s core/editor data and get post attribute. Here, we need the ‘meta’ attribute of current post.
- postType: In this variable, we collect the name of the current post type entry.
- setPostMeta() is used to change the meta value with new value.
At this stage, we get a text area input field in case.

Add controller with post-title block
Now, we going to add controler in a way,
- User can control that post-title link-up with
_wpdevagent_case_link
.
From page editor, we just need to say – is this go to case link or not. For control this behavior, we just use a toggler.
We use a seperate file to write all the logic about post-title modification. Let’s say, the name of this file will be case-link-post-title.js
. Create this file at src folder, at same level as index.js
Import case-link-post-title.js in index.js by just add a line
import './case-link-post-title';
With this import, the index.js
file is
import { registerPlugin } from '@wordpress/plugins';
import CASE_LINK from './case-link-meta-field';
import './case-link-post-title';
registerPlugin( 'case-link', {
render() {
return <CASE_LINK />;
},
} );
In case-link-post-title.js
file, we will use a component group and two filter.
/**
* A lightweight & efficient EventManager for JavaScript.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-hooks/
*/
import { addFilter } from '@wordpress/hooks';
/**
* This module allows you to create and use standalone block editors.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/
*/
import { InspectorControls } from '@wordpress/block-editor';
/**
* This package includes a library of generic WordPress components to be used for
* creating common UI elements shared between screens and features of the WordPress dashboard.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/
*/
import {
Panel,
PanelBody,
PanelRow,
ToggleControl,
} from '@wordpress/components';
/**
* Internationalization utilities for client-side localization.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
/**
* The compose package is a collection of handy Hooks and Higher Order Components (HOCs) you can use to wrap your WordPress components.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-compose/
*/
import { createHigherOrderComponent } from '@wordpress/compose';
addFilter(
'blocks.registerBlockType',
'wpdevagent/query-title',
( settings, name ) => {
if ( name !== 'core/post-title' ) {
return settings;
}
return {
...settings,
attributes: {
...settings.attributes,
isCaseLink: {
type: 'boolean',
default: false,
},
},
};
}
);
function Edit( props ) {
const setCaseLink = () => {
props.setAttributes( {
isCaseLink: ! props.attributes.isCaseLink,
} );
};
return (
<InspectorControls>
{ props.attributes.isLink && (
<Panel>
<PanelBody>
<PanelRow>
<ToggleControl
label={ __(
'Link to _wpdevagent_case_link meta',
'case'
) }
checked={ props.attributes.isCaseLink }
onChange={ setCaseLink }
/>
</PanelRow>
</PanelBody>
</Panel>
) }
</InspectorControls>
);
}
addFilter(
'editor.BlockEdit',
'wpdevagent/query-loop-post-title',
createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
if ( 'core/post-title' !== props.name ) {
return <BlockEdit { ...props } />;
}
return (
<>
<Edit { ...props } />
<BlockEdit { ...props } />
</>
);
};
} )
);
Using these codes creates a toggle controller in the post title block within the query loop block.

Let’s become more familiar with this code.
addFilter
is a JS function like PHP add_filter function. The general syntax for addFilter function is
addFilter( 'hookName', 'namespace', callback, priority )
- The post-title block can link with _wpdevagent_case_link meta or not. To define this, we will add additional attribute of post-title block. To add additional attribute
blocks.registerBlockType
hook is perfect. - To control this attribute, we will add a additional control. I think, a toggler is perfect for this. To add additional control
editor.BlockEdit
is right hook.
InspectorControls is a component that appears in the post settings sidebar while a block is being edited. The components Panel, PanelBody, PanelRow and ToggleControl are used to arrange InspectorControls.
Modify the output
For change the output of the post-title block, we will use render_block_{$this->name} . Also, this example could help.
Add this codes at wpdevagent.php
/**
* Filter button blocks for possible link attributes
*
* @param string $block_content string The block content about to be rendered.
* @param array $block The full block, including name add attributes.
* @returns string $block_content The block content about to be rendered.
*/
function filter_post_title_render_block( $block_content, $block ) {
if ( isset( $block['attrs']['isCaseLink'] ) && true === $block['attrs']['isCaseLink'] ) {
$p = new WP_HTML_Tag_Processor( $block_content );
if ( $p->next_tag( 'a' ) ) {
$p->set_attribute( 'href', get_post_meta( get_the_ID(), '_case_link', true ) );
$p->set_attribute( 'rel', 'bookmark' );
}
$block_content = $p->get_updated_html();
}
return $block_content;
}
add_filter( 'render_block_core/post-title', 'filter_post_title_render_block', 10, 2 );
First we hook with core/post-title where the arguments are $block_content string and $block array.
- Check the $block array that it has isCaseLink attributes and its value is true.
- If yes than process $block_content with WP_HTML_Tag_Processor class.
- When <a> tag found, set the attribute href and rel.
- href attribute set as _wpdevagent_case_link post meta.
- The $block_content string is updated with get_updated_html()
Finally, the query loop block’s post title block link to the value in link mentioned in post meta.
