本文约 6,419 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:插件一停用,博客页面满屏
短代码(Shortcode)是 WordPress 最古老也最”耐用”的内容扩展机制,从 2.5 时代活到现在。它有不可替代的优势:写在文章正文里、随主题无损迁移、一句 [mybox] 就能输出复杂结构。
但它的问题同样刺眼:与插件强绑定。一旦停用提供短代码的插件,所有文章里那些方括号字符串就会原样输出到前台——[woo_slider]、[contact-form-7 id="3"]、[vc_row] 之类的裸文本铺满页面,这是 WordPress 站点最惨烈的一类”换插件事故”。另一个问题是编辑器里不可视:作者看不到短代码渲染后的样子,只能记住一堆参数名,可读性极差。
Gutenberg 出现以后,官方的建议很明确:新建的复杂组件应该做成区块(Block),而不是短代码。但现实中仍有大量场景离不开短代码——邮件模板、WooCommerce 邮件内容、第三方服务嵌入,以及那些还没有区块版本的老插件。本文把短代码的正确开发方式、安全输出要点,以及转向区块的最小代价路径讲清楚。
原理分析:短代码是怎么被解析的
| 阶段 | 处理函数 | 说明 |
|---|---|---|
| 注册 | add_shortcode( $tag, $callback ) |
存进全局 $shortcode_tags |
| 解析 | do_shortcode( $content ) |
默认挂在 the_content,优先级 11 |
| 匹配 | get_shortcode_regex() |
生成正则,支持 [tag]/[tag]x[/tag]/[tag a=1] |
| 属性 | shortcode_atts( $pairs, $atts, $tag ) |
合并默认值,过滤未知键 |
| 属性钩子 | shortcode_atts_{$tag} |
可动态覆盖默认属性 |
两条必须记住的规则:
第一,短代码回调必须 return,不能 echo。 WordPress 把返回值拼进 the_content 的字符串里。如果直接 echo,这段内容会跑到页面顶部(因为在 the_content 执行时整页 HTML 还没输出到缓冲区的最佳位置),然后出现”内容跑到标题上面”的经典故障。必须 echo 时要先 ob_start()。
第二,shortcode_atts 的第三个参数 $tag 一定要传。 传了才会触发 shortcode_atts_{$tag} 过滤器,很多插件(包括 Beaver Builder、Elementor)都靠这个钩子给短代码注入默认值。
另一个极少人注意的细节:短代码的标签名不能包含 - 之外的标点,官方建议只使用小写字母和数字,避免使用 camelCase(历史上 get_shortcode_regex() 会用 [a-zA-Z0-9_-] 匹配,大写虽能命中但不建议)。
基础概念可参考:WordPress 钩子机制与过滤器使用
保姆级实操
步骤 1:写一个生产级的短代码
下面这个例子是一个带丰富参数、安全输出、无残留能力的”提示卡片”短代码,可直接放进插件文件或 functions.php:
<?php
/**
* [mf_card type="tip" title="注意"]这里是内容[/mf_card]
*/
function mf_card_shortcode( $atts, $content = null, $tag = '' ) {
$atts = shortcode_atts( array(
'type' => 'tip',
'title' => '',
), $atts, $tag );
$allowed_types = array( 'tip', 'warn', 'error', 'note' );
$type = in_array( $atts['type'], $allowed_types, true ) ? $atts['type'] : 'tip';
// 内容里的短代码先 recursively 解析,再做安全过滤
$inner = do_shortcode( shortcode_unautop( $content ) );
$title = $atts['title'] ? sprintf( '<p class="mf-card-title">%s</p>', esc_html( $atts['title'] ) ) : '';
return sprintf(
'<div class="mf-card mf-card-%s">%s<div class="mf-card-body">%s</div></div>',
esc_attr( $type ),
$title,
wp_kses_post( $inner )
);
}
add_shortcode( 'mf_card', 'mf_card_shortcode' );
步骤 2:不用插件统一管理片段的替代做法
很多站长用 Shortcoder(由 Aakash Chakravarthy 开发的免费插件)来集中管理反复使用的 HTML 片段(比如联盟订阅箱、免责声明)。这类插件的好处是集中管理、一处修改全站生效;缺点是插件停用同样会导致输出片段消失。
替代方案是把”内容片段”做成站内的可重用区块(Reusable Block),它对应 WordPress 核心内置的 wp_block 文章类型,在 Gutenberg 里原生支持,不受任何插件影响:
# 查看站内已存在的可重用区块(wp_block 是核心 post type)
wp post list --post_type=wp_block --fields=ID,post_title,post_status
在编辑器里插入可重用区块后,它的更新是全站的,且没有任何插件依赖。
步骤 3:为短代码加”降级输出”,避免插件停用后残留在前台
方法一:写一个兜底插件(mu-plugin 最合适),把可能残留的短代码注册成”输出为空”或”输出静态 HTML”的版本:
<?php
// wp-content/mu-plugins/shortcode-fallback.php
add_action( 'init', function () {
$legacy = array( 'vc_row', 'vc_column', 'su_box', 'wpcf7_form' );
foreach ( $legacy as $tag ) {
if ( ! shortcode_exists( $tag ) ) {
add_shortcode( $tag, '__return_empty_string' );
}
}
} );
这样即使原插件被停用,方括号也不会裸奔——它们会被静默吞掉。
步骤 4:批量清理正文里的死短代码(一次性数据库处理)
-- 找出还残留某个老短代码的文章(操作前备份数据库)
SELECT ID, post_title
FROM wp_posts
WHERE post_status IN ('publish','draft')
AND post_content REGEXP '\\[(vc_row|su_box|wpcf7_form)[^\\]]*\\]';
确认范围后用 WP-CLI 批量处理比 SQL UPDATE 更安全(会触发 revision 与缓存清理):
wp post list --post_type=post --format=ids | tr ',' ' ' | while read id; do
content=$(wp post get "$id" --field=content)
new_content=$(printf '%s' "$content" | sed -E 's/\[\/?(vc_row|su_box)([^\]]*)\]//g')
if [ "$content" != "$new_content" ]; then
wp post update "$id" --post_content="$new_content" --quiet
echo "cleaned post $id"
fi
done
步骤 5:把短代码包装成 Gutenberg 区块(迁移的最小代价)
不需要用 React 从零写。最省力的是 Server-Side Rendered Block:注册一个区块,它的 save 返回 null,渲染时复用已有短代码逻辑。
// blocks/card/index.js —— 用 @wordpress/create-block 脚手架即可
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, PanelColorSettings, InspectorControls } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
registerBlockType('mf/card', {
title: __('提示卡片'),
category: 'widgets',
attributes: {
type: { type: 'string', default: 'tip' },
title: { type: 'string', default: '' },
},
edit: ({ attributes, setAttributes }) => {
const blockProps = useBlockProps({ className: `mf-card mf-card-${attributes.type}` });
return (
<div {...blockProps}>
<RichText
tagName="p" className="mf-card-title"
value={attributes.title}
onChange={(title) => setAttributes({ title })}
placeholder={__('标题…')}
/>
</div>
);
},
// save 返回 null:由 PHP 端 render_callback 渲染
save: () => null,
});
PHP 端注册:register_block_type() 时把短代码原来的输出函数挂到 render_callback,旧短代码与新区块共用同一套渲染逻辑,历史文章无需改动。
步骤 6:构建与检查
npx @wordpress/create-block mf-card
cd mf-card && npm start
常见踩坑
- 回调里 echo 导致内容跑到页顶:必须
return。必须用 echo(比如第三方库只能输出)就包一层ob_start(); ... return ob_get_clean();。 [mf_card][mf_card]嵌套不解析:嵌套短代码不会自动展开,父级回调内必须显式调用do_shortcode( $content )。- 案例:某外贸站把 Contact Form 7 换成了 Fluent Forms,页面里
[contact-form-7 id="8" title="报价"]全部裸奔输出到前台三百多篇文章。更糟的是,同一时间并行做了 Redis 全站缓存,导致这些脏内容被缓存进去,运营三天都没发现。最终靠上面那个 mu-plugin 兜底 +wp cache flush才恢复——教训:换插件之前,先在 staging 用 SQL 正则扫一遍 post_content 里该插件的短代码覆盖率。 - 属性里有
-导致取不到值:shortcode_atts拿到的 key 是原始大小写的,建议属性名统一用小写和下划线,并在回调里sanitize_key()处理。 - 短代码参数未转义导致 XSS:
[mf_notice]<?php恶意?>[/mf_notice]不会执行 PHP,但[mf_card title="<script>alert(1)</script>"]会。所有输出的属性值必须esc_attr()/esc_html(),HTML 内容用wp_kses_post()。 the_content里的<p>包裹导致多余空行:给短代码内容加shortcode_unautop(),避免 WordPress 自动<p>插入。wptexturize把引号变成弯引号:WordPress 会把正文里的直角引号转为智能引号,若短代码属性值依赖精确字符串比对会失效,比对前做归一化处理。- 被放在
<p>里导致的 HTML 层级错乱:块级短代码输出<div>会被wpautop塞进<p>内,浏览器自动闭合<p>产生一堆空段落。用shortcode_unautop()只对行内短代码处理,块级短代码应改用区块或 server-side block。
总结
短代码的选择标准:反复使用、不涉及复杂编辑交互的内容片段可以保留短代码;需要作者可视化编辑、需要和 posts 深度绑定的应该做成区块。无论哪种,一定要做到两点:①短代码回调必须 return,②写出 degradation 输出(__return_empty_string 兜底),避免换插件时方括号裸奔在前台。迁移路径上不必推倒重来——用 Server-Side Rendered Block 复用原有渲染函数,旧文章零改动,新内容走区块,是性价比最高的一条路。
相关实践:WordPress 自定义文章类型 CPT 开发
