主题模板制作指南

最后更新 2026-07-29 ·主题目录结构、模板变量、pages.php 字段机制与输出安全规则。

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"
}

后台读取 nameversionauthordescriptionscreenshot。若未声明截图,会依次探测 screenshot.pngscreenshot.jpgscreenshot.webppreview.pngpreview.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_tpldetail_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多行纯文本字符串
richtextSunEditor 富文本HTML 字符串
image媒体库图片选择URL 字符串,同时生成 {name}_alt
number数字输入框输入字符串,模板需要自行转换/格式化
date日期选择器YYYY-MM-DD 字符串或空字符串
toggle开关复选框字符串 10
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.phpplugins.enabled 中显式启用。插件不得修改 flight/

8. 安全与输出规则

  • 普通文本和 HTML 属性使用 htmlspecialchars(..., ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
  • 富文本仅输出来自后台可信编辑流程的字段;不得把查询参数、表单值直接作为 HTML。
  • 外链需验证协议;表单必须带 CSRF 字段并提交到已有 POST 路由。
  • 上传文件只引用媒体库 URL,不在主题内实现第二套上传逻辑。
  • 不捕获模板异常后输出空白页;缺文件、缺变量或数据形状错误应留日志并修正根因。

9. 调试与验收

  1. 对主题全部 PHP 文件执行 php -l
  2. 后台激活主题,逐页检查首页、列表、详情、单页、联系、搜索和 404。
  3. 同时检查默认语言和至少一个非默认语言 URL。
  4. 检查浏览器控制台、网络 404、storage/logs/ 和后台审计记录。
  5. 验证移动端、图片 alt、表单 CSRF、SEO head、sitemap、RSS 与 llms.txt。
  6. 在关闭 debug 的配置下再次验收,确保问题不会只在开发模式下暴露。