Let’s get started by registering a Custom Post Type in WordPress. Post Types are the varieties of content such as posts, pages, taxonomies, products, etc. We’re going to add a new post type, person. WordPress makes it easy to add a new custom post type with a little effort.


In this example, we’re going to:

1 ) Create a custom post type “Person”

2 ) Register Meta Fields to the “Person” post type

3) Add Meta Fields to the “Person” post type

4 ) Create a Gutenberg block “Profile”

5 ) Display the Person’s information with the block “Profile” using REST API.

6 ) Popup modal to display additional information about the Person (React Modal)

7 ) Writing PHP Unit tests for the REST API and Custom Post Types.


Basically, you can call it a “profile card” plugin that displays the profile of any person in the community using the block “Profile”.

Note that the examples are based on the Class and OOP context.

1) Create a custom post type “Person”

Firstly, we’ll need to register our custom post type “Person” using the register_post_type function.

    add_action('init', [ $this, 'registerPersonCPT' ]);
    
    /**
     * Register a person post type. The post type is named "person".
     *
     * @return void.
     */
    public function registerPersonCPT()
    {

          $labels = apply_filters('co_person_post_type_labels', [
            'name' => esc_html__('Persons', 'company-overview'),
            'singular_name' => esc_html__('Person', 'company-overview'),
            'plural_name' => esc_html__('Persons', 'company-overview'),
            'add_new' => esc_html__('Add New', 'company-overview'),
            'add_new_item' => esc_html__('Add New Person', 'company-overview'),
            'edit_item' => esc_html__('Edit Person', 'company-overview'),
            'new_item' => esc_html__('New Person', 'company-overview'),
            'all_items' => esc_html__('All Persons', 'company-overview'),
            'search_items' => esc_html__('Search Person', 'company-overview'),
            'not_found' => esc_html__('No Person Found', 'company-overview'),
            'not_found_in_trash' => esc_html__('No Person Found in trash', 'company-overview'),
            'parent_item_colon' => '’',
            'menu_name' => 'Persons',
          ]);

          // Custom post type arguments, which can be filtered if needed.
          $args = apply_filters(
              'person_post_type_args',
              [
                'labels' => $labels,
                'description' => esc_html__('Displays the overview of the person of the company', 'company-overview'),
                'public' => false,
                'show_ui' => true,
                'menu_position' => 5,
                'menu_icon' => 'dashicons-businessperson',
                'supports' => [ 'title', 'editor', 'custom-fields' ],
                'show_in_rest' => true,
              ]
          );

        register_post_type('person', $args);
    }

There are a couple of props to notice “show_in_rest” => true, which allows us to retrieve the person’s CPT information via REST API. “show_ui” displays the Person CPT UI just like posts, pages in the admin panel.

At this point, we already have a UI with our custom post type.

Custom Post Type

This post type supports title, editor, and custom fields which means, if you click on Add New to add a new person, you’ll already see Title and Editor. We’ll need to fill up the custom fields we’ll need, which I’ll explain below in step 2.


2) Register Meta Fields to the “Person” post type

Now that we have a Person post type. We’re adding the Name of the person in the Title field inside the editor and Person Description in the body of the editor itself. But, we’ll need other information about the person. So, we’re going to add the following fields:

  • Image
  • Position in the company
  • Facebook
  • Xing
  • Linkedin
  • Github

Let’s add the fields in the same registerPersonCPT() method below register_post_type('person', $args);

// Post Meta.
$fields = array( 'image', 'position', 'facebook', 'github', 'xing', 'linkedin' );

foreach ( $fields as $field ) {

			register_post_meta(
				'co_person',
				$field,
				[
					'type'         => 'string',
					'single'       => true,
					'default'      => '',
					'show_in_rest' => true
				] );
}


3) Add Meta Fields to the “Person” Post Type

Now that we’ve registered meta fields. We’ll create meta boxes to add meta fields to the custom post type. Previously, with the classic editor, we’d create meta boxes add_meta_box function. The meta boxes created using this function still work on the block editor. Now there are new ways to extend, giving more power to the developer and a better experience for the authors. We’re building meta boxes with block editor context. Let’s create a file fields.js.

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/
 *
 */
import { __ } from '@wordpress/i18n';

/**
 * Registering a UI to edit Document settings.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/slotfills/plugin-document-setting-panel/
 */
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';

/**
 * The compose package is a collection of handy Hooks and Higher Order Components (HOCs) you can use to wrap your WordPress components and provide some basic features like: state, instance id, pure…
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-compose/
 */
import { compose } from '@wordpress/compose';

/**
 * Read and update post from inside the component.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/
 */
import { withSelect, withDispatch } from '@wordpress/data';

/**
 * Get required utilities from @wordpress/components
 */
import { TextControl, PanelBody, PanelRow } from '@wordpress/components';

/**
 * Get Image upload field.
 */
import Image  from './image-upload';

const Fields = ( { postType, postMeta, setPostMeta } ) => {

	// Do not run on other post types.
	if ( 'person' !== postType ) {
		return null;
	}

   return ( <PluginDocumentSettingPanel title={ __( 'Additional Information', 'company-overview') } initialOpen="true">

				<Image />
				<PanelBody>
					<PanelRow>
						<TextControl
							label={ __( 'Position in the company', 'company-overview' ) }
							value={ postMeta.position }
							onChange={ ( value ) => setPostMeta( { position: value } ) }
						/>
					</PanelRow>
				</PanelBody>

				<PluginDocumentSettingPanel title={ __( 'Social Media Links', 'company-overview') } initialOpen="true">
					<PanelBody>
						<PanelRow>
							<TextControl
								label={ __( 'Facebook', 'company-overview' ) }
								value={ postMeta.facebook }
								onChange={ ( value ) => setPostMeta( { facebook: value } ) }
							/>

						</PanelRow>
					</PanelBody>

					<PanelBody>
						<PanelRow>
							<TextControl
								label={ __( 'Github', 'company-overview' ) }
								value={ postMeta.github }
								onChange={ ( value ) => setPostMeta( { github: value } ) }
							/>
						</PanelRow>
					</PanelBody>

					<PanelBody>
						<PanelRow>
							<TextControl
								label={ __( 'LinkedIn', 'company-overview' ) }
								value={ postMeta.linkedin }
								onChange={ ( value ) => setPostMeta( { linkedin: value } ) }
							/>
						</PanelRow>
					</PanelBody>
				</PluginDocumentSettingPanel>

			</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 } );
			}
		};
	} )
] )( Fields );

The code block above is already documented with reference links. Basically, we’re adding the Additional Settings (Custom Meta Fields) for our custom post type using the PluginDocumentSettingPanel utility. This allows us to register a UI in document edit settings.

Other things to note down are compose, withSelect, withDispatch. withSelect allows us to retrieve the post meta value stored in the post meta table, while withDispatch is used to update the post meta upon post publish/update. The compose package is a collection of handy Hooks and Higher Order Components (HOCs) you can use to wrap your WordPress components.

We’re also importing image upload field from another file, image-upload.js which reads:

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/
 *
 */
import { __ } from '@wordpress/i18n';

/**
 * Import media utilities.
 *
 * @see https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor/src/components/media-upload
 */
import { MediaUpload, MediaUploadCheck } from  '@wordpress/block-editor';

/**
 * The compose package is a collection of handy Hooks and Higher Order Components (HOCs) you can use to wrap your WordPress components and provide some basic features like: state, instance id, pure…
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-compose/
 */
import { compose } from '@wordpress/compose';

/**
 * Read and update post from inside the component.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/
 */
import { withSelect, withDispatch } from '@wordpress/data';

/**
 * Similar to above. WithSelect, withDispatch
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/
 */
import { select, useSelect } from '@wordpress/data';

/**
 * Required react Hooks.
 */
import { useState } from '@wordpress/element';

/**
 * Get required utilities from @wordpress/components
 */
import { PanelBody, PanelRow, Button,  ResponsiveWrapper  } from '@wordpress/components';

const Image = ( { postMeta, setPostMeta } ) => {

	// Media Id.
	const [ mediaId, setMediaId ] = useState( postMeta.image );

	// Get Image object from mediaId.
	const image = useSelect( () => select( 'core' ).getMedia( mediaId ) );

	/**
	 * On remove image click. Remove media Id and set post meta of image to empty.s
	 *
	 * @return void.
	 */
	const removeMedia = () => {
		setMediaId( '' );
		setPostMeta( { image: ''} )
	}

	/**
	 * On select media.
	 *
	 * @param  obj media The Media Object - different from Image Ojbect (image).
	 *
	 * @return void.
	 */
 	const onSelectMedia = (media) => {

 		setMediaId( media.id.toString() );
 		setPostMeta( {image: media.id.toString() } );
	}

	return (

		<div className="editor-post-featured-image">
			<MediaUploadCheck>
				<MediaUpload
					onSelect={onSelectMedia}
					value={mediaId}
					allowedTypes={ ['image'] }
					render={({open}) => (
						<Button
							className={mediaId == '' ? 'editor-post-featured-image__toggle' : 'editor-post-featured-image__preview'}
							onClick={open}
						>
							{mediaId == '' && __('Choose a profile image', 'company-overview') }
							{image != undefined &&
			            			<ResponsiveWrapper
						    		naturalWidth={ image.media_details.width }
									naturalHeight={ image.media_details.height }
						    	>
						    		<img src={image.source_url} />
						    	</ResponsiveWrapper>
			            		}
						</Button>
					)}
				/>
			</MediaUploadCheck>
			{mediaId != '' &&
				<MediaUploadCheck>
					<MediaUpload
						title={__('Replace image', 'company-overview')}
						value={mediaId}
						onSelect={onSelectMedia}
						allowedTypes={['image']}
						render={({open}) => (
							<Button onClick={open} isDefault>{__('Replace image', 'company-overview')}</Button>
						)}
					/>
				</MediaUploadCheck>
			}
			{mediaId != '' &&
				<MediaUploadCheck>
					<Button onClick={removeMedia} isLink isDestructive>{__('Remove image', 'company-overview')}</Button>
				</MediaUploadCheck>
			}
		</div>
	)
}

export default compose( [
	withSelect( ( select ) => {
		return {
			postMeta: select( 'core/editor' ).getEditedPostAttribute( 'meta' ),
		};
	} ),
	withDispatch( ( dispatch ) => {
		return {
			setPostMeta( newMeta ) {
				dispatch( 'core/editor' ).editPost( { meta: newMeta } );
			}
		};
	} )
] )( Image );

At this point, we can see the Custom Meta Fields in the document sidebar of Person Post Type.

Additional Information

So, at this point, we successfully created a Custom Post Type Person and added the custom post meta fields. The post meta fields are being retrieved by withSelect and updated by using withDispatch.


The next step is to create a Gutenberg block and display the person’s information from the Person post type in the block.


4 ) Create a Gutenberg block “Profile”

Creating the Gutenberg block is easier than ever before. If you haven’t ever created a block, I’d recommend checking out creating a basic block in WordPress.

To create a block, we’ll have a block.json file in the plugin’s root directory.

{
    "apiVersion": 2,
    "name": "company-overview/profile",
    "title": "Company Overview",
    "category": "widgets",
    "icon": "admin-users",
    "description": "Display the overview of a person of the company.",
    "supports": {
        "html": false
    },
    "textdomain": "company-overview",
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css"
}

From the PHP file, let’s register a block.

add_action('init', [ $this, 'initiateBlock' ]);
/**
 * Registers the block using the metadata loaded from the `block.json` file.
 * Behind the scenes, it registers also all assets so they can be enqueued
 * through the block editor in the corresponding context.
 *
 * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/writing-your-first-block-type/
 */
public function initiateBlock()
{

    register_block_type(
        COMPANY_OVERVIEW_PLUGIN_PATH,
        [
            'attributes' => [
                'person' => [
                    'type' => 'string',
                ],
            ],
            'render_callback' => [ $this, 'printData' ],
        ]
    );
}

The first parameter in register_block_type COMPANY_OVERVIEW_PLUGIN_PATH reads the block settings from the block.json file in the root directory.

The attributes are what we’re saving in the database. Basically, we’ll only need to save the selected person’s ID.

We’ll also have the JS files, index.js, edit.js and save.js. These are all documented in Edit and Save handbook.


5 ) Display the Person’s information with the block “Profile” using REST API.

We’re using apiFetch to get data from the person’s post type using REST API. You might want to check getting data from the promise of WordPress apiFetch.

const [error, setError] = useState(null);
const [posts, setPosts]	= useState( null )
const [gotPosts, setGotPosts ] = useState( false );

    /**
     * Get all the Person IDs and set in setOptions.
     *
     * Note: the empty deps array [] means this useEffect will run once.
     *
     * Similar to componentDidMount()
     */
	useEffect( () => {
		apiFetch( { path: '/wp/v2/person' } ).then(
			(result) => {

				const response = result.map( function( post ) {
					return post;
				});

				setPosts( response );
				setGotPosts(true);
			},

			/**
			 * @todo Display error message in case posts couldn't be fetched with REST API.
			 */
			(error) => {
				setError(error);
			}
		)
	}, []);

Now the posts constant will give all the person’s information and so can be used when displaying the data from the block. The information can also be quickly looked at by entering the URL:

https://example.com/wp-json/wp/v2/person



Since we already got all the details in our post constant using REST API. We’ll just need to display the additional content on the modal. There’s a great tutorial on How to create a Modal Component that can be adapted to create your own modal and display the person’s information in the modal.



Thank you!

Creating a Custom Post Type with Gutenberg and REST API
Tagged on:                 

Sanjeev Aryal

Don't bury your thoughts, put your vision into reality ~ Bob Marley.

Leave a Reply

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

× WhatsApp