主题开发指南

更新于 2026-11-08 15:32:31 47 次浏览

主题开发指南

本文面向 BlogKit 主题开发者,介绍主题的目录结构、模板语法、页面模板约定、主题设置项以及打包发布流程。插件与主题的协作方式见 插件开发指南。

1. 主题是什么

主题决定博客前台的完整外观,由模板(templates)、静态资源(assets)和描述文件(theme.json)组成。系统按 default_theme 配置加载 themes/{主题目录名}/templates/ 下的模板渲染页面;前台外观由主题模板与已启用插件的钩子输出共同决定。

一个主题的典型目录结构如下(与默认主题一致):

themes/mytheme/
├── theme.json              # 主题描述文件(必需)
├── screenshot.png          # 主题缩略图(也支持 screenshot.jpg)
├── templates/              # 页面模板目录(必需),内含 common/ 公共模板子目录
└── assets/                 # 静态资源(css/js/images)

theme.json 字段说明

theme.json 为 JSON 格式,各字段如下:

字段必需说明
id是主题唯一标识,建议与主题目录名保持一致
name是主题显示名称
version是版本号,建议遵循 主版本.次版本.修订号
author否作者名称
description否主题功能简介
settings否设置项数组,每项含 key/label/type/default/description,见第 6 节

screenshot.png 要求

放在主题根目录,命名 screenshot.png(或 screenshot.jpg),用于后台「主题管理」列表的预览缩略图。建议使用主题首页截图,宽度 1200px 以上效果最佳。

2. 快速开始

以复制默认主题为例,五步创建一个新主题:

  1. 复制 themes/default/ 整个目录,重命名为 themes/mytheme/。
  2. 修改 mytheme/theme.json:更新 id、name、version、author、description。
  3. 将模板中的资源引用路径改为新主题目录,例如 header.html 中的:
<link rel="stylesheet" href="{site.url}/themes/mytheme/assets/css/style.css">
  1. 进入后台「外观 - 主题管理」,在新主题卡片上点击「设为默认主题」。
  2. 访问前台任意页面即可看到新主题生效;调整模板后刷新页面即可看到变化(若开启了缓存,需清空缓存,见第 9 节)。

3. 模板语法参考

模板以 .html 文件存放于 templates/,使用 {...} 风格标签。以下所有语法均给出默认主题中的真实用法。

3.1 变量输出

<meta name="author" content="{site.name}">
<link rel="icon" href="{site.url}/favicon.ico" type="image/x-icon">
<title>{seo_title}</title>

说明:

  • {变量名} 输出模板变量,未赋值的变量输出空字符串。
  • 支持点号路径访问嵌套数组:{user.username}、{pagination_info.total}、{config.icp_number}。
  • {site.xxx} 输出站点配置,可用键包括:name、url、description、charset、logo、logo_display_mode、rss_url,以及 comment_enabled、like_enabled、favorite_enabled、message_enabled、user_registration 等功能开关。
  • {config.xxx} 输出系统配置项(如 config.comment_enabled、config.captcha_enabled)。

3.2 条件判断

条件标签支持 {/if} 与 {endif} 两种闭合写法,均可与 {else} 搭配,支持任意深度嵌套。以下七种写法均为默认主题真实用例:

<!-- 写法一:完整条件表达式,支持比较运算、&&/|| 与括号组合 -->
{if condition="(site.logo_display_mode == 'auto' && site.logo) || (site.logo_display_mode == 'image' && site.logo)"}
    <img src="{site.logo}" alt="{site.name}" class="logo-image">
{else}
    {site.name}
{/if}
<!-- 写法二:isset 判断,变量存在且不为 false 时成立,可用 &&/|| 追加表达式 -->
{if isset="config.icp_display" && config.icp_display == 1 && config.icp_number}
    <a href="{config.icp_link}" class="footer-link">{config.icp_number}</a>
{/if}
<!-- 写法三:单变量真值判断(非空数组、非空字符串、非 0 即成立) -->
{if articles}
    ...
{else}
    <div class="empty-state"><p></p></div>
{/if}
<!-- 写法四:多变量与判断 -->
{if interaction_enabled && config.like_enabled}
    <button class="action-btn article-like-btn">...</button>
{endif}
<!-- 写法五:empty 判断,变量为空时成立 -->
{if empty="related_articles"}<p></p>{/if}
<!-- 写法六:equal 等值比较,支持模板变量、get.参数名 与 current_url -->
<li {if equal="get.m" value="info"}class="active"{endif}>
    <a href="{url:profile}?m=info"></a>
</li>
<!-- 写法七:field 字段判断(循环体内、文章页常用),支持比较运算与 {elseif} 链 -->
<button class="action-btn article-like-btn {if field='is_liked'}liked{/if}" data-article-id="{field name='id'}">
{if field='source' == 'liked'}
    ...
{elseif field='source' == 'favorited'}
    ...
{/if}

条件中的变量来源顺序:先取全局模板变量(支持点号路径),再回退到当前循环项字段。

3.3 循环 loop

{loop name="categories"}
    <li><a href="{url:category:id}">{field name="name"} ({field name="article_count"})</a></li>
{/loop}

name 支持四种数据源写法:

写法数据源示例
name="变量名"顶级模板变量(数组){loop name="articles"}
name="a.b"顶级变量的嵌套路径{loop name="user.articles"}
name=":"当前循环项自身(用于嵌套遍历)年-月-文章三级嵌套
name=":字段"当前循环项的某字段(支持 :a.b){loop name=":tags"}

循环体内可用变量:

名称说明写法
当前项字段循环项的任意字段{field name="title"}
当前循环键关联数组的键(如归档页的年份、月份){field name="_key"}
当前循环索引从 0 开始的自增序号{field name="_index"}
当前项元素数当前项(数组)的元素个数{count name=":"}

归档页的三级嵌套循环是以上用法的完整示例:

{loop name="archives"}
    <h2>{field name="_key"}</h2>
    {loop name=":"}
        <h3>{field name="_key"} ({count name=":"})</h3>
        {loop name=":"}
            <p>{field name="title"}</p>
        {/loop}
    {/loop}
{/loop}

3.4 字段输出 field

<span class="post-date">{field name="created_at" filter="relative1"}</span>
<span class="post-views">{field name="view_count" filter="cn_number"} </span>
  • name 必填;取值顺序为:当前循环项 - 全局变量 - article/tag/page/category 专用数据。
  • filter 可选,值为过滤器链(多个用 | 分隔,参数用 : 传递),见 3.7 过滤器表。
  • escape 可选,默认 true 自动 HTML 转义;输出可信 HTML 时显式关闭,如 <div class="post-content">{field name="content" escape="false"}</div>。

注意:过滤器应写在 filter 属性中(如 {field name='summary' filter="truncate:200"}),不要写在 name 值里。

3.5 URL 生成 url

<a href="{url:article:id}">{field name="title"}</a>
<a href="{url:category:category_id}">{field name="category_name"}</a>
<a href="{url:tag:id}" class="tag">{field name="name"}</a>
<form action="{url:search}" method="get" class="search-form">
<a href="{url:profile}" class="dropdown-item"></a>

格式为 {url:类型} 或 {url:类型:参数}。参数可以是字段名(自动从当前循环项或全局数据取值)、数字 ID 或 slug 字符串。全类型清单:

类型生成的路径(默认规则)说明
articlearticle/{slug或id}文章详情
categorycategory/{slug或id}分类页
tagtag/{id}标签文章列表
tagstags标签列表页
archivesarchives文章归档
pagepage/{slug或id}独立页面
searchsearch搜索页
login / register / logoutlogin / register / logout登录、注册、登出
forgot_password / reset_passwordforgot-password / reset-password找回/重置密码
profile 及子页profile、profile/articles 等个人中心(含 profile_articles / profile_comments / profile_settings / profile_favorites / profile_likes / profile_notifications / profile_history / profile_following / profile_followers)
useruser/{id}用户公开主页
follow / following / followersfollow/{id} 等关注关系页
rssrssRSS 订阅
apiapi.php/v1API 入口(固定前缀)

说明:未启用伪静态时自动带 /index.php 前缀;站长在后台自定义了某类型的重写规则时按自定义规则生成;未知类型回退为 /{类型};login/register/logout 以 redirect 为参数时自动附加当前页回跳地址。

3.6 包含 include

{include file="header"}
{include file="sidebar"}
{include file="common/pagination.html"}

查找顺序:先在 templates/ 根目录查找,再在 templates/common/ 查找,扩展名可省略。文件不存在时输出一条 <!-- Include file 'xxx' not found --> 注释,便于排错。

3.7 过滤器表

过滤器用于 {field} 的 filter 属性,可链式调用(filter="strip_tags|truncate:100")。时间类过滤器同时接受时间戳与日期字符串。

过滤器参数作用
upper / lower无转大写 / 转小写
ucfirst / ucwords无首字母大写 / 单词首字母大写
truncate长度(默认100)、后缀(默认...)按字符截断,超长追加后缀
date / time格式(date 默认 Y-m-d,time 默认 H:i:s)按格式输出日期 / 时间
datetime / date_en / datetime_en / time_12h无固定格式输出:Y-m-d H:i:s、June 4, 2024、June 4, 2024 3:30 PM、3:30 PM
timestamp / iso8601无转 Unix 时间戳 / ISO 8601 格式(RSS 等场景)
relative / relative1 / relative_en / time_ago无相对时间:中文标准版(3分钟前)、中文增强版(N秒前、昨天 HH:MM、5-12、2025-03-10)、英文版(3 minutes ago)、扩展注册版
escape / strip_tags / urlencode无HTML 转义 / 去除 HTML 标签 / URL 编码
number_format / cn_number / file_size小数位(cn_number 默认 1,其余默认 0)千分位数字 / 中文数字(1.2万、3.4亿)/ 字节数(KB/MB)
percent总数、小数位(默认2)计算百分比
markdown_parse无安全解析 Markdown 基本格式(粗体、斜体、链接)

默认主题中的常用示例:filter="relative1"(时间)、filter="cn_number"(浏览量)、filter="date"(归档日期)、filter="markdown_parse" escape="false"(消息内容)。

3.8 其他内置标签

标签说明
{assign var="名" value="值"}模板内定义变量,value 中可再用模板标签
{echo value="变量名"} / {count name="数组变量"}输出指定变量 / 输出数组元素个数
{top_badge}输出文章置顶标识(依据当前项 top_type)
{picture src="封面字段,回退字段" alt="alt字段"}输出响应式图片标签,参数见下文
{content_process field="content"}输出并处理正文内容(图片处理)
{breadcrumb}输出面包屑导航 HTML
{pagination} / {pagination:info}输出分页链接 / 分页信息(共 N 条,第 M / X 页)
{if-permission type="is_user"}仅登录用户可见;type 另有 is_guest(仅游客)、is_admin(仅管理员)
{csrf_token} / {csp_nonce}CSRF 令牌值 / CSP nonce(内联 script 使用)
{year} / {current_lang}当前年份 / 当前语言代码
{plugin_hook name="钩子名"}输出插件钩子内容,见第 8 节

{picture} 常用参数:src 必填,支持逗号分隔多字段回退;alt 默认 title;class 默认 lazy-img;wrap_url="article" 时外裹文章链接;url_field 指定链接取值字段;lazy 默认 true 启用懒加载。

{picture src="cover_image" alt="title" class="lazy-img" wrap_url="article" url_field="id"}

3.9 模板继承

模板引擎实现了 {extend file="父模板"} 与 {block name="块名"}...{/block}(支持 priority 属性与嵌套块):子模板中定义同名 block 即可替换父模板中的对应区块。需要说明的是,默认主题未使用继承,而是以 {include} 组合公共模板;新主题如需父模板骨架可使用该机制,写法如下:

{extend file="layout"}
{block name="content"}<h1>{field name="title"}</h1>{/block}

4. 页面模板清单

默认主题的模板文件与页面/路由对应关系如下(新主题建议保持同名文件以覆盖所有页面):

模板文件页面渲染入口
index.html / category.html / tag.html / tags.html / archives.html / search.html各类文章列表页(首页、分类、标签、标签列表、归档、搜索)对应列表路由
article.html / page.html / user.html / 404.html文章详情、独立页面、用户公开主页、404 页面对应详情路由;404 用于未匹配路由
login.html / register.html / forgot-password.html / reset-password.html登录 / 注册 / 找回密码 / 重置密码对应路由
user_profile.html个人中心框架页个人中心路由
profile_info / profile_settings / profile_privacy / profile_articles / profile_comments / profile_favorites / profile_likes / profile_following / profile_followers / profile_history / profile_notifications / profile_messages.html个人中心各子页(个人信息、设置、隐私、我的文章 / 评论 / 收藏 / 点赞 / 关注 / 粉丝、阅读历史、通知设置、消息中心)个人中心子页路由
common/header.html / navbar.html / footer.html / sidebar.html / pagination.html全站头部、导航栏、底部、侧边栏、分页条header 引用 navbar,页面引用 header/footer/sidebar,列表页引用 pagination
common/article_comments.html / article_vertical_actions.html / profile_header.html / profile_nav.html / avatar_upload.html评论区、桌面端文章悬浮操作栏、个人中心头部 / 导航 / 头像上传被对应页面引用

5. 公共模板与 include

公共模板放在 templates/common/ 下,通过 {include} 引入。默认主题的标准骨架:

{assign var="page_title" value="最新文章"}
{include file="header"}
<main>
    <div class="container">
        <!-- 页面主体内容 -->
        {include file="sidebar"}
    </div>
</main>
{include file="footer"}

约定说明:

  • 页面模板首行用 {assign} 定义 page_title,作为该页标题的统一入口。
  • header.html 承担 SEO 标签({seo_title} 等)、CSRF 令牌、RSS 声明与资源引入。

6. 主题设置项开发

主题可在 theme.json 的 settings 数组中声明设置项,站长在后台「主题管理 - 设置」中填写,模板中直接读取。完整流程三步:

第一步:在 theme.json 中声明设置项。

{
    "settings": [
        {
            "key": "show_back_to_top",
            "label": "显示回到顶部按钮",
            "type": "checkbox",
            "default": "1",
            "description": "是否在页面显示回到顶部按钮"
        }
    ]
}

支持的 type 类型:

type渲染控件说明
text单行文本框普通文本设置
textarea多行文本框代码片段、统计代码等
checkbox开关保存值为 1 或 0
select下拉框需提供 options(键值对:值 - 显示文本)
color颜色选择器主题色等颜色值

第二步:站长在后台「外观 - 主题管理 - 设置」中填写并保存;设置值以 JSON 存储并支持导入导出。

第三步:模板中读取。每个设置项会生成两个可用形式:独立变量 {theme_设置键} 与数组变量 {theme_settings.设置键}。

{if theme_show_back_to_top}<div id="back-to-top" class="back-to-top">...</div>{endif}
{if theme_sidebar_custom_html}<div class="widget sidebar-custom-html">{theme_sidebar_custom_html}</div>{/if}

说明:checkbox 类型直接用于 {if} 判断;textarea 类型常用于输出站长自定义的 HTML 片段。

7. 常用模板变量速查

以下变量由前台控制器在渲染时注入,按页面分组(「通用」部分各页面均可用):

【通用(header/footer/sidebar/navbar 等全站模板)】
site.* / config.*       站点配置与系统配置开关(名称、URL、描述、评论、验证码、备案号等)
seo_title / seo_description / seo_keywords   当前页 SEO 三要素
csrf_token / csp_nonce  安全令牌与 CSP nonce
theme_设置键 / theme_settings   主题设置(见第 6 节)
user                    当前登录用户信息(未登录不可用)
total_notifications     未读通知数(「消息通知」插件提供,默认 0)
interaction_enabled / notification_enabled   插件启用标记(见第 8 节)
is_mobile / is_desktop / is_tablet   设备判断
current_lang            当前语言
categories / all_tags / latest_articles / pages / friendlinks   分类、标签云、最新文章、独立页面、友情链接
breadcrumb              面包屑数据(配合 {breadcrumb} 标签)

【首页与分类、标签列表页】
articles                文章列表(字段含 id/title/description/cover_image/user_id/author/category_id/
                        category_name/created_at/view_count/comment_count/tags/top_type)
pagination / pagination_info    分页对象与信息(total/current_page/total_pages)
category / tag          当前分类 / 标签实体(对应页面)

【文章详情页】
article                 当前文章(含 content/summary/updated_at 等)
tags                    当前文章的标签列表
comments / comment_count  评论列表与总数
previous_article / next_article   上一篇 / 下一篇
related_articles        相关文章
canonical_url / current_user_id   规范链接 / 当前登录用户 ID

【搜索页】
keyword / search.keyword   搜索关键词
articles                结果列表(额外含 highlighted_title/highlighted_excerpt 高亮字段)
total_articles / has_search_results   结果总数 / 是否有结果
sort / sort_options     当前排序与排序选项

【归档页】
archives                按年份分组的嵌套数组(年 - 月 - 文章三级)
total_articles          归档文章总数

【登录、注册、找回密码页】
error / success         错误与成功提示
redirect                登录后的回跳地址
captcha_enabled 等验证码相关变量

【个人中心(profile_* / user_profile)】
profile_user            被查看用户的资料(nickname/username/bio/website/avatar 等)
user                    当前登录用户资料
articles / comments / favorites 等   各 Tab 对应的数据列表

8. 钩子与插件配合

主题模板中通过 {plugin_hook} 标签输出插件内容,未启用对应插件时输出为空,不影响页面。默认主题在 header.html 与 footer.html 中预留了两个挂载点:

{plugin_hook name="page_header"}   <!-- 页面头部 -->
{plugin_hook name="page_footer"}   <!-- 页面底部 -->

插件可监听同名钩子向该位置注入内容。此外模板引擎在渲染前还会触发 template_render_before、template_render_after、template_assign 等渲染期钩子,插件可借此修改变量或输出。

主题模板应以「插件标记变量」守卫插件相关 UI,避免插件未启用时出现死链接或无效按钮:

{if interaction_enabled}
    <span class="post-likes">{field name="like_count" filter="cn_number"} </span>
{endif}
{if notification_enabled}
    <a href="{url:profile}?m=messages">
        {if total_notifications}<span class="unread-count">{total_notifications}</span>{/if}
    </a>
{endif}

可用标记变量:interaction_enabled(互动增强:点赞/收藏/关注/阅读历史)、notification_enabled(消息通知:消息中心/通知设置)。插件的开发方式见 插件开发指南。

8.1 插件挂载点(强制规范)

除 {plugin_hook} 钩子外,主题模板还必须预留两个插件挂载点(插件经 Plugin::slot() 向这些位置注入样式、脚本与配置)。这是主题的强制要求:缺失挂载点的主题将导致依赖头部注入的插件(如行为验证类插件)无法加载前端资产,相关功能不可用。

<!-- header.html:必须位于 </head> 之前 -->
{php}Plugin::slot('front.head');{/php}

<!-- footer.html:必须位于 </body> 之前 -->
{php}Plugin::slot('front.footer');{/php}

注意:

  • 主题模板是字符串解析后输出,裸 <?php 代码不会执行,必须使用 {php}...{/php} 标签块;
  • 两个挂载点的位置不可挪动:front.head 必须在 </head> 闭合前(头部 CSS/JS/配置对象的规范注入位),front.footer 必须在 </body> 闭合前(尾部脚本的规范注入位);
  • 没有插件订阅时 Plugin::slot() 输出为空字符串,不影响页面渲染与性能;
  • 默认主题已内置这两个挂载点,第三方主题请对照补齐。

8.2 行为验证类插件的适配说明

以官方「极验行为验证」插件为例,验证码组件的渲染分两层:

  • L1 标记渲染:模板中的验证码区块经 {if geetest_active} 分支输出 <div class="geetest-wrapper" data-geetest-scene="场景名"></div> 容器,插件 JS 扫描该标记并在容器内初始化极验组件;
  • L2 自动接管:若主题模板未改造(仍只有内置验证码容器 captcha-wrapper),插件 JS 会尝试自动接管内置容器。此为兜底路径,主题应优先按 L1 方式改造验证码区块。

主题开发者改造验证码区块的标准写法(以登录页为例,其余场景同构):

{if captcha_enabled && captcha_login_enabled}
    {if geetest_active}
        <!-- 插件接管:渲染极验容器 -->
        <div class="geetest-wrapper" data-geetest-scene="login"></div>
    {else}
        <!-- 未接管:保留内置验证码区块(原有代码不动) -->
        <div class="form-group">
            <label></label>
            <div class="captcha-wrapper" data-captcha-width="120" data-captcha-height="40"></div>
        </div>
    {/if}
{endif}

插件停用或未安装时 geetest_active 恒为假,内置验证码即刻恢复,主题无需任何额外处理;场景名取值与页面一一对应:login、register、forgot、reset、comment(后台为 admin_login、admin_forgot、admin_reset,后台模板由系统维护,主题无需处理)。

9. 调试技巧

开启调试模式:编辑 core/config/system.php 中的 debug 配置:

'debug' => ['enabled' => true, 'panel_enabled' => true, 'error_display' => 'development'],
// enabled 为调试总开关(上线前改回 false);error_display 用 development 显示错误详情、production 隐藏

调试要点:

  • 模板变量不存在时输出空字符串而非报错,可先用 {if isset="变量名"} 或 {if empty="变量名"} 定位变量是否存在。
  • {include} 的文件找不到时会输出 <!-- Include file 'xxx' not found --> 注释,查看页面源码即可定位。
  • 模板标签书写错误(如 {/if}、{/loop} 未闭合配对)会导致对应代码块不渲染。
  • 模板缓存位于 storage/cache/templates/,跟随后台「缓存功能」开关;开启缓存期间修改模板可能不立即生效,可在后台清空缓存后刷新。
  • 调试完成后务必将 debug.enabled 改回 false 再上线。

10. 打包与发布

主题开发完成后按以下结构打包为 zip 文件:

mytheme.zip
└── mytheme/(theme.json、screenshot.png、templates/、assets/)

发布要求:

  • theme.json 为必需清单文件,id 必须与主题目录名一致,且不能与已有主题重复;version 使用三段式版本号,功能更新时同步递增。
  • zip 内允许带一层顶层目录(如上例的 mytheme/),安装时会自动识别清单文件位置。

分发方式:站长可在后台「应用市场」中直接搜索安装主题(zip 由市场托管);也提供 zip 文件供站长手动解压到 themes/ 目录后,在「主题管理」中启用。主题与插件的协作机制见 插件开发指南。