Gutenberg Custom Post Block

Share with:

WordPress FSE is still in its infancy and its functionality for archive loops is really limited. We needed a query loop that displays different content under different circumstances, which neither the Query Loop block or the Post Template block supports. So the solution was to create a custom block.

The block creation shows the usual schema.

Layout

The project layout is the following:

.
..
block.json
index.js
init.php
[partials]
  |..template-one.php
  \.. template-default.php

Partials is a directory, where the template files are kept as PHP name, following the convention template-{name}.php

block.json:

Only the essential parts are highlighted

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
.....
	"usesContext": ["postId", "postType"],
	"parent": ["core/post-template"]
.....
	"editorScript": "file:./index.js",

}

index.js:

I’ve actually copied the way the vanilla Gutenberg block extract the title, featured image and categories from their defualt post-template blocks.

import { registerBlockType } from "@wordpress/blocks";

import { store, useEntityProp } from "@wordpress/core-data";
import { useSelect } from "@wordpress/data";

import metadata from "./block.json";
const { name, attributes, title, parent, icon } = metadata;

import './scss/style.scss';

registerBlockType(
	name,
	{
		title,
		icon,
		parent,
		attributes,
		usesContext: ["postId", "postType"],

		edit (props) {
			const {postId, postType} = props.context;

			const [title] = useEntityProp('postType', postType, 'title', postId);
			const [featuredImage] = useEntityProp('postType', postType, 'featured_media', postId); 
			let [catId] = useEntityProp('postType', postType, 'categories', postId);

			catId = catId ? catId[0] : null;

			let featuredImg = '';
			let category = '';

			if (catId) {
				category = useSelect((select) => {
					const categories = select('core').getEntityRecords('taxonomy', 'category', { term_id : catId });

					if (!categories) {
						return '';
					}

					for (const [_, catObj] of Object.entries(categories)) {
						if (catObj.id === catId) {
							return catObj.name;
						}
					}
				});
			}

			const media = useSelect(
				(select) =>
					featuredImage &&
					select( store ).getMedia( featuredImage, { context: 'view' } ),
				[featuredImage]
			);

			if (media) {
				featuredImg = (
					<img src={media.media_details.sizes.medium.source_url} />
				);
			}

			return (
				<article className="custom-post is-admin">
					<div className="custom-post__link">
						<div className="custom-post__meta">
						<span className="custom-post__meta-field category">
							{category}
						</span>
						</div>
						<h3 className="custom-post__title">
							{title}
						</h3>
						<figure className="custom-post__featured">
							{featuredImg}
						</figure>
					</div>
				</article>
			);
		},

		save() {
			return null;
		}
	}
);

index.php


And since this is a dynamic block – we need a callback function, which in our case is custom_post. For this to work – we need a custom block initialization, in the project I implemented this we had this in the theme. You will need to figure how to load a block on your own (GIYF) 🙂

<?php

function custom_post($attributes, $content, $block)
{
	if (empty($block) || empty($block->context) || empty($block->context['postId'])) {
		return '';
	}

	$post_id = $block->context['postId'];

	if (empty($post_id)) {
		return "";
	}

	$CustomPost = new CustomPost($post_id);
	return $CustomPost->process_card();
}

class CustomPost
{
	private int $post_id;
	private string $template;

	public function __construct($post_id)
	{
		$this->post_id = $post_id;
		$this->template = "minimal";
	}

	public function process_card() : string
	{
		//this is the main place where you execute the logic which determins which template to use
		switch($something) {
			case "one":
				$this->template = "one";
				break;
                        .........
			default:
				$this->tempalte = "default";
		}

		return $this->get_template();
	}

	private function get_template() : string
	{
		$path = __DIR__ . "/partials/template-{$this->template}.php";

		ob_start();
		if (file_exists($path)) {
			require $path;
		} else {
			require __DIR__ . "/partials/template-minimal.php";
		}

		$result = ob_get_clean();

		return empty($result)
			? ''
			: $result;
	}
}

Templates

The templates looks something like this

<?php
$figure_class = "custom-post__featured";

$featured_image = get_the_post_thumbnail_url($post_id, 'medium');
$title = get_the_title($post_id);
$url = get_permalink($post_id);
?>

<article class="custom-post blog">
	<a href="<?php echo $url; ?>" class="custom-post__link">
		<h5 class="custom-post__title">
			<?php echo $title; ?>
		</h5>
		<figure class="<?php echo $figure_class; ?>">
			<?php if (!empty($featured_image)) : ?>
				<img
					src="<?php echo $featured_image; ?>"
					alt="<?php echo $title; ?>"
				>
			<?php endif; ?>
		</figure>
	</a>
</article>

Keep in mind that every parameter you pass to this template needs to be defined in the get_template method in the above class.