Overview

In this tutorial, we’ll cook up a custom post type with custom fields and its own block theme templates! Just like a signature dish needs the perfect presentation, your post type will come with tailored templates that integrate seamlessly with any active block theme. Follow along as we mix the right ingredients to create a fully customized content experience! 🍽️📜👨‍🍳

Celebrity Chef

ndiego Avatar

This fantastic recipe comes from the talented Nick Diego, a true master in the world of blocks! Known for his brilliant creations like Block Visibility and the Icon Block, Nick brings his expertise and creativity to every dish he crafts. His work is a testament to innovation and flavor, making this recipe a must-try for any block enthusiast! 🍽️✨

Setup

You can choose to either use the repository which provides a development environment or to just download the standalone plugin

Standalone

Instructions

Run the following command in a terminal of your choice from inside the plugins directory of your local WordPress installation.

Zsh
npx @wordpress/create-block@latest your-people --template @block-developer-cookbook/your-people

Once the scaffold has completed completed, start the build process from inside the newly created plugin

Zsh
cd your-people && npm run start

Finally, make sure to activate the plugin.

Repository

Instructions

Checkout the repository (skip this step if already done)

Zsh
git clone git@github.com:ryanwelcher/block-developer-cookbook.git

Install the dependencies

Zsh
npm install

Start the development environment (make sure you have Docker installed )

Zsh
npm run env start

Run the following script from the root of the repository

Zsh
npm run prep:your-people

Once the scaffold has completed completed, start the build process from inside the newly created plugin

Zsh
cd plugins/your-people && npm run start

Step 1 – Register the data

This recipe requires a fair amount of setup to create the custom post type, the taxonomy, and all of the custom fields that will be associated with each person. To streamline the process, the template provides the majority of it for yuor

Open the your-people.php file and take a minute to familiarize yourself with the code

your-people.php
<?php
/**
 * Plugin Name:       Your People
 * Description:       Create a custom block to display team members or staff.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           1.0.0
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       your-people
 *
 * @package block-developers-cookbook
 */

namespace BlockDevelopersCookbook;

/**
 * Register the 'person' custom post type.
 */
function register_person_post_type() {
	$labels = array(
		'name'                  => _x( 'People', 'Post type general name', 'your-people' ),
		'singular_name'         => _x( 'Person', 'Post type singular name', 'your-people' ),
		'menu_name'             => _x( 'People', 'Admin Menu text', 'your-people' ),
		'name_admin_bar'        => _x( 'Person', 'Add New on Toolbar', 'your-people' ),
		'add_new'               => __( 'Add New Person', 'your-people' ),
		'add_new_item'          => __( 'Add New Person', 'your-people' ),
		'new_item'              => __( 'New Person', 'your-people' ),
		'edit_item'             => __( 'Edit Person', 'your-people' ),
		'view_item'             => __( 'View Person', 'your-people' ),
		'all_items'             => __( 'All People', 'your-people' ),
		'search_items'          => __( 'Search People', 'your-people' ),
		'not_found'             => __( 'No people found.', 'your-people' ),
		'not_found_in_trash'    => __( 'No people found in Trash.', 'your-people' ),
		'featured_image'        => _x( 'Person Profile Image', 'Overrides the "Featured Image" phrase', 'your-people' ),
		'set_featured_image'    => _x( 'Set profile image', 'Overrides the "Set featured image" phrase', 'your-people' ),
		'remove_featured_image' => _x( 'Remove profile image', 'Overrides the "Remove featured image" phrase', 'your-people' ),
		'use_featured_image'    => _x( 'Use as profile image', 'Overrides the "Use as featured image" phrase', 'your-people' ),
	);

	$args = array(
		'labels'             => $labels,
		'public'             => true,
		'has_archive'        => 'people',
		'hierarchical'       => false,
		'menu_icon'          => 'dashicons-groups',
		'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
		'show_in_rest'       => true,
	);

	register_post_type( 'person', $args );
}
add_action( 'init', __NAMESPACE__ . '\register_person_post_type' );

/**
 * Register the 'role' taxonomy for the 'person' post type.
 */
function register_role_taxonomy() {
	$labels = array(
		'name'              => _x( 'Roles', 'taxonomy general name', 'your-people' ),
		'singular_name'     => _x( 'Role', 'taxonomy singular name', 'your-people' ),
		'search_items'      => __( 'Search Roles', 'your-people' ),
		'all_items'         => __( 'All Roles', 'your-people' ),
		'parent_item'       => __( 'Parent Role', 'your-people' ),
		'parent_item_colon' => __( 'Parent Role:', 'your-people' ),
		'edit_item'         => __( 'Edit Role', 'your-people' ),
		'update_item'       => __( 'Update Role', 'your-people' ),
		'add_new_item'      => __( 'Add New Role', 'your-people' ),
		'new_item_name'     => __( 'New Role Name', 'your-people' ),
		'menu_name'         => __( 'Role', 'your-people' ),
	);

	$args = array(
		'labels'            => $labels,
		'show_admin_column' => true,
		'query_var'         => true,
		'rewrite'           => array( 'slug' => 'role' ),
		'show_in_rest'      => true,
	);

	register_taxonomy( 'role', array( 'person' ), $args );
}
add_action( 'init', __NAMESPACE__ . '\register_role_taxonomy' );

/**
 * Register custom meta fields for the 'person' post type.
 */
function register_person_meta() {
	register_post_meta(
		'person',
		'yp_job_title',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Job Title', 'your-people' ),
			'description'       => __( 'The person\'s job title', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);
}
add_action( 'init', __NAMESPACE__ . '\register_person_meta' );

/**
 * Register the plugin templates.
 */
function register_plugin_templates() {
	// Register your custom templates here.
}
add_action( 'init', __NAMESPACE__ . '\register_plugin_templates' );

/**
 * Helper function to get the content of a template file.
 *
 * @param string $template The name of the template file.
 * @return string The content of the template file.
 */
function get_template_content( $template ) {
	ob_start();
	include plugin_dir_path( __FILE__ ) . "/templates/{$template}";
	return ob_get_clean();
}

// Include some files we need
require_once __DIR__ . '/includes/admin.php';
require_once __DIR__ . '/includes/editor.php';

There is a lot going on here but basically, this code is registering the person custom post type, role taxonomy, and a single custom field. When registering post types, taxonomies and custom fields it’s extremely important to make sure you set the show_in_rest parameter to true to ensure that these items are available to the block editor via the REST API.

There are 4 more custom fields to register so go ahead and add those to the register_person_meta functon now

your-people.php
<?php
/**
 * Plugin Name:       Your People
 * Description:       Create a custom block to display team members or staff.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           1.0.0
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       your-people
 *
 * @package block-developers-cookbook
 */

namespace BlockDevelopersCookbook;

/**
 * Register the 'person' custom post type.
 */
function register_person_post_type() {
	$labels = array(
		'name'                  => _x( 'People', 'Post type general name', 'your-people' ),
		'singular_name'         => _x( 'Person', 'Post type singular name', 'your-people' ),
		'menu_name'             => _x( 'People', 'Admin Menu text', 'your-people' ),
		'name_admin_bar'        => _x( 'Person', 'Add New on Toolbar', 'your-people' ),
		'add_new'               => __( 'Add New Person', 'your-people' ),
		'add_new_item'          => __( 'Add New Person', 'your-people' ),
		'new_item'              => __( 'New Person', 'your-people' ),
		'edit_item'             => __( 'Edit Person', 'your-people' ),
		'view_item'             => __( 'View Person', 'your-people' ),
		'all_items'             => __( 'All People', 'your-people' ),
		'search_items'          => __( 'Search People', 'your-people' ),
		'not_found'             => __( 'No people found.', 'your-people' ),
		'not_found_in_trash'    => __( 'No people found in Trash.', 'your-people' ),
		'featured_image'        => _x( 'Person Profile Image', 'Overrides the "Featured Image" phrase', 'your-people' ),
		'set_featured_image'    => _x( 'Set profile image', 'Overrides the "Set featured image" phrase', 'your-people' ),
		'remove_featured_image' => _x( 'Remove profile image', 'Overrides the "Remove featured image" phrase', 'your-people' ),
		'use_featured_image'    => _x( 'Use as profile image', 'Overrides the "Use as featured image" phrase', 'your-people' ),
	);

	$args = array(
		'labels'             => $labels,
		'public'             => true,
		'has_archive'        => 'people',
		'hierarchical'       => false,
		'menu_icon'          => 'dashicons-groups',
		'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
		'show_in_rest'       => true,
	);

	register_post_type( 'person', $args );
}
add_action( 'init', __NAMESPACE__ . '\register_person_post_type' );

/**
 * Register the 'role' taxonomy for the 'person' post type.
 */
function register_role_taxonomy() {
	$labels = array(
		'name'              => _x( 'Roles', 'taxonomy general name', 'your-people' ),
		'singular_name'     => _x( 'Role', 'taxonomy singular name', 'your-people' ),
		'search_items'      => __( 'Search Roles', 'your-people' ),
		'all_items'         => __( 'All Roles', 'your-people' ),
		'parent_item'       => __( 'Parent Role', 'your-people' ),
		'parent_item_colon' => __( 'Parent Role:', 'your-people' ),
		'edit_item'         => __( 'Edit Role', 'your-people' ),
		'update_item'       => __( 'Update Role', 'your-people' ),
		'add_new_item'      => __( 'Add New Role', 'your-people' ),
		'new_item_name'     => __( 'New Role Name', 'your-people' ),
		'menu_name'         => __( 'Role', 'your-people' ),
	);

	$args = array(
		'labels'            => $labels,
		'show_admin_column' => true,
		'query_var'         => true,
		'rewrite'           => array( 'slug' => 'role' ),
		'show_in_rest'      => true,
	);

	register_taxonomy( 'role', array( 'person' ), $args );
}
add_action( 'init', __NAMESPACE__ . '\register_role_taxonomy' );

/**
 * Register custom meta fields for the 'person' post type.
 */
function register_person_meta() {
	register_post_meta(
		'person',
		'yp_job_title',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Job Title', 'your-people' ),
			'description'       => __( 'The person\'s job title', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_institution',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Institution/Employer', 'your-people' ),
			'description'       => __( 'The person\'s institution or employer', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_location',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Location', 'your-people' ),
			'description'       => __( 'The person\'s location', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_website_url',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Website', 'your-people' ),
			'description'       => __( 'The person\'s website', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_linkedin_url',
		array(
			'show_in_rest' => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'LinkedIn URL', 'your-people' ),
			'description'       => __( 'The person\'s LinkedIn URL', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);
}
add_action( 'init', __NAMESPACE__ . '\register_person_meta' );

/**
 * Register the plugin templates.
 */
function register_plugin_templates() {
	// Register your custom templates here.
}
add_action( 'init', __NAMESPACE__ . '\register_plugin_templates' );

/**
 * Helper function to get the content of a template file.
 *
 * @param string $template The name of the template file.
 * @return string The content of the template file.
 */
function get_template_content( $template ) {
	ob_start();
	include plugin_dir_path( __FILE__ ) . "/templates/{$template}";
	return ob_get_clean();
}

// Include some files we need
require_once __DIR__ . '/includes/admin.php';
require_once __DIR__ . '/includes/editor.php';

Step 2 – Managing the custom fields

Now that all of the custom fields are in place, you need a way to edit them in the block editor.

Jump over to the your-people.js file inside the src directory.

your-people.js
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/editor';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';

function SocialLinksPanel() {
	return (
		<>
			<PluginDocumentSettingPanel
				name="yp-general-info"
				title={ __( 'General Info', 'your-people' ) }
				className="yp-general-info"
				initialOpen={ true }
			>
				<TextControl
					label={ __( 'Title', 'your-people' ) }
					value={ '' }
					help={ __( "The person's job title(s).", 'your-people' ) }
				/>
			</PluginDocumentSettingPanel>
		</>
	);
}

registerPlugin( 'yp-social-links-panel', { render: SocialLinksPanel } );

In this file you’re going to be using SlotFill to add panels to sidebar of the Person post where you can set the values of the custom fields. This initial template demonstrates how to register the plugin that contains the slot and it’s content to be added.

As is, this code will add a new panel to the sidebar with a single TextControl component to mange the person title field. However, it’s not actually connected to the custom field at this point.

To do this you will need to use the useEntityProp hook. This is a React hook that provides a way to retrieve and set the meta.

Adding the following code to the file:

your-people.js
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/editor';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';

function SocialLinksPanel() {
	const [ meta, setMeta ] = useEntityProp( 'postType', 'person', 'meta' );
	return (
		<>
			<PluginDocumentSettingPanel
				name="yp-general-info"
				title={ __( 'General Info', 'your-people' ) }
				className="yp-general-info"
				initialOpen={ true }
			>
				<TextControl
					label={ __( 'Title', 'your-people' ) }
					value={ meta?.yp_job_title || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_job_title: newValue,
						} )
					}
					help={ __( "The person's job title(s).", 'your-people' ) }
				/>
			</PluginDocumentSettingPanel>
		</>
	);
}

registerPlugin( 'yp-social-links-panel', { render: SocialLinksPanel } );

The concept of entities can be a little confusing but you can thing of them as a data source. You can read more about them in the official documentation.

The useEnityProp hook is receiving three parameters:

  • Entity kind. In your case this is postType but it could also be taxonomy.
  • Entity name. The specific type of entity you wish to access.
  • Prop. The information you want to access.

Once called, the hook returns an array of values, commonly referred to a as a tuple, where the first item is the data you’re requesting and the second is a function that can be used to save the data.

Using array destructuring, it’s possible to name these variable whatever you’d like. In your case, meta and setMeta but these can be anything but there is a best practice to use {{variable}} and set{{Variable}} when naming.

JavaScript
const [ personInfo, setPersonInfo ] = useEntityProp( 'postType', 'person', 'meta' );

The rest of the change in the file are updating the TextControl to use the new meta values. The value prop is using optional chaining to display the title field if it is set or an empty string

JavaScript
value={ meta?.yp_job_title || '' }

The onChange property is then updating the meta when the TextControl is updated and saving the meta. The setMeta function received an object of the meta values to save for this post.

JavaScript
onChange={ ( newValue ) =>
	setMeta( {
		...meta,
		yp_job_title: newValue,
	} )
}

One thing to note there is that when calling setMeta, ALL of the meta for the post is saved and you need to be sure that you’re only changing the yp_job_title value. This is done by using the JavaScript spread operator to add all of the existing values in the meta variable and then we only change the value of the individual meta field being changed.

This ensures we’re only change the field for this control and that all existing meta is not lost.

There are 9 more custom fields that you need to manage so go ahead and update the code with the following

your-people.js
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/editor';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';

function SocialLinksPanel() {
	const [ meta, setMeta ] = useEntityProp( 'postType', 'person', 'meta' );
	return (
		<>
			<PluginDocumentSettingPanel
				name="yp-general-info"
				title={ __( 'General Info', 'your-people' ) }
				className="yp-general-info"
				initialOpen={ true }
			>
				<TextControl
					label={ __( 'Title', 'your-people' ) }
					value={ meta?.yp_job_title || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_job_title: newValue,
						} )
					}
					help={ __( "The person's job title(s).", 'your-people' ) }
				/>
				<TextControl
					label={ __( 'Employer/Institution', 'your-people' ) }
					value={ meta?.yp_institution || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_institution: newValue,
						} )
					}
				/>
				<TextControl
					label={ __( 'Location', 'your-people' ) }
					value={ meta?.yp_location || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_location: newValue,
						} )
					}
				/>
				<TextControl
					label={ __( 'Personal Website', 'your-people' ) }
					value={ meta?.yp_website_url || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_website_url: newValue,
						} )
					}
				/>
				<TextControl
					label={ __( 'LinkedIn URL', 'your-people' ) }
					value={ meta?.yp_linkedin_url || '' }
					onChange={ ( newValue ) =>
						setMeta( {
							...meta,
							yp_linkedin_url: newValue,
						} )
					}
				/>
			</PluginDocumentSettingPanel>
		</>
	);
}

registerPlugin( 'yp-social-links-panel', { render: SocialLinksPanel } );

This code will add more fields and a second panel to the sidebar of the post. Take a second to make sure the field are saving and retrieving the custom fields as expected.

Step 3 – The templates

At this point, everything is in place to save and mange the team members. However, as the post type is stored in a plugin, there is no way that the active theme can know what needs to be displayed for the templates associated with the Person post type.

It’s possible to into the Templates section of the Site Editor and manually create a template for each one but there is a much better way to do this using the register_block_template function to add templates from your plugin to the used in the active theme

register_block_template accepts two parameters:

  • The name of the template. This name must follow the plugin_uri//template_name format
  • An array of arguments such as the label, content and some others

Update the register_plugin_templates function to register the provided single post template that is stored in the templates directory

your-people.php
<?php
/**
 * Plugin Name:       Your People
 * Description:       Create a custom block to display team members or staff.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           1.0.0
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       your-people
 *
 * @package block-developers-cookbook
 */

namespace BlockDevelopersCookbook;

/**
 * Register the 'person' custom post type.
 */
function register_person_post_type() {
	$labels = array(
		'name'                  => _x( 'People', 'Post type general name', 'your-people' ),
		'singular_name'         => _x( 'Person', 'Post type singular name', 'your-people' ),
		'menu_name'             => _x( 'People', 'Admin Menu text', 'your-people' ),
		'name_admin_bar'        => _x( 'Person', 'Add New on Toolbar', 'your-people' ),
		'add_new'               => __( 'Add New Person', 'your-people' ),
		'add_new_item'          => __( 'Add New Person', 'your-people' ),
		'new_item'              => __( 'New Person', 'your-people' ),
		'edit_item'             => __( 'Edit Person', 'your-people' ),
		'view_item'             => __( 'View Person', 'your-people' ),
		'all_items'             => __( 'All People', 'your-people' ),
		'search_items'          => __( 'Search People', 'your-people' ),
		'not_found'             => __( 'No people found.', 'your-people' ),
		'not_found_in_trash'    => __( 'No people found in Trash.', 'your-people' ),
		'featured_image'        => _x( 'Person Profile Image', 'Overrides the "Featured Image" phrase', 'your-people' ),
		'set_featured_image'    => _x( 'Set profile image', 'Overrides the "Set featured image" phrase', 'your-people' ),
		'remove_featured_image' => _x( 'Remove profile image', 'Overrides the "Remove featured image" phrase', 'your-people' ),
		'use_featured_image'    => _x( 'Use as profile image', 'Overrides the "Use as featured image" phrase', 'your-people' ),
	);

	$args = array(
		'labels'             => $labels,
		'public'             => true,
		'has_archive'        => 'people',
		'hierarchical'       => false,
		'menu_icon'          => 'dashicons-groups',
		'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
		'show_in_rest'       => true,
	);

	register_post_type( 'person', $args );
}
add_action( 'init', __NAMESPACE__ . '\register_person_post_type' );

/**
 * Register the 'role' taxonomy for the 'person' post type.
 */
function register_role_taxonomy() {
	$labels = array(
		'name'              => _x( 'Roles', 'taxonomy general name', 'your-people' ),
		'singular_name'     => _x( 'Role', 'taxonomy singular name', 'your-people' ),
		'search_items'      => __( 'Search Roles', 'your-people' ),
		'all_items'         => __( 'All Roles', 'your-people' ),
		'parent_item'       => __( 'Parent Role', 'your-people' ),
		'parent_item_colon' => __( 'Parent Role:', 'your-people' ),
		'edit_item'         => __( 'Edit Role', 'your-people' ),
		'update_item'       => __( 'Update Role', 'your-people' ),
		'add_new_item'      => __( 'Add New Role', 'your-people' ),
		'new_item_name'     => __( 'New Role Name', 'your-people' ),
		'menu_name'         => __( 'Role', 'your-people' ),
	);

	$args = array(
		'labels'            => $labels,
		'show_admin_column' => true,
		'query_var'         => true,
		'rewrite'           => array( 'slug' => 'role' ),
		'show_in_rest'      => true,
	);

	register_taxonomy( 'role', array( 'person' ), $args );
}
add_action( 'init', __NAMESPACE__ . '\register_role_taxonomy' );

/**
 * Register custom meta fields for the 'person' post type.
 */
function register_person_meta() {
	register_post_meta(
		'person',
		'yp_job_title',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Job Title', 'your-people' ),
			'description'       => __( 'The person\'s job title', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);
	
	register_post_meta(
		'person',
		'yp_institution',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Institution/Employer', 'your-people' ),
			'description'       => __( 'The person\'s institution or employer', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_location',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Location', 'your-people' ),
			'description'       => __( 'The person\'s location', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_website_url',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Website', 'your-people' ),
			'description'       => __( 'The person\'s website', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_linkedin_url',
		array(
			'show_in_rest' => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'LinkedIn URL', 'your-people' ),
			'description'       => __( 'The person\'s LinkedIn URL', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

}
add_action( 'init', __NAMESPACE__ . '\register_person_meta' );

/**
 * Register the plugin templates.
 */
function register_plugin_templates() {
	// Register your custom templates here.
	register_block_template( 'your-people//single-person', [
		'title'       => __( 'Single Person', 'your-people' ),
		'description' => __( 'Displays a single person\'s profile page.', 'your-people' ),
		'content'     => get_template_content( 'single-person.php' )
	] );
}
add_action( 'init', __NAMESPACE__ . '\register_plugin_templates' );


 * Helper function to get the content of a template file.
 *
 * @param string $template The name of the template file.
 * @return string The content of the template file.
 */
function get_template_content( $template ) {
	ob_start();
	include plugin_dir_path( __FILE__ ) . "/templates/{$template}";
	return ob_get_clean();
}

// Include some files we need
require_once __DIR__ . '/includes/admin.php';
require_once __DIR__ . '/includes/editor.php';

This change will register the template and uses the `get_template_content` helper function to retrieve the content from the PHP files and assign it to the template.

Save the file and navigate to the Templates section of the site editor to see your new template!

The last step is to add an Archive template for the Person post type and a template for the Role taxonomy

your-people.php
<?php
/**
 * Plugin Name:       Your People
 * Description:       Create a custom block to display team members or staff.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           1.0.0
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       your-people
 *
 * @package block-developers-cookbook
 */

namespace BlockDevelopersCookbook;

/**
 * Register the 'person' custom post type.
 */
function register_person_post_type() {
	$labels = array(
		'name'                  => _x( 'People', 'Post type general name', 'your-people' ),
		'singular_name'         => _x( 'Person', 'Post type singular name', 'your-people' ),
		'menu_name'             => _x( 'People', 'Admin Menu text', 'your-people' ),
		'name_admin_bar'        => _x( 'Person', 'Add New on Toolbar', 'your-people' ),
		'add_new'               => __( 'Add New Person', 'your-people' ),
		'add_new_item'          => __( 'Add New Person', 'your-people' ),
		'new_item'              => __( 'New Person', 'your-people' ),
		'edit_item'             => __( 'Edit Person', 'your-people' ),
		'view_item'             => __( 'View Person', 'your-people' ),
		'all_items'             => __( 'All People', 'your-people' ),
		'search_items'          => __( 'Search People', 'your-people' ),
		'not_found'             => __( 'No people found.', 'your-people' ),
		'not_found_in_trash'    => __( 'No people found in Trash.', 'your-people' ),
		'featured_image'        => _x( 'Person Profile Image', 'Overrides the "Featured Image" phrase', 'your-people' ),
		'set_featured_image'    => _x( 'Set profile image', 'Overrides the "Set featured image" phrase', 'your-people' ),
		'remove_featured_image' => _x( 'Remove profile image', 'Overrides the "Remove featured image" phrase', 'your-people' ),
		'use_featured_image'    => _x( 'Use as profile image', 'Overrides the "Use as featured image" phrase', 'your-people' ),
	);

	$args = array(
		'labels'             => $labels,
		'public'             => true,
		'has_archive'        => 'people',
		'hierarchical'       => false,
		'menu_icon'          => 'dashicons-groups',
		'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
		'show_in_rest'       => true,
	);

	register_post_type( 'person', $args );
}
add_action( 'init', __NAMESPACE__ . '\register_person_post_type' );

/**
 * Register the 'role' taxonomy for the 'person' post type.
 */
function register_role_taxonomy() {
	$labels = array(
		'name'              => _x( 'Roles', 'taxonomy general name', 'your-people' ),
		'singular_name'     => _x( 'Role', 'taxonomy singular name', 'your-people' ),
		'search_items'      => __( 'Search Roles', 'your-people' ),
		'all_items'         => __( 'All Roles', 'your-people' ),
		'parent_item'       => __( 'Parent Role', 'your-people' ),
		'parent_item_colon' => __( 'Parent Role:', 'your-people' ),
		'edit_item'         => __( 'Edit Role', 'your-people' ),
		'update_item'       => __( 'Update Role', 'your-people' ),
		'add_new_item'      => __( 'Add New Role', 'your-people' ),
		'new_item_name'     => __( 'New Role Name', 'your-people' ),
		'menu_name'         => __( 'Role', 'your-people' ),
	);

	$args = array(
		'labels'            => $labels,
		'show_admin_column' => true,
		'query_var'         => true,
		'rewrite'           => array( 'slug' => 'role' ),
		'show_in_rest'      => true,
	);

	register_taxonomy( 'role', array( 'person' ), $args );
}
add_action( 'init', __NAMESPACE__ . '\register_role_taxonomy' );

/**
 * Register custom meta fields for the 'person' post type.
 */
function register_person_meta() {
	register_post_meta(
		'person',
		'yp_job_title',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Job Title', 'your-people' ),
			'description'       => __( 'The person\'s job title', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_institution',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Institution/Employer', 'your-people' ),
			'description'       => __( 'The person\'s institution or employer', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_location',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Location', 'your-people' ),
			'description'       => __( 'The person\'s location', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_website_url',
		array(
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'Website', 'your-people' ),
			'description'       => __( 'The person\'s website', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);

	register_post_meta(
		'person',
		'yp_linkedin_url',
		array(
			'show_in_rest' => true,
			'single'            => true,
			'type'              => 'string',
			'label'             => __( 'LinkedIn URL', 'your-people' ),
			'description'       => __( 'The person\'s LinkedIn URL', 'your-people' ),
			'sanitize_callback' => 'wp_strip_all_tags'
		)
	);
}
add_action( 'init', __NAMESPACE__ . '\register_person_meta' );

/**
 * Register the plugin templates.
 */
function register_plugin_templates() {
	// Register your custom templates here.
	register_block_template( 'your-people//single-person', [
		'title'       => __( 'Single Person', 'your-people' ),
		'description' => __( 'Displays a single person\'s profile page.', 'your-people' ),
		'content'     => get_template_content( 'single-person.php' )
	] );

	register_block_template( 'your-people//archive-person', [
		'title'       => __( 'Archive Person', 'your-people' ),
		'description' => __( 'Displays a list of people.', 'your-people' ),
		'content'     => get_template_content( 'archive-person.php' )
	] );

	register_block_template( 'your-people//taxonomy-role', [
		'title'       => __( 'Role Archive', 'your-people' ),
		'description' => __( 'Displays a list of people in a specific role.', 'your-people' ),
		'content'     => get_template_content( 'taxonomy-role.php' )
	] );
}
add_action( 'init', __NAMESPACE__ . '\register_plugin_templates' );

/**
 * Helper function to get the content of a template file.
 *
 * @param string $template The name of the template file.
 * @return string The content of the template file.
 */
function get_template_content( $template ) {
	ob_start();
	include plugin_dir_path( __FILE__ ) . "/templates/{$template}";
	return ob_get_clean();
}

// Include some files we need
require_once __DIR__ . '/includes/admin.php';
require_once __DIR__ . '/includes/editor.php';

Great job! Order up!