主题模板制作指南
1. 工作方式
主题是 public/themes/{slug}/ 下的一组纯 PHP 文件。当前主题由数据库设置 active_theme 决定,后台“主题管理”可切换。静态资源位于主题目录中,由 Web 服务器直接返回,不需要编译。
制作新主题时复制 public/themes/default/,例如:
public/themes/acme/
theme.json
pages.php
layout.php
home.php
list.php
detail.php
page.php
contact.php
search.php
page-about.php
assets/style.css目录名就是主题 slug,只允许使用稳定的英文小写、数字和连字符。主题文件缺失时系统会写入日志并直接抛错,不会静默切回默认主题。
2. theme.json
{
"name": "Acme 官网",
"slug": "acme",
"version": "1.0.0",
"author": "Your Team",
"description": "Acme 企业站主题",
"screenshot": "screenshot.webp"
}后台读取 name、version、author、description 和 screenshot。若未声明截图,会依次探测 screenshot.png、screenshot.jpg、screenshot.webp、preview.png、preview.jpg。
3. 必需模板及输入
所有页面先渲染内容模板,再把结果作为 $content 交给 layout.php。
| 文件 | 用途 | 主要变量 |
|---|---|---|
layout.php | 全站 HTML 外壳 | $content、$title、$description、$locale、$prefix、$navSolid |
home.php | 首页 | $fields、$latest及共享变量 |
list.php | 栏目列表 | $cat、$pager或$tabs、$model |
detail.php | 内容详情 | $content、$cat、$model、$fields |
page.php | 标准单页 | $content、$cat、$fields、$faq |
contact.php | 联系页 | $cat、$fields、$faq |
search.php | 搜索页 | $q、$pager、$terms |
栏目可以在后台指定自定义 list_tpl 和 detail_tpl;值是不带 .php 的主题文件名。单页则通过 pages.php 决定模板。任何动态模板名都必须对应真实文件。
共享变量以 SiteController::shared() 的实际返回值为准。模板开发时不要假设可选变量一定存在,但也不要捕获和隐藏模板错误。
4. 页面模板 pages.php
pages.php 返回以模板 slug 为键的数组:
<?php
return [
'standard' => [
'name' => '标准单页',
'file' => 'page',
'fields' => [],
],
'landing' => [
'name' => '落地页',
'file' => 'page-landing',
'fields' => [
['name' => 'heading', 'label' => '标题', 'type' => 'text'],
['name' => 'intro', 'label' => '简介', 'type' => 'richtext'],
['name' => 'cover', 'label' => '封面', 'type' => 'image'],
],
],
];后台保存的模板字段会出现在 $fields 中。file 对应同目录的 {file}.php。
内容保存时,核心只覆盖本次表单负责的标准字段和当前 pages.php 已声明字段;extra 中其他主题或插件键会原样保留。仅从 pages.php 删除字段不会顺便删除历史数据。确需永久清理或改变数据结构时,应提供显式、可记录、可重复执行的数据迁移,不能借一次普通内容保存隐式删除。
5. 字段类型
当前后台渲染和保存逻辑支持:
| type | 后台控件 | 保存值 |
|---|---|---|
text | 单行文本 | 字符串 |
textarea | 多行纯文本 | 字符串 |
richtext | SunEditor 富文本 | HTML 字符串 |
image | 媒体库图片选择 | URL 字符串,同时生成 {name}_alt |
number | 数字输入框 | 输入字符串,模板需要自行转换/格式化 |
date | 日期选择器 | YYYY-MM-DD 字符串或空字符串 |
toggle | 开关复选框 | 字符串 1 或 0 |
video | 视频 URL 输入框 | URL 字符串 |
select | 下拉选择 | options 中的字符串 |
repeater | 可新增、删除、排序的重复行 | 二维数组 |
section | 只读分段标题与说明 | 不保存数据,也不需要 name |
图片字段示例:
['name' => 'cover', 'label' => '封面图', 'type' => 'image']模板读取 $fields['cover'] 和 $fields['cover_alt']。输出时必须转义属性:
<img src="<?= htmlspecialchars((string) ($fields['cover'] ?? ''), ENT_QUOTES) ?>"
alt="<?= htmlspecialchars((string) ($fields['cover_alt'] ?? ''), ENT_QUOTES) ?>">Repeater 示例:
[
'name' => 'cards',
'label' => '卡片',
'type' => 'repeater',
'fields' => [
['name' => 'title', 'label' => '标题', 'type' => 'text'],
['name' => 'image', 'label' => '图片', 'type' => 'image'],
['name' => 'url', 'label' => '链接', 'type' => 'text'],
],
]模板中先确认每一行是数组,再读取 image_alt。后台会清理非数组行,并为图片保存 alt 文本。
6. 资源与 URL 工具
主题资源应通过:
use App\Core\Theme;
<link rel="stylesheet" href="<?= htmlspecialchars(Theme::url('assets/style.css')) ?>">前台路由文件提供:
theme_url($path, $prefix):生成本地化路径;默认语言无前缀。theme_link($path, $prefix):在本地化路径基础上按站点设置追加伪静态后缀。url_suffix():读取当前伪静态后缀。theme_section_bg():为连续内容区块生成交替背景类。
不要硬编码域名、语言前缀、主题目录或 .html 后缀。
7. 布局钩子与插件
layout.php 应保留:
<?php \App\Core\Hook::doAction('theme.head'); ?>
<?php \App\Core\Hook::doAction('theme.footer'); ?>theme.head 已由核心用于注入 SEO 元数据;移除它会破坏 canonical、hreflang、OG、Twitter Card 和 JSON-LD。theme.footer 可供插件插入页脚资源。目前这两个是已实际调用的主题动作钩子;不要把尚未调用的示例过滤器当成稳定 API。
插件位于 app/plugins/{slug}/plugin.php,并需在 config/config.php 的 plugins.enabled 中显式启用。插件不得修改 flight/。
8. 安全与输出规则
- 普通文本和 HTML 属性使用
htmlspecialchars(..., ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')。 - 富文本仅输出来自后台可信编辑流程的字段;不得把查询参数、表单值直接作为 HTML。
- 外链需验证协议;表单必须带 CSRF 字段并提交到已有 POST 路由。
- 上传文件只引用媒体库 URL,不在主题内实现第二套上传逻辑。
- 不捕获模板异常后输出空白页;缺文件、缺变量或数据形状错误应留日志并修正根因。
9. 调试与验收
- 对主题全部 PHP 文件执行
php -l。 - 后台激活主题,逐页检查首页、列表、详情、单页、联系、搜索和 404。
- 同时检查默认语言和至少一个非默认语言 URL。
- 检查浏览器控制台、网络 404、
storage/logs/和后台审计记录。 - 验证移动端、图片 alt、表单 CSRF、SEO head、sitemap、RSS 与 llms.txt。
- 在关闭 debug 的配置下再次验收,确保问题不会只在开发模式下暴露。