模板开发
CnLog 采用原生 PHP 模板引擎,通过 doAction() / addAction() 钩子机制实现插件扩展。模板位于 themes/default/ 目录。
一、系统概述
1.1 核心常量
| 常量 | 说明 | 示例值 |
|---|---|---|
CNLOG_ROOT | 系统根目录物理路径 | /var/www/html/ |
BLOG_URL | 站点根 URL(末尾带 /) | https://blog.example.com/ |
THEME_PATH | 主题根目录物理路径 | /var/www/html/themes |
CURRENT_THEME | 当前主题标识 | default |
CONFIG_PATH | 配置文件目录物理路径 | /var/www/html/config/ |
1.2 安全要求
所有模板文件开头必须包含安全检查:
<?php if (!defined('CNLOG_ROOT')) { exit('error!'); } ?>
1.3 模板加载流程
用户请求 → index.php → 路由分发 → Controller
→ 设置模板变量
→ define THEME_PATH / CURRENT_THEME
→ require 模板文件
→ require header.php
→ 页面内容
→ require footer.php
二、目录结构
themes/default/ ├── theme.php # 主题元信息(返回 PHP 数组:name/version/author/description) ├── screenshot.png # 主题预览截图(可选) ├── layout.php # 全局布局(<head> + 导航栏 + 页脚 + 钩子挂载点) ├── assets/ │ └── style.css # 主题样式表 ├── images/ # 主题图片资源 ├── index/ # 前台页面模板 │ ├── index.php # 首页 │ ├── apps.php # 应用列表页 │ ├── detail.php # 应用详情页 │ ├── download.php # 下载页 │ └── docs.php # 文档页 ├── auth/ # 认证页面 ├── user/ # 用户中心页面 └── payment/ # 支付页面
三、主题元信息 — theme.php
主题通过 theme.php 文件声明元信息,返回一个 PHP 数组:
<?php
return [
'name' => 'Cnlog 默认主题',
'version' => '1.0.0',
'description' => '简洁现代的设计风格,支持响应式布局',
'author' => 'Cnlog Team',
'author_url' => 'https://cnlog.dev',
'has_settings'=> true,
];
| 字段 | 必填 | 说明 |
|---|---|---|
name | 必填 | 主题显示名称 |
version | 必填 | 主题版本号 |
description | 可选 | 主题描述 |
author | 可选 | 作者名称 |
author_url | 可选 | 作者主页 |
has_settings | 可选 | 是否支持后台设置(默认 true) |
四、主题设置 — 数据库持久化
主题设置通过后台 /admin/extension/theme-settings 管理,持久化存储在 app_theme_settings 表中(按主题标识 + 键名隔离)。前台 layout.php 启动时自动从数据库读取当前主题的设置。
4.1 默认设置项
| 键名 | 类型 | 说明 |
|---|---|---|
logo_type | radio | LOGO 类型:text/image |
logo_text | text | 文字 LOGO 内容 |
logo_image | text+upload | 图片 LOGO URL(通过 /admin/upload/logo 上传) |
nav_style | select | 导航栏样式 |
color_scheme | select | 配色方案 |
show_sidebar | switch | 是否显示侧边栏 |
show_friendlinks | switch | 是否显示友情链接 |
footer_text | textarea | 页脚文字 |
custom_css | textarea | 自定义 CSS(追加到样式表末尾) |
4.2 在模板中读取设置
// layout.php 顶部自动从数据库加载到 $themeSettings 数组
$logoType = $themeSettings['logo_type'] ?? 'text';
$logoText = $themeSettings['logo_text'] ?? site_config('site_name', 'Cnlog 应用中心');
五、页面模板详解
5.1 全局头部 — header.php
加载方式:layout.php 自动包含 <head> + 导航栏,通过 $content 变量注入页面内容
职责:初始化模板设置、输出 DOCTYPE 到 body、SEO meta 标签、CSS 加载、导航栏、搜索浮层。
| 变量 | 类型 | 说明 |
|---|---|---|
$site_name | string | 站点名称 |
$site_title | string | 页面标题 |
$site_description | string | 站点描述 |
$site_keywords | string | 站点关键词 |
$config | array | 系统配置数组 |
$sorts | array | 分类列表 |
$currentPage | string | 当前页面标识 |
5.2 全局页脚 — footer.php
加载方式:layout.php 自动包含页脚部分,通过 doAction('footer_before') / doAction('footer_after') 提供钩子挂载点
职责:页脚钩子、版权信息、全局 JS 加载。
5.3 文章列表页 — log_list.php
路由:首页、分类页、标签页
| 变量 | 类型 | 说明 |
|---|---|---|
$logs | array | 文章列表数组 |
$page_url | string/array | 分页 HTML |
$page | int | 当前页码 |
$total | int | 文章总数 |
$category | array|null | 当前分类信息 |
$tag | array|null | 当前标签信息 |
5.4 单篇文章数据结构
| 键 | 类型 | 说明 |
|---|---|---|
id | int | 文章 ID |
title | string | 文章标题 |
content | string | 文章内容(HTML) |
cover_image | string | 封面图 URL |
excerpt | string | 摘要 |
created_at | string | 发布时间 |
views | int | 浏览量 |
comment_count | int | 评论数 |
category_id | int | 分类 ID |
category_name | string | 分类名称 |
author_nickname | string | 作者昵称 |
is_top | int | 置顶等级(0=否,1=首页,2=分类,3=全局) |
5.5 文章详情页 — echo_log.php
路由:?r=article&id={id}
| 变量 | 类型 | 说明 |
|---|---|---|
$log | array | 当前文章详情(含 tags、like_count 等) |
$neighborLog | array | 上一篇/下一篇 |
$comments | array | 评论列表 |
$comments_count | int | 评论数 |
$commentCsrfField | string | 评论表单 CSRF 字段 |
$reactionCsrfToken | string | 点赞/收藏 CSRF Token |
5.6 搜索页 — search.php
路由:?r=search&keyword={keyword}
| 变量 | 类型 | 说明 |
|---|---|---|
$keyword | string | 搜索关键词 |
$totalCount | int | 结果总数 |
注意:搜索结果中的 $value['title'] 已包含高亮标签,不需要 htmlspecialchars() 转义。
5.7 个人中心 — profile.php / profile.clean.php
路由:?r=profile 或 ?r=profile&user_id={id}
| 变量 | 类型 | 说明 |
|---|---|---|
$user | array | 用户信息 |
$userArticles | array | 用户文章列表 |
$favoriteArticles | array | 收藏文章列表 |
$isSelf | bool | 是否查看自己 |
$isLoggedIn | bool | 是否已登录 |
5.8 独立页面
登录、注册、找回密码、重置密码、邮箱验证页面不加载 header.php / footer.php,是完整的独立 HTML 页面。
| 文件 | 路由 | 说明 |
|---|---|---|
login.php | ?r=login | 用户登录 |
register.php | ?r=register | 用户注册 |
forgot_password.php | ?r=forgot_password | 找回密码 |
reset_password.php | ?r=resetPassword | 重置密码 |
email_verify.php | ?r=verifyEmailCode | 邮箱验证 |
六、侧边栏组件系统
6.1 组件注册机制
侧边栏组件通过 widget_{name} 命名的函数实现,由 renderSidebarWidgets() 统一调度渲染。
// 渲染侧边栏(自动读取后台配置)
renderSidebarWidgets(); // 自动识别当前页面类型
renderSidebarWidgets('profile'); // 强制指定页面类型
6.2 内置组件
| 组件名 | 函数名 | 说明 | 缓存 |
|---|---|---|---|
blogger | widget_blogger() | 博主信息 | — |
search | widget_search() | 搜索框 | — |
recent_posts | widget_recent_posts($limit=5) | 最新文章 | 5 分钟 |
categories | widget_categories() | 分类目录 | 10 分钟 |
tags | widget_tags($limit=20) | 标签云 | 10 分钟 |
links | widget_links() | 友情链接 | 10 分钟 |
recent_comments | widget_recent_comments($limit=5) | 最新评论 | 5 分钟 |
archives | widget_archives() | 文章归档 | 15 分钟 |
hot_posts | widget_hot_posts($limit=5) | 热门文章 | 10 分钟 |
random_posts | widget_random_posts($limit=5) | 随便看看 | — |
6.3 自定义组件
if (!function_exists('widget_my_custom')) {
function widget_my_custom() {
?>
<div class="side">
<h3>我的组件</h3>
<p>自定义内容</p>
</div>
<?php
}
}
在后台侧边栏配置中启用 my_custom 组件即可。每个组件渲染前后会自动触发 widget_{name}_before 和 widget_{name}_after 钩子。
七、URL 生成 — Url 类
URL 生成使用全局 url() 函数,自动适配伪静态/动态 URL(定义在 app/core/functions.php)。
| 方法 | 参数 | 返回示例 |
|---|---|---|
Url::log($id) | 文章 ID | ?r=article&id=1 |
Url::sort($sortId) | 分类 ID | ?r=category&id=2 |
Url::tag($tagId) | 标签 ID | ?r=tag&id=3 |
Url::guestbook() | — | ?r=guestbook |
Url::profile($userId, $tab, $page) | 用户ID, Tab, 页码 | ?r=profile&user_id=1 |
Url::login() | — | ?r=login |
Url::register() | — | ?r=register |
Url::logout() | — | ?r=logout |
Url::rss() | — | ?r=rss |
Url::page($pageNum) | 页码 | 分页 URL |
注意:URL 格式取决于后台的伪静态配置。url() 函数根据 $GLOBALS['config'] 中的 rewrite_enabled 和 rewrite_suffix 自动适配。
八、钩子系统(模板开发视角)
8.1 核心机制
// 注册钩子
addAction('hook_name', 'my_callback_function', 10);
addAction('hook_name', function($param) { /* ... */ }, 10);
// 触发钩子(模板中)
doAction('hook_name');
doAction('hook_name', $param);
doAction('hook_name', ['key' => 'value']);
优先级:数字越小越先执行,默认为 10。
8.2 模板开发常用钩子
| 钩子名称 | 位置 | 说明 |
|---|---|---|
index_head | </head> 前 | 注入 CSS、meta 标签等 |
header_before | <body> 后 | 页面 body 开始后扩展 |
navbar | 导航栏内 | 插入导航链接 |
header_after | </header> 后 | 头部后扩展 |
footer_before | <footer> 前 | 页脚前扩展 |
footer_after | </footer> 后 | 页脚后扩展 |
article_content_before | 文章内容前 | 正文前扩展 |
article_content_after | 文章内容后 | 正文后扩展 |
index_loglist_top | 列表顶部 | 列表前扩展 |
index_loglist_bottom | 列表底部 | 列表后扩展 |
sidebar_before | 侧边栏顶部 | 所有组件前扩展 |
sidebar_after | 侧边栏底部 | 所有组件后扩展 |
九、开发规范与注意事项
9.1 缩略图获取优先级
封面图 → 内容首图 → 默认图 default-cover.svg
9.2 CSRF 防护
所有 POST 表单必须包含 CSRF Token,模板中通过变量获取:
<?php echo $commentCsrfField; ?> <!-- 评论表单 --> <?php echo $csrfField; ?> <!-- 独立页面 -->
9.3 前端资源加载
CSS 和 JS 建议通过 View::asset() 引用,自动附加版本号防止缓存:
<link rel="stylesheet" href="<?php echo View::asset('style.css'); ?>">
<script src="<?php echo View::asset('main.js'); ?>"></script>
9.4 创建自定义模板步骤
- 复制
themes/default/到新目录(如themes/mytheme/) - 修改
theme.php填写主题信息 - 根据需要修改
layout.php实现自定义布局和样式 - 在后台「扩展中心 → 主题管理」中启用新主题
9.5 安全注意事项
- 所有模板文件开头必须包含
if (!defined('CNLOG_ROOT')) { exit('error!'); } - 输出用户输入内容时必须使用
htmlspecialchars()转义 - 搜索结果的标题和摘要已包含高亮标签,不需要额外转义
- 表单必须包含 CSRF Token 字段