LiteLog 主题开发规范 v1.0
只需阅读这一个文件,就能开发出一个完整可用的 LiteLog 主题。
本文档覆盖目录结构、文件约定、可用变量、API 函数、钩子清单、静态资源、完整示例、常见问题。
目录
一、快速开始
最小可运行主题(4 个文件)
在 content/templates/mytheme/ 下创建:
header.php
<?php
/**
* Theme Name: My Theme
* Description: 我的第一个主题
* Version: 1.0.0
* Author: 你的名字
*/
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
</head>
<body>
<header>
<a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
</header>
<main>
log_list.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
include View::resolve('header');
?>
<?php foreach ($logs as $log): ?>
<article>
<h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
<p><?= h($log['excerpt']) ?></p>
</article>
<?php endforeach; ?>
<?= $pageHtml ?? '' ?>
<?php include View::resolve('footer'); ?>
echo_log.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<article>
<h1><?= h($log['title']) ?></h1>
<div><?= $log['content'] ?></div>
</article>
<?php include View::resolve('footer'); ?>
footer.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<footer>© <?= date('Y') ?> <?= h(Option::get('site_title')) ?></footer>
</body>
</html>
启用:后台 → 主题 → 找到 "My Theme" → 点"启用此主题"。
二、目录结构
标准结构
content/templates/主题名/
├── header.php ← 必需,页头
├── footer.php ← 必需,页尾
├── log_list.php ← 必需,文章列表(首页/分类/标签/作者/归档)
├── echo_log.php ← 必需,文章详情
├── page.php ← 可选,自定义页面
├── 404.php ← 可选,404 页面
├── side.php ← 可选,侧边栏
├── module.php ← 可选,公共模块
├── plugins.php ← 可选,主题级插件调用
├── options.php ← 可选,主题后台设置项
├── callback.php ← 可选,主题生命周期回调
├── preview.jpg ← 可选,主题预览图(900×600 推荐)
├── css/
│ └── style.css
├── js/
│ └── theme.js
└── images/
└── logo.png
命名规则
| 项 | 规则 | 示例 |
|---|---|---|
| 目录名 | 小写字母、数字、下划线、短横线 | nebula、my-theme |
| PHP 文件 | 与内核约定一致(见上文) | header.php |
| CSS/JS | 建议放 css/、js/ 目录 | css/style.css |
禁止事项
- ❌ 不要用中文、空格命名
- ❌ 不要修改 LiteLog 内核文件
- ❌ 不要把
index.php放进主题目录
三、头部注释规范
header.php 最顶部必须包含主题信息:
<?php
/**
* Theme Name: 主题显示名称
* Description: 一句话描述主题风格
* Version: 1.0.0
* Author: 作者名
* Author URI: https://example.com
*/
字段说明
| 字段 | 必填 | 说明 |
|---|---|---|
Theme Name | ✅ | 后台显示的名称(也接受 Name) |
Description | 否 | 主题描述 |
Version | 否 | 版本号 |
Author | 否 | 作者名 |
Author URI | 否 | 作者主页(也接受 Author URL) |
格式要求
- 冒号用英文
: - 注释块必须在文件最前面
- 用
/** ... /或/ ... */ - 描述中不要含
*/
四、必需文件清单
4.1 header.php —— 页头
职责:<!DOCTYPE html> 到主内容区之前的所有内容。
必须包含:
if (!defined('LITELOG_ROOT')) exit('error!');(安全判断)- 顶部注释头
- HTML
<head>(title、meta、CSS 引用) - 顶部导航区
<body>起始
核心变量(可选注入):
| 变量 | 说明 |
|---|---|
$page_title | 页面标题,模板可在 include 前自行赋值 |
$log | 文章详情页才有,其他页面未定义 |
核心钩子:
Hook::doAction('header_meta')—— meta 标签之后Hook::doAction('template_head')——<head>内 CSS 之后Hook::doAction('header_after')——</header>之后Hook::doAction('before_content')—— 主内容区之前
推荐模板:
<?php
/**
* Theme Name: Your Theme
* Description: ...
* Version: 1.0.0
* Author: ...
*/
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<?php Hook::doAction('template_head'); ?>
</head>
<body>
<header class="site-header">
<a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
<nav>
<a href="<?= BLOG_URL ?>">首页</a>
<?php foreach (SortModel::all() as $s): ?>
<a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a>
<?php endforeach; ?>
</nav>
</header>
<?php Hook::doAction('header_after'); ?>
<?php Hook::doAction('before_content'); ?>
<main class="container">
4.2 footer.php —— 页尾
职责:</main> 之后的收尾。
核心钩子:
Hook::doAction('after_content')—— 主内容区之后Hook::doAction('footer_before')——<footer>之前Hook::doAction('template_footer')——<footer>内部,脚本之前
推荐模板:
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="site-footer">
© <?= date('Y') ?> <?= h(Option::get('site_title')) ?>
</footer>
<?php Hook::doAction('template_footer'); ?>
</body>
</html>
4.3 log_list.php —— 文章列表
职责:首页、分类页、标签页、作者页、归档页共用。
上下文变量:
| 变量 | 说明 |
|---|---|
$logs | 数组,文章列表,见 5.1 |
$pageHtml | 字符串,分页 HTML(如为空说明只有一页) |
$pageType | 字符串,index / sort / tag / author / record |
$sort | 分类页才有,见 5.2 |
$tag | 标签页才有,见 5.3 |
$author | 作者页才有,见 5.4 |
$recordYear / $recordMonth | 归档页才有 |
核心钩子:
Hook::doAction('list_page_before', $pageType)Hook::doAction('list_item_before', $log, $pageType)(循环内)Hook::doAction('list_item_after', $log, $pageType)(循环内)Hook::doAction('list_page_after', $pageType)
推荐模板:
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
if (!empty($sort)) $page_title = $sort['sortname'] . ' - ' . $page_title;
if (!empty($tag)) $page_title = '标签:' . $tag['tagname'] . ' - ' . $page_title;
if (!empty($author)) $page_title = ($author['nickname'] ?: $author['username']) . ' 的文章';
if (!empty($recordYear)) $page_title = "{$recordYear}年{$recordMonth}月归档";
include View::resolve('header');
?>
<div class="layout">
<div class="main-col">
<?php Hook::doAction('list_page_before', $pageType ?? 'index'); ?>
<?php if (empty($logs)): ?>
<p class="empty">暂无文章</p>
<?php else: foreach ($logs as $log): ?>
<?php Hook::doAction('list_item_before', $log, $pageType ?? 'index'); ?>
<article class="post-item">
<h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
<div class="meta">
<span><?= h($log['date_fmt']) ?></span>
<span>浏览 <?= (int)$log['views'] ?></span>
<span>评论 <?= (int)$log['comnum'] ?></span>
</div>
<p class="excerpt"><?= h($log['excerpt']) ?></p>
<a href="<?= h($log['url']) ?>">阅读全文 →</a>
</article>
<?php Hook::doAction('list_item_after', $log, $pageType ?? 'index'); ?>
<?php endforeach; endif; ?>
<?= $pageHtml ?? '' ?>
<?php Hook::doAction('list_page_after', $pageType ?? 'index'); ?>
</div>
<aside class="side-col">
<?php include View::resolve('side'); ?>
</aside>
</div>
<?php include View::resolve('footer'); ?>
4.4 echo_log.php —— 文章详情
上下文变量:
| 变量 | 说明 |
|---|---|
$log | 数组,文章对象,见 5.1,含额外字段 |
$prev | 数组或 null,上一篇(含 gid、title、url) |
$next | 数组或 null,下一篇 |
$comments | 数组,评论树(含 children),见 5.5 |
$log 的额外字段(控制器补充):
| 字段 | 说明 |
|---|---|
url | 文章完整 URL |
date_fmt | 格式化的日期 |
tags | 标签数组(每项含 tid、tagname) |
sortname | 分类名 |
sorturl | 分类页 URL |
author_name | 作者昵称 |
核心钩子:
Hook::doAction('log_view_before', $log)Hook::doAction('log_content_before', $log)Hook::doAction('log_content_after', $log)Hook::doAction('log_view_after', $log)Hook::doAction('comment_display_before', $comment)(每条评论)Hook::doAction('comment_display_after', $comment)Hook::doAction('comment_form_before', $log)Hook::doAction('comment_form_fields', $log)Hook::doAction('comment_form_after', $log)
推荐模板:
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<div class="layout">
<div class="main-col">
<?php Hook::doAction('log_view_before', $log); ?>
<article class="post-single">
<h1><?= h($log['title']) ?></h1>
<div class="meta">
<span><?= h($log['date_fmt']) ?></span>
<?php if ($log['sortname']): ?>
<span>分类:<a href="<?= h($log['sorturl']) ?>"><?= h($log['sortname']) ?></a></span>
<?php endif; ?>
<span>作者:<?= h($log['author_name']) ?></span>
<span>浏览 <?= (int)$log['views'] ?></span>
</div>
<?php if ($log['tags']): ?>
<div class="tags">
<?php foreach ($log['tags'] as $t): ?>
<a href="<?= Url::tag($t['tagname']) ?>">#<?= h($t['tagname']) ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="content">
<?php Hook::doAction('log_content_before', $log); ?>
<?= $log['content'] ?>
<?php Hook::doAction('log_content_after', $log); ?>
</div>
</article>
<?php Hook::doAction('log_view_after', $log); ?>
<nav class="post-nav">
<?php if ($prev): ?><a href="<?= h($prev['url']) ?>">← <?= h($prev['title']) ?></a><?php endif; ?>
<?php if ($next): ?><a href="<?= h($next['url']) ?>"><?= h($next['title']) ?> →</a><?php endif; ?>
</nav>
<section id="comment" class="comment-section">
<h3>评论 (<?= CommentModel::countByLog($log['gid']) ?>)</h3>
<?php $render = function ($list, $depth = 0) use (&$render) {
foreach ($list as $c) {
Hook::doAction('comment_display_before', $c);
?>
<div class="comment" style="margin-left:<?= $depth * 24 ?>px">
<strong><?= h($c['poster']) ?></strong>
<span><?= datetimeFmt($c['date']) ?></span>
<div><?= nl2br(h($c['comment'])) ?></div>
</div>
<?php
Hook::doAction('comment_display_after', $c);
if (!empty($c['children'])) $render($c['children'], $depth + 1);
}
};
$render($comments); ?>
<?php if ($log['allow_remark'] !== 'n'): ?>
<?php Hook::doAction('comment_form_before', $log); ?>
<form method="post" action="<?= BLOG_URL ?>?action=comment_post">
<input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
<input type="text" name="poster" placeholder="昵称 *" required>
<input type="email" name="email" placeholder="邮箱">
<input type="url" name="url" placeholder="网站">
<textarea name="comment" rows="5" placeholder="说点什么..." required></textarea>
<?php Hook::doAction('comment_form_fields', $log); ?>
<button type="submit">发表评论</button>
</form>
<?php Hook::doAction('comment_form_after', $log); ?>
<?php endif; ?>
</section>
</div>
<aside class="side-col">
<?php include View::resolve('side'); ?>
</aside>
</div>
<?php include View::resolve('footer'); ?>
4.5 可选文件
| 文件 | 用途 | 触发时机 |
|---|---|---|
page.php | 自定义页面(关于、留言板) | 通过 View::display('page', ['title'=>..., 'content'=>...]) 主动调用 |
404.php | 404 页面 | 文章/分类不存在时自动调用 |
side.php | 侧边栏 | log_list.php / echo_log.php 中 include View::resolve('side') |
module.php | 公共小组件(如"最新文章") | 主题内 include View::resolve('module') |
plugins.php | 主题级插件调用(启用后自动加载) | 内核 loadActivePlugins() 检测后 require |
options.php | 主题后台设置项 | 后台主题管理页 |
callback.php | 生命周期回调 | 主题启用/停用时 |
五、可用变量速查
5.1 文章对象 $log
| 字段 | 类型 | 说明 |
|---|---|---|
gid | int | 文章 ID |
title | string | 标题 |
date | int | 时间戳 |
content | string | 渲染后的 HTML(直接输出,不要 h()) |
content_md | string | Markdown 源码(一般不用) |
excerpt | string | 摘要(可能为空) |
alias | string | URL 别名 |
author | int | 作者 UID |
sortid | int | 分类 ID |
type | string | blog / page |
views | int | 浏览数 |
comnum | int | 评论数 |
top | string | 是否置顶:y / n |
hide | string | 是否隐藏:y / n |
allow_remark | string | 是否允许评论 |
| 以下字段仅详情页有 | ||
url | string | 完整 URL |
date_fmt | string | Y-m-d 格式 |
tags | array | 标签数组(每项含 tid、tagname) |
sortname | string | 分类名 |
sorturl | string | 分类页 URL |
author_name | string | 作者昵称 |
用法:
<?= h($log['title']) ?> // 标题需要转义
<?= $log['content'] ?> // 内容直接输出(已渲染)
<?= (int)$log['views'] ?> // 数字用 (int) 转
5.2 分类对象 $sort
| 字段 | 类型 | 说明 |
|---|---|---|
sid | int | 分类 ID |
sortname | string | 分类名 |
alias | string | URL 别名 |
pid | int | 父分类 ID(0 为顶级) |
description | string | 描述 |
5.3 标签对象 $tag
| 字段 | 类型 | 说明 |
|---|---|---|
tid | int | 标签 ID |
tagname | string | 标签名 |
5.4 用户对象 $author
| 字段 | 类型 | 说明 |
|---|---|---|
uid | int | 用户 ID |
username | string | 用户名 |
nickname | string | 昵称 |
email | string | 邮箱 |
role | string | admin / writer / visitor |
5.5 评论对象 $comment
| 字段 | 类型 | 说明 |
|---|---|---|
cid | int | 评论 ID |
gid | int | 所属文章 ID |
pid | int | 父评论 ID(0 为顶级) |
date | int | 时间戳 |
poster | string | 昵称 |
comment | string | 评论内容(未渲染,需要 h() + nl2br()) |
email | string | 邮箱 |
url | string | 网站 |
children | array | 子评论数组(控制器构建,含嵌套) |
5.6 全局配置
通过 Option::get() 读取:
| 键名 | 说明 | 默认 |
|---|---|---|
site_title | 站点名称 | My LiteLog |
site_description | 站点描述 | — |
site_key | 站点关键词 | — |
index_lognum | 首页文章数 | 10 |
current_template | 当前主题名 | default |
url_rewrite | 伪静态开关 | n |
六、可用 API 速查
6.1 常量
| 常量 | 示例值 | 说明 |
|---|---|---|
BLOG_URL | https://example.com/ | 站点根 URL,末尾带斜杠 |
TEMPLATE_URL | https://example.com/content/templates/nebula/ | 当前主题 URL,末尾带斜杠 |
CONTENT_PATH | /www/site/content/ | content 目录绝对路径 |
DB_PREFIX | lt_ | 数据表前缀 |
6.2 通用函数
| 函数 | 说明 |
|---|---|
h($str) | HTML 转义输出(所有用户输入都要经过) |
redirect($url) | 302 跳转并退出 |
makeAlias($str) | 生成 URL 友好字符串 |
dateFmt($ts) | 时间戳 → Y-m-d |
datetimeFmt($ts) | 时间戳 → Y-m-d H:i |
stripTags($html, $len) | 去标签 + 截断 |
getExcerpt($html, $len) | 提取摘要 |
isLoggedIn() | 是否已登录 |
checkLogin() | 未登录则跳转登录页 |
pagination($page, $total, $perPage) | 生成分页 HTML |
getPageNumber() | 获取当前页码(?page=) |
6.3 URL 生成
| 方法 | 说明 | 示例输出 |
|---|---|---|
Url::log($gid, $alias) | 文章 URL | https://example.com/post/123.html |
Url::sort($sid, $alias) | 分类 URL | https://example.com/sort/tech |
Url::tag($name) | 标签 URL | https://example.com/tag/php |
Url::author($uid) | 作者 URL | https://example.com/author/1 |
Url::record($date) | 归档 URL | https://example.com/record/2026-10 |
6.4 数据模型
LogModel(文章)
LogModel::query($page, $perPage, $cond, &$total); // 分页查询
LogModel::count(); // 总文章数
LogModel::getOne($gid);
LogModel::getByAlias($alias);
LogModel::latest($limit = 10); // 最新 N 篇
LogModel::prev($gid);
LogModel::next($gid);
SortModel(分类)
SortModel::all(); // 所有分类
SortModel::getOne($sid);
SortModel::getByAlias($alias);
SortModel::countInSort($sid); // 分类下的文章数
TagModel(标签)
TagModel::all();
TagModel::getByName($name);
TagModel::getByGid($gid);
CommentModel(评论)
CommentModel::byLog($gid);
CommentModel::countByLog($gid);
CommentModel::latest($limit = 10);
UserModel(用户)
UserModel::getOne($uid);
UserModel::getByUsername($username);
LinkModel(友链)
LinkModel::all();
6.5 配置:Option
Option::get('site_title', '默认值'); // 读取(带默认)
Option::set('key', 'value'); // 写入
Option::all(); // 所有 autoload=y 的配置
6.6 视图:View
// 解析主题文件路径(自动回退到 default 主题)
$path = View::resolve('header');
// 渲染模板并传入数据
View::display('page', ['title' => '关于', 'content' => '...']);
6.7 数据库
global $DB;
$rows = $DB->fetchAll("SELECT * FROM " . DB_PREFIX . "blog WHERE hide='n' LIMIT 10");
$row = $DB->fetchOne("SELECT * FROM " . DB_PREFIX . "blog WHERE gid=?", [$gid]);
$cnt = $DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "comment");
七、可用钩子速查
完整钩子清单见《LiteLog 插件开发规范》附录,此处只列主题文件里必须调用的钩子。
7.1 header.php 中
<?php Hook::doAction('header_meta'); ?> <!-- <meta> 之后 -->
<?php Hook::doAction('template_head'); ?> <!-- CSS 之后 -->
<?php Hook::doAction('header_after'); ?> <!-- </header> 之后 -->
<?php Hook::doAction('before_content'); ?> <!-- 主内容区之前 -->
7.2 footer.php 中
<?php Hook::doAction('after_content'); ?> <!-- 主内容区之后 -->
<?php Hook::doAction('footer_before'); ?> <!-- <footer> 之前 -->
<?php Hook::doAction('template_footer'); ?> <!-- <footer> 内部 -->
7.3 log_list.php 中
<?php Hook::doAction('list_page_before', $pageType); ?>
<?php Hook::doAction('list_item_before', $log, $pageType); ?>
<?php Hook::doAction('list_item_after', $log, $pageType); ?>
<?php Hook::doAction('list_page_after', $pageType); ?>
7.4 echo_log.php 中
<?php Hook::doAction('log_view_before', $log); ?>
<?php Hook::doAction('log_content_before', $log); ?>
<?php Hook::doAction('log_content_after', $log); ?>
<?php Hook::doAction('log_view_after', $log); ?>
<!-- 评论循环内 -->
<?php Hook::doAction('comment_display_before', $comment); ?>
<?php Hook::doAction('comment_display_after', $comment); ?>
<!-- 评论表单 -->
<?php Hook::doAction('comment_form_before', $log); ?>
<?php Hook::doAction('comment_form_fields', $log); ?>
<?php Hook::doAction('comment_form_after', $log); ?>
7.5 side.php 中
<?php Hook::doAction('sidebar_before'); ?>
<?php Hook::doAction('sidebar_after'); ?>
7.6 自定义钩子(主题自己提供)
主题可以提供额外钩子给插件用:
<?php Hook::doAction('mytheme_featured_area'); ?>
约定:自定义钩子名建议 主题名_xxx 前缀,避免与内核冲突。
八、模板路由说明
8.1 哪些 URL 走哪个模板
| URL 类型 | 触发控制器 | 使用模板 |
|---|---|---|
/(首页) | LogController::display | log_list.php |
/post/123.html | LogController::displayContent | echo_log.php |
/sort/tech | SortController::display | log_list.php |
/tag/php | TagController::display | log_list.php |
/author/1 | AuthorController::display | log_list.php |
/record/2026-10 | RecordController::display | log_list.php |
| 不存在 | — | 404.php |
判断当前页类型:在 log_list.php 里读 $pageType:
<?php if (($pageType ?? '') === 'index'): ?>
<!-- 首页专属内容 -->
<?php endif; ?>
8.2 自定义页面
用 View::display('page', [...]) 主动调用:
View::display('page', [
'title' => '关于我',
'content' => '<p>这是关于页面</p>',
]);
8.3 主题切换时的回退
如果当前主题缺某个文件(如 page.php),View::resolve() 自动回退到 default 主题的同名文件。所以你的主题不需要实现所有文件。
九、静态资源引用
9.1 CSS / JS
方法 1:用 TEMPLATE_URL(推荐)
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<script src="<?= TEMPLATE_URL ?>js/theme.js"></script>
方法 2:用相对路径
<link rel="stylesheet" href="content/templates/mytheme/css/style.css">
禁止:直接用绝对路径(如 /content/templates/xxx/),会导致装在子目录时失效。
9.2 图片
<img src="<?= TEMPLATE_URL ?>images/logo.png" alt="Logo">
9.3 用户上传
<img src="<?= BLOG_URL ?>content/uploads/2026/10/xxx.jpg">
或直接输出文章内容里的 <img>($log['content'] 已包含完整 URL)。
十、完整示例
最小但完整的主题:单栏布局 + 深色模式 + 阅读进度条。
content/templates/demo/header.php
<?php
/**
* Theme Name: Demo
* Description: 最小可用的 LiteLog 主题示例
* Version: 1.0.0
* Author: 教程
*/
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<?php Hook::doAction('template_head'); ?>
</head>
<body>
<div class="wrap">
<header class="head">
<a class="brand" href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
<nav class="nav">
<a href="<?= BLOG_URL ?>">首页</a>
<?php foreach (SortModel::all() as $s): ?>
<a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a>
<?php endforeach; ?>
</nav>
</header>
<?php Hook::doAction('header_after'); ?>
<?php Hook::doAction('before_content'); ?>
<main class="main">
content/templates/demo/footer.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="foot">
© <?= date('Y') ?> <?= h(Option::get('site_title')) ?>
</footer>
</div>
<?php Hook::doAction('template_footer'); ?>
</body>
</html>
content/templates/demo/log_list.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
if (!empty($sort)) $page_title = $sort['sortname'] . ' - ' . $page_title;
if (!empty($tag)) $page_title = '标签:' . $tag['tagname'] . ' - ' . $page_title;
if (!empty($author)) $page_title = ($author['nickname'] ?: $author['username']) . ' 的文章';
include View::resolve('header');
?>
<div class="layout">
<div class="col-main">
<?php if (empty($logs)): ?>
<p>暂无文章</p>
<?php else: foreach ($logs as $log): ?>
<article class="item">
<h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
<div class="meta"><?= h($log['date_fmt']) ?> · <?= (int)$log['views'] ?> 浏览</div>
<p class="excerpt"><?= h($log['excerpt']) ?></p>
<a href="<?= h($log['url']) ?>">阅读全文 →</a>
</article>
<?php endforeach; endif; ?>
<?= $pageHtml ?? '' ?>
</div>
<aside class="col-side">
<?php include View::resolve('side'); ?>
</aside>
</div>
<?php include View::resolve('footer'); ?>
content/templates/demo/echo_log.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<div id="progress" style="position:fixed;top:0;left:0;height:3px;width:0;background:#2563eb;z-index:99"></div>
<article class="single">
<h1><?= h($log['title']) ?></h1>
<div class="meta">
<?= h($log['date_fmt']) ?> · <?= h($log['author_name']) ?> · <?= (int)$log['views'] ?> 浏览
</div>
<div class="content"><?= $log['content'] ?></div>
</article>
<nav class="pnav">
<?php if ($prev): ?><a href="<?= h($prev['url']) ?>">← <?= h($prev['title']) ?></a><?php endif; ?>
<?php if ($next): ?><a href="<?= h($next['url']) ?>"><?= h($next['title']) ?> →</a><?php endif; ?>
</nav>
<section class="comments">
<h3>评论 (<?= CommentModel::countByLog($log['gid']) ?>)</h3>
<?php $render = function ($list, $d = 0) use (&$render) {
foreach ($list as $c) { ?>
<div class="comment" style="margin-left:<?= $d * 24 ?>px">
<b><?= h($c['poster']) ?></b> <small><?= datetimeFmt($c['date']) ?></small>
<div><?= nl2br(h($c['comment'])) ?></div>
</div>
<?php if (!empty($c['children'])) $render($c['children'], $d + 1);
}
}; $render($comments); ?>
<?php if ($log['allow_remark'] !== 'n'): ?>
<form method="post" action="<?= BLOG_URL ?>?action=comment_post">
<input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
<input type="text" name="poster" placeholder="昵称 *" required>
<input type="email" name="email" placeholder="邮箱">
<textarea name="comment" rows="5" placeholder="说点什么..." required></textarea>
<button type="submit">发表评论</button>
</form>
<?php endif; ?>
</section>
<script>
(function () {
var bar = document.getElementById('progress');
if (!bar) return;
var content = document.querySelector('.content');
if (!content) return;
function update() {
var start = content.offsetTop;
var end = start + content.offsetHeight - window.innerHeight;
var pct = end <= start ? 100 : Math.min(100, Math.max(0, (window.scrollY - start) / (end - start) * 100));
bar.style.width = pct + '%';
}
window.addEventListener('scroll', update, { passive: true });
update();
})();
</script>
<?php include View::resolve('footer'); ?>
content/templates/demo/side.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<div class="widget">
<h4>最新文章</h4>
<ul>
<?php foreach (LogModel::latest(5) as $l): ?>
<li><a href="<?= Url::log($l['gid'], $l['alias']) ?>"><?= h($l['title']) ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<div class="widget">
<h4>分类</h4>
<ul>
<?php foreach (SortModel::all() as $s): ?>
<li><a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a></li>
<?php endforeach; ?>
</ul>
</div>
content/templates/demo/css/style.css
* { box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", sans-serif; margin: 0; color: #333; background: #f6f7f9; line-height: 1.7; }
a { color: #2563eb; text-decoration: none; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; }
.head { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid #eee; }
.brand { font-size: 20px; font-weight: 700; color: #111; }
.nav a { margin-left: 16px; color: #555; font-size: 14px; }
.main { padding: 24px 0; }
.layout { display: flex; gap: 24px; }
.col-main { flex: 1; min-width: 0; }
.col-side { width: 260px; flex-shrink: 0; }
.item, .single, .widget, .comments { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.item h2 { margin: 0 0 8px; font-size: 18px; }
.meta { color: #888; font-size: 13px; margin-bottom: 8px; }
.excerpt { color: #555; margin: 0 0 10px; }
.single h1 { margin: 0 0 10px; font-size: 24px; }
.content { font-size: 16px; }
.content img { max-width: 100%; height: auto; }
.content pre { background: #1e293b; color: #e2e8f0; padding: 14px 18px; border-radius: 8px; overflow-x: auto; }
.content code:not(pre code) { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; color: #db2777; font-size: 0.9em; }
.pnav { display: flex; justify-content: space-between; margin-bottom: 16px; font-size: 14px; }
.comments input, .comments textarea { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 8px; font-family: inherit; }
.comments button { background: #2563eb; color: #fff; border: 0; padding: 9px 20px; border-radius: 6px; cursor: pointer; }
.widget h4 { margin: 0 0 10px; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 6px; }
.widget ul { list-style: none; padding: 0; margin: 0; font-size: 14px; }
.widget li { margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.foot { text-align: center; color: #999; font-size: 13px; padding: 24px 0; }
@media (max-width: 768px) { .layout { flex-direction: column; } .col-side { width: auto; } }
启用:后台 → 主题 → "Demo" → 启用此主题。
十一、开发规范
11.1 命名规范
| 类型 | 规则 | 示例 |
|---|---|---|
| 主题目录 | 小写字母 + 短横线 | nebula、my-theme |
| CSS class | 小写 + 短横线 | .post-card-title |
| JS 变量 | 小驼峰 | readProgress |
| PHP 函数 | 大驼峰 + 下划线前缀 | Demo_getLatest() |
11.2 安全规范
- ✅ 所有用户可控输出加
h():echo h($log['title']) - ✅
$log['content']例外,它已经是渲染后的安全 HTML - ✅ 打开
header.php时立刻if (!defined('LITELOG_ROOT')) exit('error!'); - ✅ 评论内容用
nl2br(h($c['comment']))(先转义后换行) - ❌ 不要直接
echo $log['excerpt'](未转义) - ❌ 不要在模板里做数据库写操作
11.3 性能规范
- 首页循环里不要为每篇文章查一次标签/分类,模型已批量提供
- 侧边栏的
LogModel::latest()加limit - 图片用
loading="lazy" - CSS / JS 放
</body>前,避免阻塞
11.4 兼容规范
- 不修改内核文件(
include/、admin/) - 不覆盖 default 主题的文件
- 主内容区必须包在
<main>里(before_content/after_content钩子位置依赖) - 主题文件可以缺
page.php/404.php/side.php,内核会自动回退到 default
11.5 代码块样式(推荐)
复制 content/templates/default/css/style.css 里的 .content pre 段落(保证代码块深色样式一致)。并在 footer.php 底部引入前台代码块复制脚本:
<script src="<?= TEMPLATE_URL ?>js/codeblock.js"></script>
codeblock.js 的内容可从 default 主题拷贝。
十二、调试技巧
12.1 打开错误显示
编辑 init.php,临时改:
error_reporting(E_ALL);
ini_set('display_errors', 1);
调试完务必改回:
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_WARNING);
ini_set('display_errors', 0);
12.2 快速查看变量
在模板任意位置插入:
<pre><?php var_dump($log); exit; ?></pre>
12.3 检查当前页类型
在 header.php 或 log_list.php 加:
<!-- pageType: <?= $pageType ?? 'n/a' ?> -->
<!-- sort: <?= !empty($sort) ? 'yes' : 'no' ?> -->
12.4 检查钩子是否被调用
在 footer.php 里加:
<?php Hook::doAction('template_footer'); ?>
<!-- template_footer 已触发 -->
12.5 F12 排查
- Console:JS 报错、资源 404
- Network:CSS / JS 是否加载成功
- Elements:DOM 结构是否符合预期
十三、常见问题
Q1:启用主题后前台白屏
- 打开错误显示(见 12.1),看报错
- 检查
header.php顶部是否漏了if (!defined('LITELOG_ROOT')) exit('error!'); - 检查
$page_title是否在使用前赋值(未定义会 notice,但不会白屏) - 检查是否引用了内核不存在的方法(如
LogModel::foo())
Q2:文章列表为空
$logs可能真的是空数组(站点没有文章)- 用
var_dump($logs)打印看看 - 检查
hide='n'的文章是否存在
Q3:分页不显示
$pageHtml为空说明总文章数 ≤ 每页数(Option::get('index_lognum'))- 打印
count($logs)和Option::get('index_lognum')对比
Q4:代码块没样式 / 复制按钮没出来
- 检查是否引入
codeblock.js - 检查 CSS 里是否有
.content pre { ... }段落 - 打开 F12 → Network,看 JS 是否 404
Q5:评论提交后 404
- 评论表单的
action必须是<?= BLOG_URL ?>?action=comment_post method必须是post- 必须有
<input type="hidden" name="gid" value="...">
Q6:主题切换后还是旧样式
- 强制刷新
Ctrl + Shift + R - 或 F12 → Network → Disable cache
- 或清空浏览器缓存
Q7:想给主题加后台设置项
在主题目录新建 options.php:
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<div class="card">
<div class="card-title">主题设置</div>
<form method="post" class="form">
<label>主题主色</label>
<input type="text" name="theme_color" value="<?= h(Option::get('theme_color', '#2563eb')) ?>">
<button class="btn" type="submit">保存</button>
</form>
</div>
在 admin/index.php 的 theme_activate 或新增路由里处理保存,或者用 钩子 option_save 监听。具体实现参考插件开发规范。
Q8:怎么让主题支持插件?
主题只要在合适的 HTML 位置主动调用钩子即可,插件会自动生效。见 第七节。
Q9:怎么让主题有自己的专属钩子?
在主题任意位置加:
<?php Hook::doAction('mytheme_header_banner'); ?>
插件监听:
Hook::addAction('mytheme_header_banner', 'MyPlugin_renderBanner');
约定用 主题名_xxx 前缀,避免与内核冲突。
Q10:主题想要深色/浅色切换
在 header.php 加一个按钮:
<button id="theme-toggle">切换模式</button>
在主题 JS 里:
(function () {
var KEY = 'theme_mode';
var saved = localStorage.getItem(KEY);
if (saved === 'light') document.body.classList.add('light');
var btn = document.getElementById('theme-toggle');
if (btn) btn.addEventListener('click', function () {
document.body.classList.toggle('light');
localStorage.setItem(KEY, document.body.classList.contains('light') ? 'light' : 'dark');
});
})();
CSS 里定义:
body { background: #0a0e1a; color: #e6ecf5; }
body.light { background: #f6f7f9; color: #333; }
结语
开发一个 LiteLog 主题的步骤:
content/templates/下建目录(英文小写)- 创建
header.php(顶部写Theme Name注释头) - 创建
footer.php、log_list.php、echo_log.php - 按需补
side.php、page.php、404.php - 放
css/style.css和js/theme.js - 后台 → 主题 → 启用
- 前台刷新查看
记住三个原则:
- 所有用户可控输出加
h()—— 安全 - 主内容区包
<main>—— 钩子位置依赖 - 静态资源用
TEMPLATE_URL—— 子目录兼容
调试三件套:
- 临时开
display_errors var_dump()+exit- F12 看 Console / Network
这就是 LiteLog 主题开发的全部。 有问题优先查本文档,其次查《LiteLog 插件开发规范》(那里有完整的 47 个钩子清单)。
评论 (0)
发表评论