# 01 · 技术栈与项目结构

> 适用：新建主题、判断「新功能该放在哪个文件」、核对命名是否合规。
> 相关分册：样式与构建见 `02-样式与构建.md`；模板骨架见 `03-布局与模板骨架.md`。

---

## 一、环境要求

| 项目 | 要求 | 说明 |
| --- | --- | --- |
| WordPress | ≥ 6.5（实测 7.1） | 使用了 REST API、`WP_Term`、`wp_get_attachment_image_attributes` 等 |
| PHP | ≥ 7.4（实测 8.2） | 主题**不使用** PHP 8 专有语法，保持 7.4 兼容 |
| PHP 扩展 | **mbstring（必需）**、curl（推荐）、gd（推荐） | `mb_strlen()` / `mb_substr()` 用于参数校验与首字头像；`flock` 为 PHP 内置函数，无需扩展；gd 供 WordPress 生成缩略图 |
| 数据库 | MySQL 5.5.5+ / MariaDB | 与 WordPress 自身要求一致 |
| Web 服务器 | nginx 或 Apache | nginx 需注意长轮询超时、REST 路径放行、404 拦截，见 `07-性能安全与部署.md` |
| Node / npm | **仅构建期需要**：node 20 / npm 10 | 前台运行、部署到生产都**不需要** Node；产物 `assets/css/tailwind.css` 随主题分发 |

> `npm install` 生成的 `node_modules/` 只服务构建，**绝不随主题打包**（打包排除项见 `07-性能安全与部署.md`）。

---

## 二、目录结构与文件职责

```
your-theme/
├── style.css                    主题头（真实样式在 assets/css/theme.css）
├── functions.php                主题入口：常量、加载 inc、资源入队、关闭 WindPress 前台运行时
├── front-page.php               首页（工具型首页：工作台 / 落地页）
├── header.php / footer.php      页头（品牌图标 + 导航）与页脚（品牌 + 友情链接 + 版权）
├── home.php / archive.php / search.php / index.php
│                                文章列表 / 归档 / 搜索 / 兜底列表（共用 template-parts/post-list.php）
├── single.php                   文章详情（面包屑 + 正文 + 标签 + 上下篇 + 侧栏 + 评论）
├── page.php / 404.php           单页 / 404 模板
├── searchform.php               搜索表单（daisyUI join 组件）
├── comments.php                 评论区模板（列表 + 分页 + 空态 + 表单）
├── template-parts/
│   ├── post-list.php            文章列表片段（网格卡片 + 分页 + 空态）
│   └── related-posts.php        相关文章（侧栏模块示例：同分类优先，不足用最新补齐）
├── inc/
│   ├── config.php               集中配置层（所有常量，支持 wp-config.php 覆盖）
│   ├── class-mailcx-client.php  上游 API 客户端（签名 + 接口封装）
│   ├── friend-links.php         友情链接数据层（服务端取数 + 小时级缓存 + 失败兜底）
│   ├── rest-api.php             REST 转发接口（路由 + 参数校验 + IP 限流 + 白名单过滤）
│   ├── seo.php                  主题侧 SEO 补齐（keywords、SEO 关键词字段、图片 alt、canonical）
│   └── comments.php             评论区渲染与表单配置（回调、占位头像、评论数徽标）
├── assets/
│   ├── css/tailwind.css         ★ 预编译样式产物（Tailwind 4 + daisyUI 5，勿手改）
│   ├── css/theme.css            少量补充样式（正文字号、邮件 iframe、分页外观等）
│   ├── css/src/main.css         Tailwind 构建入口（改样式时才需要）
│   ├── js/mailbox.js            前端逻辑（模块交互）
│   └── img/                     brand-icon.svg、favicon.ico、icon-512.png
├── package.json                 样式构建脚本（仅开发期需要 Node）
├── screenshot.png               后台主题缩略图（1200×900）
└── 部署文档.md                  部署说明与排错清单
```

### 文件职责表

| 文件 | 职责 | 什么时候需要它 |
| --- | --- | --- |
| `style.css` | **只放主题头**（`Theme Name` / `Description` / `Version` / `Author` / `Text Domain`），不写真实样式 | 每个主题必需（WordPress 靠它识别主题） |
| `functions.php` | 定义主题路径/URL 常量、`require_once` 加载 `inc/`、`add_theme_support`、`wp_enqueue_*` 注册静态资源、关闭 WindPress 前台运行时 | 每个主题必需 |
| `inc/config.php` | **所有可变配置**（接口基址、凭证、缓存/限流/超时参数），每个常量用 `if ( ! defined( 'X' ) )` 保护 | 有接口、缓存、限额等可调参数时 |
| `inc/class-*.php` | 上游 API 业务客户端：签名、请求、超时、错误归一为 `WP_Error` | 需要调外部接口时 |
| `inc/rest-api.php` | 对外暴露 REST 路由：参数校验 → 限流 → 转发 → 结果过滤/缓存 | 前端需要经服务端访问上游时 |
| `inc/seo.php` | 补齐第三方 SEO 插件未覆盖的标签与字段 | 站内做 SEO 时 |
| `inc/comments.php` | 评论项渲染回调、占位头像、评论数统计与徽标、评论表单字段样式 | 需要评论区时 |
| `inc/friend-links.php` | 侧栏/页脚类数据层：服务端取数 + 缓存 + 失败兜底空数组 | 需要服务端渲染的外部数据时 |
| `header.php` | 输出 `<head>` 与页头导航，并**打开** `<main class="flex-1">` | 每个主题必需 |
| `footer.php` | **闭合** `<main>` 与最外层容器，输出页脚，最后 `wp_footer()` | 每个主题必需 |
| `home.php` / `archive.php` / `search.php` / `index.php` | 各类列表页：只负责标题/描述等页头，列表主体复用 `template-parts/post-list.php` | 需要文章系统时（`index.php` 为兜底，必留） |
| `single.php` | 文章详情：面包屑 + 标题信息 + 特色图 + 正文 + 标签 + 上下篇 + 侧栏 + 评论区 | 需要文章系统时 |
| `page.php` | 单页面：极简容器 + 标题 + 正文 | 站点有「关于」「隐私政策」等页面时 |
| `front-page.php` | 静态首页（优先级高于 `home.php`） | 首页是工作台/落地页而非文章列表时 |
| `404.php` | 404 提示 + 返回首页入口 | 每个主题建议具备 |
| `searchform.php` | 供 `get_search_form()` 优先加载的搜索表单 | 需要搜索时 |
| `comments.php` | 评论区模板（由 `comments_template()` 引入） | 需要评论区时 |
| `template-parts/*.php` | 可复用片段，用 `get_template_part()` 引入 | 同一结构被多处复用时 |
| `assets/css/{tailwind,theme}.css` | 预编译产物 / 少量自定义样式 | 见 `02-样式与构建.md` |
| `assets/js/*.js` | 前端逻辑，经 `wp_localize_script()` 拿到 `restUrl` 等注入值 | 页面需要交互时 |
| `package.json` | Tailwind 构建脚本与 devDependencies | 需要构建样式时 |
| `screenshot.png` | 后台「外观 → 主题」缩略图 | 建议具备 |
| `部署文档.md` | 部署、配置、排错、上线清单 | 主题要交付给别人时 |

---

## 三、`inc/` 分层约定（新增功能该放哪一层）

分层顺序固定为：**配置 → 业务客户端 → 数据/转发 → 输出补齐 → 渲染**。判断规则（自上而下第一条命中即可）：

| 你要加的东西 | 放哪一层 | 判断依据 |
| --- | --- | --- |
| 一个新参数：地址、密钥、缓存秒数、限额、超时 | `inc/config.php` | 凡是「换环境可能要改的值」都是配置，不能硬编码在业务逻辑里 |
| 一个新的上游接口调用（签名、URL 拼接、解包） | `inc/class-*.php` | 与外部服务打交道的方法都收在客户端类里，返回 `WP_Error` 或数组 |
| 前端要经服务端访问上游 | `inc/rest-api.php` | 路由注册、参数校验、限流都留在这一层；密钥不下发前端 |
| 服务端要给模板供数据（带缓存） | 单独的数据层文件（如 `inc/friend-links.php`），或复用客户端 | 取数逻辑不该写在模板里 |
| 补 `<head>` 标签、后台字段、图片 alt 等 | `inc/seo.php` 或对应补齐文件 | 只补第三方插件未覆盖的部分，不重复输出 |
| 评论区渲染、头像、评论数、表单样式 | `inc/comments.php` | 渲染逻辑与模板分离：`comments.php` 只做结构 |
| 页面结构、循环、类名 | 模板文件 / `template-parts/` | 模板里只做输出，不做取数计算 |

硬性约定：

1. **`inc/` 下的每个 PHP 文件都必须能在 `functions.php` 里被 `require_once` 加载**，顺序为 `config → class → 数据层 → rest-api → seo → comments`。
2. 每个 PHP 文件开头都要有：

   ```php
   // 防止直接访问该文件。
   if ( ! defined( 'ABSPATH' ) ) {
   	exit;
   }
   ```

3. 每个文件都带标准文件头注释（职责 / 设计意图 / `@package`），见参考实现。

---

## 四、命名约定

| 对象 | 约定 | 示例（与参考实现一致） |
| --- | --- | --- |
| 函数 | 主题前缀 `xy_mbx_` + 小写下划线；钩子回调以 `_after_setup_theme` / `_enqueue_assets` / `_register_rest_routes` / `_handle_*` 结尾 | `xy_mbx_after_setup_theme()`、`xy_mbx_enqueue_assets()`、`xy_mbx_is_rate_limited()`、`xy_mbx_handle_emails()` |
| 常量 | 全大写下划线，前缀 `XY_MBX_`；每个都用 `if ( ! defined( 'X' ) )` 保护 | `XY_MBX_THEME_DIR`、`XY_MBX_API_BASE`、`XY_MBX_RATE_RULES`、`XY_MBX_PROXY_IP_HEADER` |
| 局部/模板变量 | 前缀 `$xy_`，避免与 WordPress 全局变量（`$post`、`$wp_query`）撞名 | `$xy_posts_page_id`、`$xy_related_posts`、`$xy_real_count` |
| meta key | 下划线开头（隐藏自定义字段）+ 前缀 | `_xy_mbx_seo_keywords`（常量 `XY_MBX_SEO_KEYWORDS_META` 包装） |
| transient 键 | 前缀 `xy_mbx_` + 业务名（涉隐私的加 `md5()`） | `xy_mbx_domains`、`xy_mbx_friend_links`、`xy_mbx_mail_` + `md5( $email_id )`、`xy_mbx_rate_{bucket}_` + `md5( $ip )` |
| 自定义 CSS 类 | 前缀 `xy-`；Tailwind / daisyUI 类**不加前缀** | `.xy-entry`（正文容器）、`.xy-comment-text`、`.xy-mail-body`、`.xy-mail-frame` |
| 前端 DOM id | 前缀 `xy-` | `#xy-toast`、`#xy-generate`、`#xy-address`、`#comments` |
| 模板片段 | `template-parts/` 下小写短横线 `.php`；引入时不带 `.php` | `get_template_part( 'template-parts/post-list' )` |
| JS 注入变量 | 驼峰、与主题同名可辨识 | `wp_localize_script( 'xy-mbx-mailbox', 'XYMailbox', array( 'restUrl' => ... ) )` → `XYMailbox.restUrl` |
| 资源 handle | `xy-mbx-` + 用途，便于依赖与调试 | `xy-mbx-tailwind`、`xy-mbx-theme`、`xy-mbx-mailbox` |

---

## 五、`add_theme_support` 必选清单

在 `after_setup_theme` 钩子里一次性声明（函数 `xy_mbx_after_setup_theme()`）：

```php
add_theme_support( 'title-tag' );             // 1
add_theme_support( 'automatic-feed-links' );  // 2
add_theme_support( 'post-thumbnails' );       // 3
add_theme_support(                           // 4
	'html5',
	array( 'comment-list', 'comment-form', 'search-form', 'gallery', 'caption', 'style', 'script' )
);
```

| 项 | 实际作用 | 不声明的后果 |
| --- | --- | --- |
| `title-tag` | WordPress 自动生成并输出 `<title>`（含分隔符与站点名） | 模板必须自己写 `<title>`，否则页面无标题；且 SEO 插件无法接管标题 |
| `automatic-feed-links` | 在 `<head>` 输出 `rel="alternate" type="application/rss+xml"` 的 feed 链接 | 文章/评论 RSS 无法被自动发现 |
| `post-thumbnails` | **开启后台编辑页的「特色图片」元框**，并启用 `has_post_thumbnail()` / `the_post_thumbnail()` / `set_post_thumbnail_size()` | 后台看不到特色图片元框（只能靠 REST 写 `_thumbnail_id`）；列表封面与侧栏缩略图全部失效 |
| `html5`（数组） | 让 `wp_list_comments()`、`comment_form()`、`get_search_form()`、图集与图注输出 HTML5 语义标签；`style`/`script` 允许 WP 省略 `type` 属性 | 评论区/表单输出 XHTML 风格标记，与现代样式与语义不符 |

> 注意：`post-thumbnails` 只影响后台元框与缩略图尺寸注册；`has_post_thumbnail()` 读的是 `_thumbnail_id` 元数据，所以「元框缺失但元数据存在」时前台仍会渲染封面——但这属于主题缺陷，应补上 `add_theme_support( 'post-thumbnails' )`。

---

## 六、`style.css` 只放主题头

`style.css` 内容固定为「主题头注释 + 一句说明」，**不写任何真实样式**：

以下为主题头示例，**复制后替换为你的主题信息**：

```css
/*
Theme Name: Your Theme Name
Description: 一句话描述你的主题。
Version: 1.0.0
Author: Your Name
Text Domain: your-theme-slug
*/

/* 说明：真实样式统一放在 assets/css/theme.css，本文件仅用于 WordPress 主题头识别。 */
```

原因：

1. WordPress 只靠 `style.css` 的主题头识别主题，与样式内容无关；
2. 真实样式分两处、职责清晰：**预编译产物** `assets/css/tailwind.css`（由 `npm run build:css` 生成）与**自定义样式** `assets/css/theme.css`（手写、无需构建）；
3. 两个文件都在 `functions.php` 里用 `wp_enqueue_style()` 显式入队，并带 `filemtime()` 版本号；若把样式写进 `style.css`，会绕开这套入队与版本控制，还会被 WordPress 在部分场景自动加载造成重复。

```php
// functions.php 中的入队约定（片段）：文件不存在时不注册，避免 404。
$tailwind_relative_path = 'assets/css/tailwind.css';
$tailwind_absolute_path = XY_MBX_THEME_DIR . $tailwind_relative_path;
if ( file_exists( $tailwind_absolute_path ) ) {
	wp_enqueue_style( 'xy-mbx-tailwind', XY_MBX_THEME_URI . $tailwind_relative_path, array(), (string) filemtime( $tailwind_absolute_path ) );
}
```

`Text Domain` 与目录名保持一致（如 `your-theme-slug`）；主题路径常量由 `trailingslashit( get_template_directory() )` 与 `trailingslashit( get_template_directory_uri() )` 生成，模板中拼资源路径时统一用这两个常量，不要手写相对路径。
