mcpField
W
wordpress-block-editor-fse

wordpress-block-editor-fse

github.com/bobmatnyc设计与 UI78466 次安装15 周前更新
代码仓库

关于 wordpress-block-editor-fse

面向 WordPress 主题与区块开发者的 FSE 技能指南,涵盖区块主题架构、站点编辑器和自定义区块开发,并提供迁移与无障碍实践建议。

主要功能

  1. 01区块主题文件与架构
  2. 02theme.json 设计系统配置
  3. 03站点编辑器模板定制
  4. 04自定义区块开发
  5. 05经典主题迁移指导
  6. 06性能与无障碍实践

适用场景

  1. 01构建新的区块主题
  2. 02通过站点编辑器配置模板
  3. 03开发服务器端或 React 区块
  4. 04将经典主题迁移到 FSE

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill wordpress-block-editor-fse

安装后在 Claude Code 中输入/wordpress-block-editor-fse确认已加载。

附带文件metadata.json

详细介绍

WordPress 区块编辑器与全站编辑

概述

全站编辑(FSE)自 WP 6.2 起已可用于生产环境,并将一切都视为区块——页眉、页脚、模板,而不仅仅是内容。区块主题使用 HTML 模板 + theme.json,而非 PHP 文件 + style.css。

主要组件:

  • theme.json:集中管理颜色、排版、间距和布局
  • HTML 模板:基于区块的文件(index.html、single.html)
  • 模板部件:可复用组件(header.html、footer.html)
  • 区块模式:预先设计的区块布局
  • 站点编辑器:可视化自定义模板

适用场景: ✅ 新主题、一致的设计系统、供非技术用户自定义 ❌ 复杂的服务器逻辑、团队不熟悉区块、依赖大量 PHP

全站编辑架构

区块主题与经典主题
区块主题经典主题
包含区块的 HTML 文件包含模板标签的 PHP 文件
theme.json + CSSfunctions.php + style.css
站点编辑器(可视化)自定义器(设置)
用户可编辑模板自定义功能有限
站点编辑器功能
  • 模板编辑(页面、文章、归档)
  • 模板部件(页眉/页脚变体)
  • 全局样式(全站颜色、排版)
  • 模式库(保存/复用区块组合)
  • 导航菜单(基于区块)
  • 样式变体(备用设计预设)

theme.json 配置

theme.json v3(WP 6.7)提供集中式设计控制。WordPress 会自动生成 CSS 自定义属性。

生产环境示例
{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "useRootPaddingAwareAlignments": true,
    "layout": {
      "contentSize": "800px",
      "wideSize": "1200px"
    },
    "color": {
      "palette": [
        { "slug": "primary", "color": "#0073aa", "name": "Primary" },
        { "slug": "secondary", "color": "#005177", "name": "Secondary" },
        { "slug": "base", "color": "#ffffff", "name": "Base" },
        { "slug": "contrast", "color": "#000000", "name": "Contrast" }
      ],
      "defaultPalette": false,
      "defaultGradients": false
    },
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
          "slug": "system",
          "name": "System Font"
        }
      ],
      "fontSizes": [
        { "slug": "small", "size": "0.875rem", "name": "Small" },
        { "slug": "medium", "size": "1rem", "name": "Medium" },
        {
          "slug": "large",
          "size": "1.5rem",
          "name": "Large",
          "fluid": { "min": "1.25rem", "max": "1.5rem" }
        }
      ],
      "fontWeight": true,
      "lineHeight": true
    },
    "spacing": {
      "units": ["px", "em", "rem", "vh", "vw", "%"],
      "padding": true,
      "margin": true,
      "spacingSizes": [
        { "slug": "30", "size": "0.5rem", "name": "XS" },
        { "slug": "40", "size": "1rem", "name": "S" },
        { "slug": "50", "size": "1.5rem", "name": "M" },
        { "slug": "60", "size": "2rem", "name": "L" }
      ]
    },
    "border": { "radius": true, "color": true, "width": true }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--base)",
      "text": "var(--wp--preset--color--contrast)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--system)",
      "fontSize": "var(--wp--preset--font-size--medium)",
      "lineHeight": "1.6"
    },
    "elements": {
      "link": {
        "color": { "text": "var(--wp--preset--color--primary)" },
        ":hover": {
          "color": { "text": "var(--wp--preset--color--secondary)" }
        }
      },
      "h1": {
        "typography": {
          "fontSize": "var(--wp--preset--font-size--large)",
          "fontWeight": "700"
        }
      },
      "button": {
        "color": {
          "background": "var(--wp--preset--color--primary)",
          "text": "var(--wp--preset--color--base)"
        },
        "border": { "radius": "4px" },
        ":hover": {
          "color": { "background": "var(--wp--preset--color--secondary)" }
        }
      }
    },
    "blocks": {
      "core/quote": {
        "border": {
          "width": "0 0 0 4px",
          "color": "var(--wp--preset--color--primary)"
        },
        "spacing": { "padding": { "left": "var(--wp--preset--spacing--60)" } }
      }
    }
  },
  "customTemplates": [
    {
      "name": "page-wide",
      "title": "Full Width Page",
      "postTypes": ["page"]
    }
  ]
}
自动生成的 CSS 自定义属性
  • 颜色:var(--wp--preset--color--primary)
  • 字体:var(--wp--preset--font-family--system)
  • 字号:var(--wp--preset--font-size--large)
  • 间距:var(--wp--preset--spacing--50)
流体排版

带有 fluid: { min, max } 的字号会使用 clamp() 自动缩放:

{
  "slug": "large",
  "size": "1.5rem",
  "fluid": { "min": "1.25rem", "max": "1.5rem" }
}

区块主题架构

必需文件
my-block-theme/
├── style.css                 # Theme metadata (REQUIRED)
├── theme.json                # Settings/styles (REQUIRED)
├── templates/
│   ├── index.html           # Fallback (REQUIRED)
│   ├── single.html
│   ├── page.html
│   └── archive.html
├── parts/
│   ├── header.html
│   └── footer.html
├── patterns/                 # Block patterns
│   └── hero.php
└── functions.php             # Optional setup
style.css 元数据
/*
Theme Name: My Block Theme
Requires at least: 6.4
Requires PHP: 8.1
Version: 1.0.0
*/
HTML 模板结构

templates/single.html:




<div class="wp-block-group">
  
  
  
  
</div>



templates/index.html(含查询循环):




<main class="wp-block-group">
  
  <div class="wp-block-query">
    
      
      
      
    

    
      
      
      
    
  </div>
  
</main>



模板部件

parts/header.html:


<div class="wp-block-group">
  
  
</div>

区块模式

patterns/hero.php:

<?php
/**
 * Title: Hero Section
 * Slug: my-theme/hero
 * Categories: featured
 */
?>

<div class="wp-block-cover alignfull">
  <div class="wp-block-cover__inner-container">
    
    <h1>Welcome to Our Site</h1>
    

    
    <div class="wp-block-buttons">
      
      <div class="wp-block-button"><a class="wp-block-button__link">Get Started</a></div>
      
    </div>
    
  </div>
</div>

注册模式类别:

add_action('init', 'register_pattern_categories');
function register_pattern_categories() {
  register_block_pattern_category('hero', [
    'label' => __('Hero Sections', 'my-theme')
  ]);
  register_block_pattern_category('cta', [
    'label' => __('Call to Action', 'my-theme')
  ]);
}

自定义区块开发

block.json 元数据(区块 API v3)

blocks/testimonial/block.json:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-theme/testimonial",
  "title": "Testimonial",
  "category": "widgets",
  "icon": "format-quote",
  "attributes": {
    "content": {
      "type": "string",
      "source": "html",
      "selector": ".testimonial-content"
    },
    "author": { "type": "string", "default": "" },
    "role": { "type": "string", "default": "" },
    "rating": { "type": "number", "default": 5 }
  },
  "supports": {
    "html": false,
    "align": ["wide", "full"],
    "color": { "background": true, "text": true },
    "spacing": { "padding": true, "margin": true }
  },
  "render": "file:./render.php"
}
属性来源

从 HTML 提取数据的不同方式:

"attributes": {
  "title": {
    "type": "string",
    "source": "html",
    "selector": "h2"
  },
  "linkUrl": {
    "type": "string",
    "source": "attribute",
    "selector": "a",
    "attribute": "href"
  },
  "isActive": {
    "type": "boolean",
    "default": false
  },
  "items": {
    "type": "array",
    "source": "query",
    "selector": ".item",
    "query": {
      "text": { "type": "string", "source": "text" }
    }
  }
}
服务端渲染(render.php)

blocks/testimonial/render.php:

<?php
$content = $attributes['content'] ?? '';
$author = $attributes['author'] ?? '';
$role = $attributes['role'] ?? '';
$rating = absint($attributes['rating'] ?? 5);

$wrapper_attributes = get_block_wrapper_attributes([
  'class' => 'testimonial-block',
]);
?>

<div <?php echo $wrapper_attributes; ?>>
  <blockquote class="testimonial-content">
    <?php echo wp_kses_post($content); ?>
  </blockquote>

  <?php if ($rating > 0) : ?>
    <div class="testimonial-rating">
      <?php for ($i = 1; $i <= 5; $i++) : ?>
        <span class="star <?php echo $i <= $rating ? 'filled' : 'empty'; ?>">
          <?php echo $i <= $rating ? '★' : '☆'; ?>
        </span>
      <?php endfor; ?>
    </div>
  <?php endif; ?>

  <?php if ($author || $role) : ?>
    <cite class="testimonial-author">
      <span class="author-name"><?php echo esc_html($author); ?></span>
      <?php if ($role) : ?>
        <span class="author-role"><?php echo esc_html($role); ?></span>
      <?php endif; ?>
    </cite>
  <?php endif; ?>
</div>
客户端渲染(React)

blocks/testimonial/index.js:

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

registerBlockType('my-theme/testimonial', {
  edit: ({ attributes, setAttributes }) => {
    const { content, author, role, rating } = attributes;
    const blockProps = useBlockProps();

    return (
      <>
        <InspectorControls>
          <PanelBody title={__('Settings', 'my-theme')}>
            <TextControl
              label={__('Author', 'my-theme')}
              value={author}
              onChange={(v) => setAttributes({ author: v })}
            />
            <TextControl
              label={__('Role', 'my-theme')}
              value={role}
              onChange={(v) => setAttributes({ role: v })}
            />
            <RangeControl
              label={__('Rating', 'my-theme')}
              value={rating}
              onChange={(v) => setAttributes({ rating: v })}
              min={1}
              max={5}
            />
          </PanelBody>
        </InspectorControls>

        <div {...blockProps}>
          <RichText
            tagName="blockquote"
            value={content}
            onChange={(v) => setAttributes({ content: v })}
            placeholder={__('Testimonial text...', 'my-theme')}
          />

          <div className="testimonial-rating">
            {[1, 2, 3, 4, 5].map((star) => (
              <span
                key={star}
                onClick={() => setAttributes({ rating: star })}
              >
                {star <= rating ? '★' : '☆'}
              </span>
            ))}
          </div>

          <cite>
            <RichText
              tagName="span"
              value={author}
              onChange={(v) => setAttributes({ author: v })}
              placeholder={__('Author', 'my-theme')}
            />
          </cite>
        </div>
      </>
    );
  },
  save: () => null, // Server-side rendering
});
区块注册

functions.php:

add_action('init', 'register_custom_blocks');
function register_custom_blocks() {
  register_block_type(__DIR__ . '/blocks/testimonial');
}
InspectorControls(设置侧边栏)

区块设置的常用控件:

import {
  InspectorControls,
  PanelColorSettings,
  MediaUpload
} from '@wordpress/block-editor';
import {
  PanelBody,
  SelectControl,
  ToggleControl,
  RangeControl,
  Button
} from '@wordpress/components';

<InspectorControls>
  <PanelBody title="Layout">
    <SelectControl
      label="Columns"
      value={columns}
      options={[
        { label: '2', value: 2 },
        { label: '3', value: 3 },
        { label: '4', value: 4 }
      ]}
      onChange={(v) => setAttributes({ columns: parseInt(v) })}
    />

    <ToggleControl
      label="Enable Shadow"
      checked={enableShadow}
      onChange={(v) => setAttributes({ enableShadow: v })}
    />

    <RangeControl
      label="Border Radius"
      value={borderRadius}
      onChange={(v) => setAttributes({ borderRadius: v })}
      min={0}
      max={50}
    />
  </PanelBody>

  <PanelBody title="Media">
    <MediaUpload
      onSelect={(media) => setAttributes({ imageUrl: media.url })}
      allowedTypes={['image']}
      render={({ open }) => (
        <Button onClick={open} variant="secondary">
          {imageUrl ? 'Change Image' : 'Select Image'}
        </Button>
      )}
    />
  </PanelBody>

  <PanelColorSettings
    title="Colors"
    colorSettings={[
      {
        value: bgColor,
        onChange: (v) => setAttributes({ bgColor: v }),
        label: 'Background'
      }
    ]}
  />
</InspectorControls>
区块支持

启用 WordPress 功能:

"supports": {
  "html": false,
  "anchor": true,
  "align": ["wide", "full"],
  "color": {
    "background": true,
    "text": true,
    "gradients": true
  },
  "spacing": {
    "padding": true,
    "margin": true,
    "blockGap": true
  },
  "typography": {
    "fontSize": true,
    "lineHeight": true,
    "fontWeight": true
  }
}

使用区块编辑器的自定义文章类型

add_action('init', 'register_book_cpt');
function register_book_cpt() {
  register_post_type('book', [
    'labels' => [
      'name' => __('Books', 'my-theme'),
      'singular_name' => __('Book', 'my-theme'),
    ],
    'public' => true,
    'has_archive' => true,
    'supports' => ['title', 'editor', 'thumbnail'],
    'show_in_rest' => true,  // REQUIRED for block editor
    'menu_icon' => 'dashicons-book',
    'template' => [          // Default blocks
      ['core/paragraph', ['placeholder' => 'Book description...']],
      ['core/image'],
      ['my-theme/book-details'],
    ],
    'template_lock' => 'insert', // Can't add/remove blocks
  ]);

  // Register taxonomy
  register_taxonomy('genre', 'book', [
    'labels' => ['name' => __('Genres', 'my-theme')],
    'hierarchical' => true,
    'show_in_rest' => true,  // REQUIRED
  ]);
}
模板锁定
  • false:无限制
  • 'all':无法修改结构
  • 'insert':无法添加/移除,但可以重新排序
  • 'contentOnly':仅可编辑内容
在 theme.json 中注册
"customTemplates": [
  {
    "name": "single-book",
    "title": "Book Template",
    "postTypes": ["book"]
  }
]

开发工作流

@wordpress/scripts

package.json:

{
  "scripts": {
    "start": "wp-scripts start",
    "build": "wp-scripts build"
  },
  "devDependencies": {
    "@wordpress/scripts": "^27.0.0"
  }
}

命令:

npm install
npm run start  # Development with hot reload
npm run build  # Production build (minified)
wp-env 设置

.wp-env.json:

{
  "core": "WordPress/WordPress#6.7",
  "phpVersion": "8.3",
  "themes": ["./my-block-theme"],
  "config": {
    "WP_DEBUG": true,
    "SCRIPT_DEBUG": true
  }
}

用法:

npx @wordpress/env start
# Access: http://localhost:8888
# Admin: admin / password

npx @wordpress/env stop
npx @wordpress/env clean  # Reset database

从经典主题迁移

模板标签到区块的映射
经典主题对应区块
the_title()``
the_content()``
the_post_thumbnail()``
the_date()``
wp_nav_menu()``
get_header()``
get_footer()``
get_sidebar()``
迁移步骤
  1. 提取设计令牌,从 style.css 提取到 theme.json
  2. 转换 PHP 模板为 HTML 区块模板
  3. 在 functions.php 中添加区块支持:
add_theme_support('wp-block-styles');
add_theme_support('align-wide');
add_theme_support('responsive-embeds');
  1. 使用真实内容全面测试
区块验证

WordPress 会根据已注册的区块定义验证区块标记。无效区块会在编辑器中显示错误:

常见验证错误:

  • 属性类型不匹配(字符串与数字)
  • 缺少必需属性
  • HTML 结构不正确
  • 属性名称已更改

修复验证错误:

// Add deprecated versions for backward compatibility
const deprecated = [
  {
    attributes: {
      oldName: { type: 'string' }
    },
    migrate: (attributes) => ({
      newName: attributes.oldName
    }),
    save: (props) => {
      // Old save function
    }
  }
];

性能与最佳实践

性能

✅ 尽可能使用服务器端渲染(render.php) ✅ 利用区块支持(减少自定义 CSS) ✅ 禁用未使用的功能:"defaultPalette": false ✅ 使用 CSS 自定义属性以保持一致性 ❌ 避免对静态内容进行客户端渲染 ❌ 不要使用 !important 覆盖核心区块

无障碍

✅ 语义化 HTML(<header>、<main>、<footer>) ✅ 为自定义区块提供键盘导航 ✅ WCAG AA 颜色对比度(最低 4.5:1) ✅ 为所有图像提供替代文本 ❌ 不要假设 FSE 就意味着无障碍(必须进行测试)

反模式

❌ 混用经典方式和区块方式 ❌ 将颜色硬编码(使用 CSS 变量) ❌ 重复实现区块支持 ❌ 跳过无障碍测试 ❌ 在 HTML 模板中使用 get_header()

相关技能

  • wordpress-plugin-fundamentals:钩子系统、CPT
  • react:区块编辑器组件
  • typescript:类型安全的区块开发
  • php-security:清理区块属性

重要提醒

  1. 区块主题必须使用 theme.json
  2. FSE 中的 HTML 模板取代 PHP
  3. 服务器端渲染通常优于客户端渲染
  4. 区块支持可减少自定义代码
  5. 无障碍需要通过测试来确保

危险信号

  • CSS 文件超过 5 个 → 使用 theme.json
  • HTML 模板中包含 PHP 标签 → 使用区块
  • 对静态内容进行客户端渲染 → 使用 render.php
  • 未进行键盘测试 → 存在无障碍问题
  • 值被硬编码 → 使用 CSS 自定义属性

WordPress: 6.7+ | PHP: 8.1+ | 工具: @wordpress/scripts、wp-env

评论

支持 Markdown