创建把内容存入文章元数据的 WordPress 自定义区块

文章元数据(post meta)是 WordPress 开发中常用的数据存储方式。在 WordPress 5.0 引入区块编辑器之前,开发者通常在文章编辑页加入元数据框,让用户输入与某一篇文章关联的数据,再在主题或插件模板中读取。

自定义区块和区块样板提供了另一种选择:把数据作为区块属性,随文章正文保存。究竟选择元数据还是文章正文,取决于具体需求;通常可以先看数据是否需要作为查询条件,或者是否会显示在所属文章类型的单篇模板之外。本教程通过一个具体例子建立基本做法。

需求:产品评价与作者信息

假设客户的网站已经有一个名为 Product 的自定义文章类型。他们希望给产品填写评价,并在首页显示最近五个有评价的产品。评价还要记录作者姓名和网站 URL,但这两项仅在产品的单篇模板中显示。

确定数据的存放位置

需要保存三项数据:评价正文、作者姓名、作者网站 URL。由于首页需要查询有评价的产品,评价正文放入文章元数据;作者姓名和 URL 不参与查询,只在单篇页面出现,可以作为区块属性随文章正文保存。

Quote 或 Pullquote 区块很适合引用文字,也可以通过 区块过滤器扩展字段。不过,它们把引用内容保存在文章正文中,不能直接满足本例把评价保存到 post meta 的需求。此外,前端需要用 get_post_meta() 从 WordPress 读取评价,因此这里采用自定义动态区块。

注册文章类型和元数据

这个示例假设现有 Product 文章类型已按下面的片段注册:

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',
		),
	)
);

要在文章编辑器中访问元数据,必须通过 REST API 暴露它。使用 register_post_meta 注册 product 类型的 testimonial,把 show_in_rest 设为 true。sanitize_callback 使用 wp_kses_post,在保存到数据库前按允许的文章 HTML 规则清理内容:

register_post_meta(
	'product',
	'testimonial',
	array(
		'show_in_rest'       => true,
		'single'             => true,
		'type'               => 'string',
		'sanitize_callback'  => 'wp_kses_post',
	)
);

这里同时注册了字符串类型和单值字段;文章类型的 supports 中包含 custom-fields。

用脚手架创建动态区块

@wordpress/create-block 可以生成注册区块的插件。在主题内注册区块也是可行的,有些项目更适合这样做;本教程把代码放入插件,以减少额外设置。

以下是原教程的脚手架命令。在自己的开发环境的 wp-content/plugins 目录中执行它,会生成一个包含动态区块的插件:

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."

--variant dynamic 指定动态区块:前端由 PHP 渲染,不把完整的前端展示标记直接存入文章正文。其他参数设定命名空间、区块分类和简短描述。详细选项见 create-block 官方文档。

若向现有插件添加区块,也可以用 --no-plugin 仅生成区块文件;这种情况下应在插件的 src 目录中运行命令。

上面的名称是 post-meta-testimonial,因此应在生成的实际插件目录中工作,再在开发站点激活该插件。原文后续写成 post-meta-tutorial,与命令中的名称不一致,这里按命令名说明。

进入 wp-content/plugins/post-meta-testimonial 后运行 npm run start 启动构建监听。这个命令由 @wordpress/scripts 提供,监听 src 文件的修改并重新编译。此时可以在编辑器中插入刚生成的区块。

官方原图:脚手架区块插入编辑器
初始动态区块的编辑器示意图。

这是一篇 2023 年教程的实现路径。下面保留其 apiVersion: 2 等示例设置;脚手架输出可能随版本变化,本次没有执行安装、构建或站点操作。

修改 block.json

脚手架准备好后,先定义两个字符串属性:authorName 和 authorURL。它们不设默认值,分别保存作者姓名和网站 URL:

"attributes": {
	"authorName": {
		"type": "string"
	},
	"authorURL": {
		"type": "string"
	}
},

区块属性的详细定义见 官方文档。曾经可以通过 block.json 把属性直接连接到文章元数据,但这种方式已被弃用,不应在这个实现中继续使用。

区块还需要知道当前文章 ID 和类型。usesContext 允许区块接收祖先区块或编辑器提供的上下文;加入 postId 和 postType:

"usesContext": ["postId", "postType"]

相关概念见 区块上下文文档。接着把图标改成更贴合评价用途的引用图标:

"icon": "format-quote"

同一篇文章共享一个 testimonial 字段,所以不应允许插入多个这样的编辑区块。在 supports 中设置 multiple: false:

"supports": {
	"html": false,
	"multiple": false
},

可以用 example 定义区块插入器中的预览数据:

"example": {
	"attributes": {
		"authorName": "WordPress",
		"authorURL": "developer.wordpress.org/news"
	}
},

其他属性可以按实际需求调整。完成后,block.json 的内容类似下面这样:

{
	"$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"
}

在 edit.js 中构建编辑界面

接下来修改编辑器中的区块行为。打开脚手架生成的 edit.js。原始文件通常有较多注释,下面保留教程简化后的版本:

/**
 * 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>
	);
}

调整标记和样式

把 JSX 改成一个 blockquote,内部包含评价段落 p 和署名 cite。署名中用两个 span 分别显示作者姓名和网站,网站文字包在 a 中。前端也将使用相同结构:

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>
	);
}

在 style.scss 中加入下面的样式,让区块呈现引用文字的效果:

.wp-block-tutorial-post-meta-testimonial {
	border-left: 0.25em solid;
	margin: 0 0 1.75em;
	padding-left: 1em;
	border-width: 1px;
}

这段样式最后的 border-width: 1px 会覆盖前面声明的左边框宽度;代码按原文保留。

官方原图:加入标记和 CSS 后的区块
引用区块标记与样式的示意图。

加入组件和交互

Edit 接收一个 props 对象。通过解构取得 attributes、setAttributes,以及 context 中的 postType 和 postId:

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>
	);
}

现在需要让用户输入数据。TextControl 等输入组件也可以使用;这里选用 RichText,提供所见即所得的编辑体验和基本格式控制。从 @wordpress/block-editor 导入它:

import { useBlockProps, RichText } from '@wordpress/block-editor';

本例使用 RichText 的六种属性:

  • tagName:可编辑元素的标签名。
  • value:字段当前内容。
  • onChange:内容改变时调用的函数。
  • placeholder:字段为空时显示的提示文字。
  • allowedFormats:允许的格式,例如粗体和斜体。
  • disableLineBreaks:是否禁止换行。

需要三个 RichText:一个编辑评价正文,另外两个编辑作者属性。先更新 cite 中的内容:

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>
	);
}

姓名和 URL 的 onChange 用 setAttributes 更新对应属性。这里的 allowedFormats={ [] } 不允许格式,disableLineBreaks 禁止换行。更多行为可查看 RichText 源码。

读取和更新文章元数据

要给评价正文的 RichText 提供数据,先从 @wordpress/core-data 导入 useEntityProp:

import { useEntityProp } from '@wordpress/core-data';

这个 hook 接收四个参数:实体种类 kind、实体名称 name、属性名 prop,以及可显式指定实体 ID 的 id。

本例使用 'postType'、'product'、'meta' 和上下文提供的 postId。显式传入文章 ID,能让它读取区块所指向文章的元数据;区块可能位于文章正文、站点编辑器,或 Query Loop 的模板中。

useEntityProp 返回数组:第一项是读取到的数据,第二项是更新数据的函数。与 useState 的形式相似,可以解构为 meta 和 updateMeta。meta 包含该文章类型已注册的元数据,再从中取出 testimonial。

这里的字段只注册给 Product,并且代码把实体名称写死为 'product'。把区块用于其他文章类型时会出现问题;这些示例不包含跨类型使用的防护逻辑。

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>
	);
}

取得字段后,把评价正文的 RichText 的 value 设为 testimonial,在 onChange 中调用 updateMeta。该函数接收元数据对象;为了不覆盖其他字段,先展开 ...meta,再设置新的 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>
	);
}

区块现在可以显示和编辑评价正文。修改时不会立刻写入数据库;只有保存文章后,元数据更改才会真正保存。

在 render.php 中更新前端

动态区块用 render.php 生成前端标记。应把这个文件当作前端模板,类似 get_template_part 使用的模板;它主要包含要显示的标记,不适合在其中定义大量 PHP 函数或类。脚手架的文件类似这样:

<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php esc_html_e( 'Post Meta Testimonial – hello from a dynamic block!', 'post-meta-testimonial' ); ?>
</p>

先把标记调整成与 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>

render.php 可以使用三个变量:$attributes 是区块属性数组,$content 是内部内容,$block 是区块实例。本例从属性中取姓名与 URL:

$author_name = $attributes['authorName'];
$author_url  = $attributes['authorURL'];

评价正文使用 get_post_meta() 读取。从 $block->context 取得 usesContext 提供的文章 ID,并传入元数据键和 true,表示读取单个值:

$testimonial = get_post_meta( $block->context['postId'], 'testimonial', true );
$author_name = $attributes['authorName'];

下面是原教程的最终标记示例。需要注意,它这里改用了全局 $post->ID,与上一步的区块上下文方式并不一致。若区块嵌入 Query Loop 等位置,应根据实际上下文确认文章 ID,不能把这两种取得 ID 的方式视为始终等价。

<?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>

最终示例用 esc_html 转义评价和姓名,用 esc_url 转义链接地址,并为新窗口链接设置 noopener noreferrer。esc_html($testimonial) 会把评价中的 HTML 标签作为文字转义显示;尽管保存时使用 wp_kses_post 允许部分 HTML,前端并不会按原来的富文本格式渲染。这一点在采用 RichText 格式时需要明确。

文章元数据区块还有许多使用层次和场景。这个例子给出从注册字段、编辑实体数据,到动态渲染的基本路径;首页查询五个产品的具体查询代码不在原教程中。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容