搜索全站
插件 17 分钟阅读

WordPress 短代码(Shortcode)插件开发与最佳实践:从 add_shortcode 到区块化

短代码还在用,但已经不是最佳实践了。本文讲清 add_shortcode 注册、属性解析与安全输出、短代码残留、短代码比较全面的迁移路径,给出包装现有短代码为 Gutenberg 区块的最小实现。

U
UFO_PF 2025-08-26 · 阅读 17 分钟
Wo 插件
阅读提示
本文约 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

常见踩坑

  1. 回调里 echo 导致内容跑到页顶:必须 return。必须用 echo(比如第三方库只能输出)就包一层 ob_start(); ... return ob_get_clean();。
  2. [mf_card][mf_card] 嵌套不解析:嵌套短代码不会自动展开,父级回调内必须显式调用 do_shortcode( $content )。
  3. 案例:某外贸站把 Contact Form 7 换成了 Fluent Forms,页面里 [contact-form-7 id="8" title="报价"] 全部裸奔输出到前台三百多篇文章。更糟的是,同一时间并行做了 Redis 全站缓存,导致这些脏内容被缓存进去,运营三天都没发现。最终靠上面那个 mu-plugin 兜底 + wp cache flush 才恢复——教训:换插件之前,先在 staging 用 SQL 正则扫一遍 post_content 里该插件的短代码覆盖率。
  4. 属性里有 - 导致取不到值:shortcode_atts 拿到的 key 是原始大小写的,建议属性名统一用小写和下划线,并在回调里 sanitize_key() 处理。
  5. 短代码参数未转义导致 XSS:[mf_notice]<?php恶意?>[/mf_notice] 不会执行 PHP,但 [mf_card title="<script>alert(1)</script>"] 会。所有输出的属性值必须 esc_attr()/esc_html(),HTML 内容用 wp_kses_post()。
  6. the_content 里的 <p> 包裹导致多余空行:给短代码内容加 shortcode_unautop(),避免 WordPress 自动 <p> 插入。
  7. wptexturize 把引号变成弯引号:WordPress 会把正文里的直角引号转为智能引号,若短代码属性值依赖精确字符串比对会失效,比对前做归一化处理。
  8. 被放在 <p> 里导致的 HTML 层级错乱:块级短代码输出 <div> 会被 wpautop 塞进 <p> 内,浏览器自动闭合 <p> 产生一堆空段落。用 shortcode_unautop() 只对行内短代码处理,块级短代码应改用区块或 server-side block。

总结

短代码的选择标准:反复使用、不涉及复杂编辑交互的内容片段可以保留短代码;需要作者可视化编辑、需要和 posts 深度绑定的应该做成区块。无论哪种,一定要做到两点:①短代码回调必须 return,②写出 degradation 输出(__return_empty_string 兜底),避免换插件时方括号裸奔在前台。迁移路径上不必推倒重来——用 Server-Side Rendered Block 复用原有渲染函数,旧文章零改动,新内容走区块,是性价比最高的一条路。

相关实践:WordPress 自定义文章类型 CPT 开发

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。