主题开发指南
主题开发指南
本文面向 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. 快速开始
以复制默认主题为例,五步创建一个新主题:
- 复制
themes/default/整个目录,重命名为themes/mytheme/。 - 修改
mytheme/theme.json:更新 id、name、version、author、description。 - 将模板中的资源引用路径改为新主题目录,例如
header.html中的:
<link rel="stylesheet" href="{site.url}/themes/mytheme/assets/css/style.css">- 进入后台「外观 - 主题管理」,在新主题卡片上点击「设为默认主题」。
- 访问前台任意页面即可看到新主题生效;调整模板后刷新页面即可看到变化(若开启了缓存,需清空缓存,见第 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 字符串。全类型清单:
| 类型 | 生成的路径(默认规则) | 说明 |
|---|---|---|
| article | article/{slug或id} | 文章详情 |
| category | category/{slug或id} | 分类页 |
| tag | tag/{id} | 标签文章列表 |
| tags | tags | 标签列表页 |
| archives | archives | 文章归档 |
| page | page/{slug或id} | 独立页面 |
| search | search | 搜索页 |
| login / register / logout | login / register / logout | 登录、注册、登出 |
| forgot_password / reset_password | forgot-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) |
| user | user/{id} | 用户公开主页 |
| follow / following / followers | follow/{id} 等 | 关注关系页 |
| rss | rss | RSS 订阅 |
| api | api.php/v1 | API 入口(固定前缀) |
说明:未启用伪静态时自动带 /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/ 目录后,在「主题管理」中启用。主题与插件的协作机制见 插件开发指南。