版本:1.0(2026-10-13) 定位:既是规范(必须遵守的硬性规则),也是经验(实战教训),更是手册(可索引、可执行)。 来源:LiteLog 内核 v1.2 + default 主题 v1.3.1 + 每日一文/论坛插件两轮修复战暴露出的主题层全部问题。 给 AI 下指令时,请把本文件完整附上。
阅读指引
本手册分 8 个部分,按"从零到一"和"从一到生产"两条路径组织:
| 你的角色 | 推荐阅读顺序 |
|---|---|
| 第一次做主题 | 第 1 部分 → 第 2 部分 → 第 3 部分 → 第 7 部分 |
| 改造现有主题 | 第 2 部分 → 第 4 部分 → 第 5 部分 |
| 排错 | 第 4 部分 → 第 6 部分 → 第 5 部分(按症状查阅) |
| 给 AI 下指令 | 第 8 部分附录 E |
⚠️ 关键条款标记:
- 🔴 红线:违反一票否决
- ⭐ 重点:最容易踩坑的地方
- 📌 口诀:记忆口诀
第 1 部分:入门
第 1 章 · 快速上手(15 分钟做出第一个主题)
1.1 目标
创建一个名为 mytheme 的主题,风格极简:白底黑字、单栏布局。
1.2 最小目录结构
content/templates/mytheme/
├── header.php ← 【必须】HTML head + 页头
├── footer.php ← 【必须】页脚 + 脚本注入
├── log_list.php ← 【必须】文章列表
├── echo_log.php ← 【必须】文章详情
├── css/
│ └── style.css ← 【必须】主样式
└── side.php ← 【可选】右侧栏
1.3 header.php
<?php
/**
* Theme Name: MyTheme
* Description: 极简单栏主题
* Version: 1.0.0
* Author: 你的名字
*/
if (!defined('LITELOG_ROOT')) exit('error!');
/* ★ 三变量兜底:主题必须同时识别三种标题变量 */
$__final_title = '';
if (isset($page_title) && $page_title !== '') {
$__final_title = $page_title;
} elseif (isset($title) && $title !== '') {
$__final_title = $title;
} elseif (isset($__pageTitle) && $__pageTitle !== '') {
$__final_title = $__pageTitle;
} else {
$__final_title = Option::get('site_title', '');
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($__final_title) ?></title>
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<?php Hook::doAction('layout_head_end'); ?>
<?php Hook::doAction('template_head'); ?>
</head>
<body>
<?php Hook::doAction('layout_body_start'); ?>
<header class="site-header">
<a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
<nav>
<?php foreach (SortModel::all() as $s): ?>
<a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a>
<?php endforeach; ?>
</nav>
</header>
<main class="container">
<?php Hook::doAction('layout_content_start'); ?>
1.4 footer.php
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php Hook::doAction('layout_content_end'); ?>
</main>
<footer class="site-footer">
<p>© <?= date('Y') ?> <?= h(Option::get('site_title')) ?> · Powered by LiteLog</p>
</footer>
<?php Hook::doAction('layout_footer_scripts'); ?>
<?php Hook::doAction('body_end'); ?>
<?php Hook::doAction('layout_body_end'); ?>
</body>
</html>
1.5 log_list.php
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
$page_title = Option::get('site_title', '');
$title = $page_title;
$__pageTitle = $page_title;
include View::resolve('header');
?>
<h1 class="page-title">最新文章</h1>
<?php foreach ($logs as $log): ?>
<article class="post">
<h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
<div class="meta"><?= h($log['date_fmt']) ?></div>
<p><?= h($log['excerpt']) ?></p>
</article>
<?php endforeach; ?>
<?= $pageHtml ?>
<?php include View::resolve('footer'); ?>
1.6 echo_log.php
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
$page_title = $log['title'] . ' - ' . Option::get('site_title', '');
$title = $page_title;
$__pageTitle = $log['title'] . ' · ' . Option::get('site_title', '');
include View::resolve('header');
?>
<article class="post-single">
<h1><?= h($log['title']) ?></h1>
<div class="meta"><?= h($log['date_fmt']) ?> · <?= h($log['author_name']) ?></div>
<div class="content"><?= $log['content'] ?></div>
</article>
<?php include View::resolve('footer'); ?>
1.7 css/style.css
* { box-sizing: border-box; }
body {
max-width: 720px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.8;
color: #1e293b;
background: #fff;
}
.site-header { padding: 20px 0; border-bottom: 1px solid #eee; }
.site-header a { color: #0f172a; text-decoration: none; font-weight: 700; font-size: 20px; }
.site-header nav a { margin-left: 16px; font-size: 14px; color: #64748b; font-weight: 400; }
.post { padding: 24px 0; border-bottom: 1px solid #f1f5f9; }
.post h2 { margin: 0 0 8px; }
.post h2 a { color: #2563eb; text-decoration: none; }
.meta { color: #94a3b8; font-size: 13px; }
.site-footer { text-align: center; padding: 40px 0; color: #94a3b8; font-size: 13px; }
1.8 启用与访问
- 把
mytheme/上传到content/templates/ - 后台 → 主题管理 → 启用
MyTheme - 访问首页
第 2 章 · 内核架构总览
2.1 主题在请求链中的位置
请求 → index.php
↓
init.php
├─ 定义常量(BLOG_URL / CONTENT_PATH ...)
├─ 加载 lib / model / controller
├─ ★ 移动端 router(isMobileDevice() 时加载)
├─ loadActivePlugins()(注册插件钩子)
├─ ★ 定义 TEMPLATE_URL / TEMPLATE_MOBILE_URL / TEMPLATE_CSS_VER
└─ Dispatcher::dispatch()
↓
├─ 路由匹配
├─ 控制器执行
↓
View::display('log_list', [...])
├─ View::resolve('log_list') ← 解析到主题文件
├─ Hook::doAction('view_before', 'log_list', $data)
├─ extract($data)
├─ include $file ← 你的主题文件开始执行
│ ├─ include View::resolve('header') ← 主题 header.php
│ │ ├─ Hook::doAction('layout_head_end') ← 插件注入 CSS
│ │ └─ Hook::doAction('layout_body_start')
│ ├─ 主题页面内容
│ └─ include View::resolve('footer')
│ ├─ Hook::doAction('layout_footer_scripts') ← 插件注入 JS
│ └─ Hook::doAction('layout_body_end')
└─ Hook::doAction('view_after', 'log_list', $data)
2.2 主题必须做的三件事
| # | 必须做的事 | 为什么 |
|---|---|---|
| 1 | header.php 调用 layout_head_end + layout_body_start | 让插件注入 CSS、顶部横幅 |
| 2 | footer.php 调用 layout_footer_scripts + layout_body_end | 让插件注入 JS、底部元素 |
| 3 | header.php 识别三种标题变量 | 兼容 PC / 移动端 / 插件 |
第 3 章 · 主题目录与文件
3.1 标准目录结构
content/templates/{主题名}/
├── header.php ← 【必须】HTML head + 页头
├── footer.php ← 【必须】页脚 + 脚本注入
├── log_list.php ← 【必须】文章列表(首页/分类/标签/作者/归档)
├── echo_log.php ← 【必须】文章详情
├── side.php ← 【推荐】右侧栏
├── author.php ← 【可选】作者主页
├── search.php ← 【可选】搜索结果
├── page.php ← 【可选】单页
├── 404.php ← 【可选】404 页
├── css/
│ └── style.css ← 【必须】主样式
├── js/
│ └── codeblock.js ← 【可选】代码块复制
├── preview.jpg ← 【可选】主题预览图
└── mobile/ ← 【可选】移动端子主题
├── header.php
├── footer.php
├── log_list.php
├── echo_log.php
├── search.php
├── 404.php
├── style.css
├── router.php ← 移动端路由接管器
├── routes.php ← 路由规则
└── views/
├── _lib.php
└── ...
3.2 ⭐ 文件命名与自动加载
View::resolve($name) 的查找顺序:
1. content/templates/{当前主题}/mobile/{$name}.php ← 移动端优先
2. content/templates/{当前主题}/{$name}.php
3. content/templates/default/{$name}.php ← 最后兜底
关键含义:
- 如果主题的
mobile/目录存在,且当前用户被判定为移动端,优先使用移动端版本 - 主题不需要为每个页面都提供移动端版本,缺失自动回退到 PC 版
- 主题不需要提供所有页面,缺失自动回退到
default主题
3.3 头注释规范
header.php 或 log_list.php 顶部的注释块会被 scanThemes() 解析:
<?php
/**
* Theme Name: 我的主题
* Description: 一句话描述主题特色
* Version: 1.0.0
* Author: 你的名字
* Author URI: https://example.com
*/
if (!defined('LITELOG_ROOT')) exit('error!');
支持字段:Theme Name / Name / Description / Version / Author / Author URI
主题预览图(可选):命名为 preview.jpg / preview.png / screenshot.jpg / screenshot.png 放在主题根目录。
3.4 ⭐ 主题常量
| 常量 | 值 | 说明 |
|---|---|---|
TEMPLATE_URL | http://站点/content/templates/{主题}/ | 移动端时指向 mobile/ |
TEMPLATE_ROOT_URL | http://站点/content/templates/{主题}/ | 永远指向主题根 |
TEMPLATE_MOBILE_URL | http://站点/content/templates/{主题}/mobile/ | 移动端子目录 |
TEMPLATE_CSS_VER | ?v=文件时间戳 | 用于缓存控制 |
BLOG_URL | http://站点/ | 站点根 URL,带斜杠 |
CONTENT_PATH | /绝对路径/content/ | 带斜杠 |
ADMIN_PATH | /绝对路径/admin/ | 带斜杠 |
DB_PREFIX | lt_ | 无斜杠 |
模板 URL 拼接规则:
<!-- ❌ 反例:多一个斜杠 -->
<link rel="stylesheet" href="<?= BLOG_URL ?>/content/...">
<!-- ✅ 正例 -->
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css<?= TEMPLATE_CSS_VER ?>">
第 2 部分:基础规范
第 4 章 · 主题 API
4.1 视图 API
View::resolve($name) // 返回解析后的文件绝对路径(string|false)
View::display($name, $data = []) // 渲染(内核调用,主题一般不直接调用)
主题视图内的常见模式:
include View::resolve('header'); // 加载 header.php
include View::resolve('footer'); // 加载 footer.php
include View::resolve('side'); // 加载侧边栏
4.2 选项 API
Option::get($key, '') // string(★ 第二参数永远 '')
Option::set($key, $value)
Option::all()
主题常用选项:
site_title/site_description/site_keyindex_lognum(首页文章数)current_template(当前主题名)url_rewrite(伪静态开关)comment_need_check(评论审核)
4.3 钩子 API
主题必须触发的钩子:
| 钩子 | 触发点 | 用途 |
|---|---|---|
layout_head_start | <head> 最前 | 早期 meta |
layout_head_end | </head> 前 | 插件注入 CSS |
layout_body_start | <body> 之后 | 顶部横幅 / 公告条 |
layout_content_start | <main> 之后 | 面包屑 / 页面标题栏 |
layout_content_end | 内容后、</main> 前 | 相关文章 |
layout_footer_scripts | </body> 前的脚本区 | 插件注入 JS |
layout_body_end | </body> 之前 | 底部 DIY / 悬浮元素 |
兼容旧版的传统钩子:
| 钩子 | 触发点 |
|---|---|
template_head | </head> 前(老插件用) |
body_start | <body> 之后 |
header_before / header_after | 页头前后 |
before_content / after_content | 内容前后 |
footer_before / footer_after | 页脚前后 |
template_footer | 页脚内 |
body_end | </body> 之前 |
4.4 布局主内容区覆盖
核心会触发 layout_main_content 钩子,插件可完全接管主内容区。主题需要在每个页面(log_list.php / echo_log.php / page.php / author.php / search.php / 404.php)处理这个覆盖:
<?php
$__main_override = '';
if (class_exists('Hook')) {
try {
$__main_override = trim(Hook::captureAction('layout_main_content', $ctx));
} catch (Throwable $e) { $__main_override = ''; }
}
$__hasMainOverride = ($__main_override !== '');
?>
<?php if ($__hasMainOverride): ?>
<div class="layout layout-single">
<div class="main-col" data-region="main">
<?= $__main_override ?>
</div>
</div>
<?php else: ?>
<!-- 正常三栏布局 -->
<div class="layout">
<!-- ... -->
</div>
<?php endif; ?>
$ctx 是传给钩子的上下文,不同页面传入内容不同:
log_list.php:['logs' => ..., 'pageHtml' => ..., 'sort' => ...]echo_log.php:['log' => $log]author.php:['author' => $author]page.php:['page' => $page]
4.5 小工具 API
WidgetArea::hasVisibleWidgets(WidgetArea::AREA_LEFT) // bool
WidgetArea::hasVisibleWidgets(WidgetArea::AREA_RIGHT) // bool
WidgetArea::render(WidgetArea::AREA_LEFT) // void(直接输出)
WidgetArea::render(WidgetArea::AREA_RIGHT)
典型用法:
<?php
$hasWidgets = WidgetArea::hasVisibleWidgets(WidgetArea::AREA_LEFT);
$hasLegacy = false; $legacyHtml = '';
if (!$hasWidgets) {
$legacyHtml = Hook::captureAction('left_sidebar_before')
. Hook::captureAction('left_sidebar')
. Hook::captureAction('left_sidebar_after');
$hasLegacy = trim($legacyHtml) !== '';
}
?>
<?php if ($hasWidgets || $hasLegacy): ?>
<aside class="left-col" data-region="left">
<?php if ($hasWidgets) WidgetArea::render(WidgetArea::AREA_LEFT); else echo $legacyHtml; ?>
</aside>
<?php endif; ?>
4.6 模型 API
| 模型 | 主要方法 |
|---|---|
LogModel | query() / getOne() / getByAlias() / latest() / prev() / next() / addView() / count() / search() |
SortModel | all() / getOne() / getByAlias() / countInSort() |
TagModel | all() / getByName() / getByGid() / getGidsByTag() |
CommentModel | getOne() / byLog() / byLogPaginated() / buildTree() / latest() / countByLog() |
UserModel | getOne() / getByUsername() |
4.7 URL API
Url::log($gid, $alias) // 文章 URL
Url::sort($sid, $alias) // 分类 URL
Url::tag($name) // 标签 URL
Url::author($uid) // 作者 URL
Url::record('2026-10') // 归档 URL
禁止硬编码 '/post/1.html'。
4.8 页面数据变量
log_list.php 可用变量(由控制器传入):
$logs // array:文章列表
$pageHtml // string:分页 HTML
$pageType // string:'index' / 'sort' / 'tag' / 'author' / 'record' / 'search'
$sort // array|null:分类对象
$tag // array|null:标签对象
$author // array|null:作者对象
$recordYear // int|null:归档年
$recordMonth // int|null:归档月
echo_log.php 可用变量:
$log // array:文章对象
$prev / $next // array|null:上下篇
$comments // array:评论树
$commentTotal // int:评论总数
$commentPage // int:当前评论页码
$commentPerPage // int:每页评论数
$commentPageHtml // string:评论分页 HTML
$currentUser // array|null:当前登录用户
author.php 可用变量:
$author // array:作者对象
$nickname // string:作者昵称
$profile // array:用户资料
$groups // array:用户组
$stats // array:统计数据
$isSelf // bool:是否是自己
$isLoggedIn // bool
$isFollowing // bool
$blogPosts // array
$forumThreads // array
4.9 辅助函数
| 函数 | 说明 |
|---|---|
h($s) | HTML 转义 |
redirect($url) | 跳转 + exit |
isLoggedIn() | 是否登录 |
checkLogin() | 强制登录 |
dateFmt($ts) / datetimeFmt($ts) | 日期格式化 |
getExcerpt($html, $len) | 摘要 |
makeAlias($str) | URL 别名 |
pagination($page, $total, $perPage) | 分页 HTML |
getPageNumber() | 当前页码 |
isMobileDevice() | 是否移动端 |
getCurrentTemplateName() | 当前主题名 |
hasMobileTemplateDir() | 是否有移动端子目录 |
第 5 章 · 模板文件详解
5.1 header.php 标准骨架
<?php
/**
* Theme Name: MyTheme
* Description: ...
* Version: 1.0.0
* Author: ...
*/
if (!defined('LITELOG_ROOT')) exit('error!');
/* ★ 三变量兜底 */
$__final_title = '';
if (isset($page_title) && $page_title !== '') {
$__final_title = $page_title;
} elseif (isset($title) && $title !== '') {
$__final_title = $title;
} elseif (isset($__pageTitle) && $__pageTitle !== '') {
$__final_title = $__pageTitle;
} else {
$__final_title = Option::get('site_title', '');
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<?php Hook::doAction('layout_head_start'); ?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($__final_title) ?></title>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<link rel="icon" type="image/x-icon" href="<?= BLOG_URL ?>favicon.ico">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css<?= TEMPLATE_CSS_VER ?>">
<?php Hook::doAction('template_head'); ?>
<?php Hook::doAction('layout_head_end'); ?>
</head>
<body>
<?php Hook::doAction('body_start'); ?>
<?php Hook::doAction('layout_body_start'); ?>
<header class="site-header">
<?php Hook::doAction('header_before'); ?>
<div class="container">
<?php Hook::doAction('header_inner_before'); ?>
<h1 class="logo">
<a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
</h1>
<p class="desc"><?= h(Option::get('site_description')) ?></p>
<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; ?>
<?php Hook::doAction('header_nav'); ?>
<a href="<?= BLOG_URL ?>admin/">管理</a>
</nav>
<?php Hook::doAction('header_inner_after'); ?>
</div>
<?php Hook::doAction('header_after'); ?>
</header>
<?php Hook::doAction('before_content'); ?>
<main class="container">
<?php Hook::doAction('layout_content_start'); ?>
5.2 footer.php 标准骨架
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php Hook::doAction('layout_content_end'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="site-footer">
<div class="container">
<?php Hook::doAction('footer_content'); ?>
<p>© <?= date('Y') ?> <?= h(Option::get('site_title')) ?> · Powered by LiteLog</p>
</div>
</footer>
<?php Hook::doAction('footer_after'); ?>
<?php Hook::doAction('template_footer'); ?>
<?php Hook::doAction('layout_footer_scripts'); ?>
<?php Hook::doAction('body_end'); ?>
<?php Hook::doAction('layout_body_end'); ?>
</body>
</html>
5.3 log_list.php 标准骨架
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
/* 1. 标题 */
$headTitle = Option::get('site_title', '');
if (!empty($sort)) $headTitle = $sort['sortname'] . ' - ' . $headTitle;
if (!empty($tag)) $headTitle = '标签:' . $tag['tagname'] . ' - ' . $headTitle;
if (!empty($author)) $headTitle = ($author['nickname'] ?: $author['username']) . ' 的文章 - ' . $headTitle;
if (!empty($recordYear)) $headTitle = "{$recordYear}年{$recordMonth}月 - " . $headTitle;
$page_title = $headTitle;
$title = $headTitle;
$__pageTitle = $headTitle;
/* 2. 上下文 */
$ctx = [
'logs' => $logs ?? [],
'pageHtml' => $pageHtml ?? '',
'sort' => $sort ?? null,
'tag' => $tag ?? null,
'author' => $author ?? null,
'recordYear' => $recordYear ?? null,
'recordMonth' => $recordMonth ?? null,
];
$isHomepage = empty($sort) && empty($tag) && empty($author) && empty($recordYear);
/* 3. 首页模式 */
$currentMode = null;
if ($isHomepage && class_exists('HomepageModeRegistry')) {
$currentMode = HomepageModeRegistry::getCurrent();
}
if (!$currentMode && $isHomepage && class_exists('HomepageMode_Blog')) {
$currentMode = new HomepageMode_Blog();
}
/* 4. 主内容区覆盖检查 */
$__main_override = '';
if (class_exists('Hook')) {
try {
$__main_override = trim(Hook::captureAction('layout_main_content', $ctx));
} catch (Throwable $e) { $__main_override = ''; }
}
$__hasMainOverride = ($__main_override !== '');
/* 5. 输出 */
include View::resolve('header');
?>
<?php if ($__hasMainOverride): ?>
<div class="layout layout-single">
<div class="main-col" data-region="main">
<?= $__main_override ?>
</div>
</div>
<?php else: ?>
<div class="layout">
<?php
$hasWidgets = WidgetArea::hasVisibleWidgets(WidgetArea::AREA_LEFT);
$hasLegacy = false; $legacyHtml = '';
if (!$hasWidgets) {
$legacyHtml = Hook::captureAction('left_sidebar_before')
. Hook::captureAction('left_sidebar')
. Hook::captureAction('left_sidebar_after');
$hasLegacy = trim($legacyHtml) !== '';
}
?>
<?php if ($hasWidgets || $hasLegacy): ?>
<aside class="left-col" data-region="left">
<?php if ($hasWidgets) WidgetArea::render(WidgetArea::AREA_LEFT); else echo $legacyHtml; ?>
</aside>
<?php endif; ?>
<div class="main-col" data-region="main">
<?php
if ($currentMode) {
$currentMode->render($ctx);
} else {
echo '<p class="empty">没有可用的首页模式</p>';
}
?>
</div>
<aside class="side-col" data-region="right">
<?php Hook::doAction('side_col_before'); ?>
<?php include View::resolve('side'); ?>
<?php Hook::doAction('side_col_after'); ?>
</aside>
</div>
<?php endif; ?>
<?php include View::resolve('footer'); ?>
5.4 echo_log.php 标准骨架
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
$page_title = $log['title'] . ' - ' . Option::get('site_title', '');
$title = $page_title;
$__pageTitle = $log['title'] . ' · ' . Option::get('site_title', '');
include View::resolve('header');
?>
<article class="post-single">
<h1><?= h($log['title']) ?></h1>
<div class="meta">
<span><?= h($log['date_fmt']) ?></span>
<span>作者:<?= h($log['author_name']) ?></span>
</div>
<div class="content"><?= $log['content'] ?></div>
</article>
<!-- 上下篇 -->
<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 class="comment-section">
<!-- 评论列表 -->
<!-- 评论表单(必须加 CSRF) -->
<form method="post" action="<?= BLOG_URL ?>?action=comment_post">
<?= Csrf::field() ?>
<input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
<textarea name="comment" required></textarea>
<button type="submit">发表评论</button>
</form>
</section>
<?php include View::resolve('footer'); ?>
5.5 side.php 标准骨架
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php Hook::doAction('sidebar_before'); ?>
<?php WidgetArea::render(WidgetArea::AREA_RIGHT); ?>
<?php Hook::doAction('sidebar_after'); ?>
5.6 其他页面
author.php / search.php / page.php / 404.php 参照上述骨架,必须:
- 设置三个标题变量
- 调用
layout_main_content检查 include View::resolve('header')和footer
第 6 章 · 移动端子主题
6.1 目录结构
content/templates/{主题}/
├── header.php / footer.php / log_list.php / echo_log.php ← PC 版
└── mobile/ ← 移动端版
├── header.php
├── footer.php
├── log_list.php
├── echo_log.php
├── search.php
├── 404.php
├── style.css
├── router.php ← 移动端路由接管器
├── routes.php ← 路由规则
└── views/ ← 插件页面的移动端视图
├── _lib.php
└── forum/
├── index.php
├── board.php
├── thread.php
└── new.php
6.2 移动端识别
内核通过 isMobileDevice() 判断是否使用移动端模板,判断优先级:
1. ?force_view=mobile|desktop (URL 参数,用于测试)
2. Cookie: litelog_view=mobile|desktop
3. User-Agent 匹配(含 Mobile/Android/iPhone/...)
4. 默认 false(PC)
⭐ 关键陷阱:Cookie litelog_view=mobile 会让 PC 浏览器也被判定为移动端。用户切换过移动版视图后,会一直看到移动端。
6.3 移动端 router.php
移动端 router 会在 init.php 里被加载,早于 loadActivePlugins()。它会拦截请求,匹配特定路由,然后 exit。一旦 exit,所有后续插件钩子都不会执行。
标准模板:
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
/* 不接管后台 / 静态资源 */
$__uri = strtok($_SERVER['REQUEST_URI'] ?? '/', '?');
$__base = parse_url(BLOG_URL, PHP_URL_PATH) ?: '/';
if ($__base !== '/' && strpos($__uri, $__base) === 0) {
$__uri = substr($__uri, strlen($__base));
}
$__uri = trim($__uri, '/');
if ($__uri !== '' && strpos($__uri, 'admin') === 0) return;
if ($__uri !== '' && strpos($__uri, 'content/') === 0) return;
/* 加载路由规则 */
$__routesFile = __DIR__ . '/routes.php';
if (!is_file($__routesFile)) return;
$__routes = require $__routesFile;
if (!is_array($__routes)) return;
$__lib = __DIR__ . '/views/_lib.php';
if (is_file($__lib)) require_once $__lib;
/* 匹配并执行 */
$__action = $_GET['action'] ?? '';
foreach ($__routes as $__route) {
if (empty($__route['handler'])) continue;
$__matched = false;
$__m = [];
if (!empty($__route['match']) && $__uri !== '') {
if (@preg_match($__route['match'], $__uri, $__m) === 1) {
$__matched = true;
}
}
if (!$__matched && !empty($__route['action']) && $__action !== '') {
if ($__action === $__route['action']) {
$__matched = true;
}
}
if (!$__matched) continue;
if (!empty($__route['params']) && !empty($__m)) {
foreach ($__route['params'] as $__i => $__name) {
if (isset($__m[$__i])) $_GET[$__name] = $__m[$__i];
}
}
$__handlerFile = __DIR__ . '/views/' . $__route['handler'] . '.php';
if (!is_file($__handlerFile)) continue;
try {
include $__handlerFile;
} catch (Throwable $__e) {
@file_put_contents(
CONTENT_PATH . 'backup/mobile-router-error.log',
'[' . date('Y-m-d H:i:s') . "] handler: {$__route['handler']}\n"
. $__e->getMessage() . "\n" . $__e->getFile() . ':' . $__e->getLine() . "\n\n",
FILE_APPEND | LOCK_EX
);
continue;
}
exit;
}
/* 未匹配:return 让内核继续处理 */
return;
📌 口诀:"匹配就 exit,不匹配 return。" 绝不能"未匹配就 exit"。
6.4 移动端 routes.php
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
return [
[
'match' => '#^forum/?$#',
'action' => 'forum',
'handler' => 'forum/index',
],
[
'match' => '#^forum/board/(\d+)/?$#',
'action' => 'forum_board',
'handler' => 'forum/board',
'params' => [1 => 'board_id'],
],
[
'match' => '#^forum/thread/(\d+)/?$#',
'action' => 'forum_thread',
'handler' => 'forum/thread',
'params' => [1 => 'thread_id'],
],
];
6.5 移动端 header
移动端 header.php 读的标题变量通常是 $__pageTitle:
<?php if (!defined('LITELOG_ROOT')) exit('error!');
$__siteTitle = Option::get('site_title', '');
$__siteDesc = Option::get('site_description', '');
/* ★ 三变量兜底:优先 $__pageTitle */
if (isset($__pageTitle) && $__pageTitle !== '') {
$__title = $__pageTitle;
} elseif (isset($page_title) && $page_title !== '') {
$__title = $page_title;
} elseif (isset($title) && $title !== '') {
$__title = $title;
} else {
$__title = $__siteTitle . ($__siteDesc !== '' ? ' · ' . $__siteDesc : '');
}
$__pageMode = isset($__pageMode) ? $__pageMode : 'home';
$__barTitleFinal = isset($__barTitle) && $__barTitle !== '' ? $__barTitle : $__siteTitle;
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title><?= h($__title) ?></title>
<?php if (class_exists('Hook')) Hook::doAction('theme_head'); ?>
</head>
<body class="app-body mode-<?= h($__pageMode) ?>">
<?php if (class_exists('Hook')) Hook::doAction('theme_body_start'); ?>
<header class="app-bar" id="appBar">
<!-- 顶部 AppBar -->
</header>
<main class="app-main">
6.6 ⭐ 移动端页脚的底部导航
移动端通常有底部 Tab Bar,用于首页/论坛/发帖/小说/日读等入口切换:
<?php
$__uri = strtolower($_SERVER['REQUEST_URI'] ?? '/');
$__isForum = (strpos($__uri, '/forum') !== false || strpos($__uri, 'action=forum') !== false);
$__isHome = (!$__isForum && !$__isNovel && !$__isDaily);
?>
<nav class="app-tabbar">
<a class="tab-item<?= $__isHome ? ' active' : '' ?>" href="<?= BLOG_URL ?>">首页</a>
<a class="tab-item<?= $__isForum ? ' active' : '' ?>" href="<?= BLOG_URL ?>forum">论坛</a>
<!-- ... -->
</nav>
⭐ 注意:底部 Tab Bar 在所有页面常驻,主内容区需要留出底部间距:
.app-main {
padding-bottom: calc(var(--tab-h) + var(--safe-b) + 20px);
}
第 3 部分:核心能力
第 7 章 · 布局系统
7.1 三栏布局
标准 LiteLog 主题使用三栏布局:
┌──────────┬──────────────────┬──────────┐
│ 左栏 │ 主内容 │ 右栏 │
│ left- │ main-col │ side- │
│ col │ │ col │
│ 200px │ flex:1 │ 280px │
└──────────┴──────────────────┴──────────┘
CSS 骨架:
.layout {
display: flex;
gap: 24px;
align-items: flex-start;
}
.left-col {
width: 200px;
flex-shrink: 0;
}
.main-col {
flex: 1;
min-width: 0;
}
.side-col {
width: 280px;
flex-shrink: 0;
}
响应式:
@media (max-width: 1100px) {
.left-col { display: none; }
}
@media (max-width: 900px) {
.side-col { display: none; }
.layout { flex-direction: column; }
.main-col { width: 100%; }
}
7.2 ⭐ 主内容区覆盖模式
插件可以通过 layout_main_content 钩子完全接管主内容区,此时应使用单栏布局:
.layout.layout-single {
display: block;
padding: 28px 0;
gap: 0;
}
.layout.layout-single .main-col {
width: 100%;
max-width: none;
}
log_list.php 里的关键判断:
<?php if ($__hasMainOverride): ?>
<div class="layout layout-single">
<div class="main-col" data-region="main">
<?= $__main_override ?>
</div>
</div>
<?php else: ?>
<!-- 正常三栏 -->
<?php endif; ?>
⭐ 关键:如果不做这个判断,插件的覆盖内容会被挤在三栏布局的主栏里,宽度异常。
7.3 首页模式系统
LiteLog 支持首页模式切换(博客 / 论坛 / 自定义):
$currentMode = HomepageModeRegistry::getCurrent();
if ($currentMode) {
$currentMode->render($ctx);
}
自定义首页模式:
class HomepageMode_MyMode extends HomepageMode {
public $id = 'my_mode';
public $name = '我的模式';
public $theme = 'purple';
public $priority = 50;
public function isAvailable() { return true; }
public function render($ctx = []) {
echo '<div class="my-mode">...</div>';
}
}
⭐ 关键:主题不需要知道具体有多少模式,只要调用 $currentMode->render($ctx) 即可。模式自己会处理输出。
第 8 章 · 内容渲染
8.1 文章正文输出
文章内容是 HTML(由编辑器生成),直接输出:
<div class="content"><?= $log['content'] ?></div>
⭐ 不要用 h($log['content']) 转义,否则会显示原始 HTML 标签。
⭐ 但要对用户输入的其它字段转义:
<h1><?= h($log['title']) ?></h1>
<span><?= h($log['author_name']) ?></span>
8.2 摘要与纯文本
<?= h(getExcerpt($log['content'], 200)) ?>
8.3 日期格式化
<?= h($log['date_fmt']) ?> <!-- Y-m-d -->
<?= h(dateFmt($log['date'])) ?> <!-- 同上 -->
<?= h(datetimeFmt($log['date'])) ?> <!-- Y-m-d H:i -->
8.4 分页
控制器传入的 $pageHtml 已经是完整的分页 HTML:
<?= $pageHtml ?>
8.5 评论树
<?php if (!empty($comments)): ?>
<div class="comments">
<?php foreach ($comments as $c): ?>
<?= renderComment($c) ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
评论数据结构:
$c = [
'cid' => 1,
'gid' => 10,
'pid' => 0,
'poster' => '张三',
'comment' => '写得真好',
'date' => 1234567890,
'hide' => 'n',
'children' => [ /* 嵌套回复 */ ],
];
递归渲染:
function renderComment($c, $depth = 0) {
if ($depth > 3) $depth = 3;
?>
<div class="cmt" data-cid="<?= (int)$c['cid'] ?>">
<div class="cmt-name"><?= h($c['poster']) ?></div>
<div class="cmt-text"><?= nl2br(h($c['comment'])) ?></div>
<?php if (!empty($c['children'])): ?>
<div class="cmt-children">
<?php foreach ($c['children'] as $child) renderComment($child, $depth + 1); ?>
</div>
<?php endif; ?>
</div>
<?php
}
8.6 ⭐ 评论表单必须加 CSRF
<form method="post" action="<?= BLOG_URL ?>?action=comment_post">
<?= Csrf::field() ?>
<input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
<input type="hidden" name="pid" value="0">
<textarea name="comment" required></textarea>
<button type="submit">发表评论</button>
</form>
缺少 Csrf::field() 会导致所有评论提交失败,返回 {"ok":false,"msg":"缺少 CSRF 令牌"}。
第 9 章 · 插件兼容
9.1 ⭐ 主题必须触发的钩子清单
这是主题能否兼容插件的关键。缺少任何一个,插件都可能失效。
| 位置 | 必须触发的钩子 |
|---|---|
<head> 最前 | layout_head_start |
</head> 前 | layout_head_end + template_head |
<body> 之后 | layout_body_start + body_start |
<main> 之后 | layout_content_start |
内容后、</main> 前 | layout_content_end |
</body> 前的脚本区 | layout_footer_scripts |
</body> 之前 | layout_body_end + body_end |
验证方法:启用一个依赖 layout_head_end 注入 CSS 的插件(如每日一文),检查页面源码里是否有插件 CSS 的 <link>。
9.2 ⭐ 主内容区覆盖处理
每个页面都要处理 layout_main_content:
$__main_override = '';
if (class_exists('Hook')) {
try {
$__main_override = trim(Hook::captureAction('layout_main_content', $ctx));
} catch (Throwable $e) { $__main_override = ''; }
}
$__hasMainOverride = ($__main_override !== '');
缺少这段:插件的"完全接管主内容区"能力失效,插件内容被挤在三栏布局里。
9.3 ⭐ 标题三变量兜底
这是本轮修复战最痛的教训之一。
$__final_title = '';
if (isset($page_title) && $page_title !== '') {
$__final_title = $page_title;
} elseif (isset($title) && $title !== '') {
$__final_title = $title;
} elseif (isset($__pageTitle) && $__pageTitle !== '') {
$__final_title = $__pageTitle;
} else {
$__final_title = Option::get('site_title', '');
}
原因:
- 插件可能设置
$page_title - v3.1 规范要求插件设置
$title - 移动端 header 通常读
$__pageTitle - 主题不识别三变量 → 插件页面标题退化为站点名
9.4 ⭐ 侧边栏兼容
三种侧边栏来源,优先级从高到低:
- Widget 系统(推荐):
WidgetArea::hasVisibleWidgets()+WidgetArea::render() - 传统 Hook:
left_sidebar_before/left_sidebar/left_sidebar_after - 硬编码:主题自己的侧边栏内容
$hasWidgets = WidgetArea::hasVisibleWidgets(WidgetArea::AREA_LEFT);
$hasLegacy = false; $legacyHtml = '';
if (!$hasWidgets) {
$legacyHtml = Hook::captureAction('left_sidebar_before')
. Hook::captureAction('left_sidebar')
. Hook::captureAction('left_sidebar_after');
$hasLegacy = trim($legacyHtml) !== '';
}
?>
<?php if ($hasWidgets || $hasLegacy): ?>
<aside class="left-col" data-region="left">
<?php if ($hasWidgets) WidgetArea::render(WidgetArea::AREA_LEFT); else echo $legacyHtml; ?>
</aside>
<?php endif; ?>
9.5 插件资源注入规则
插件资源(CSS/JS)通过 layout_head_end / layout_footer_scripts 注入,主题不需要做任何事。
插件资源不会自动加载,除非插件设置 $GLOBALS['__xxx_active'] = true。
9.6 兼容性检查清单
- [ ]
header.php触发layout_head_start/layout_head_end/layout_body_start - [ ]
header.php触发template_head/body_start - [ ]
header.php识别三变量标题 - [ ]
footer.php触发layout_content_end/layout_footer_scripts/layout_body_end/body_end - [ ]
footer.php触发template_footer - [ ] 每个页面文件处理
layout_main_content覆盖 - [ ] 侧边栏使用
WidgetArea+ Legacy Hook 双兼容 - [ ] 评论表单加
Csrf::field() - [ ] 用户输入全部过
h()
第 4 部分:安全与陷阱
第 10 章 · 十条主题红线
- 🔴 视图文件必须 include
header和footer——不要自建<!DOCTYPE html> - 🔴 **必须调用所有
layout_*钩子**——否则插件无法注入资源 - 🔴 必须处理
layout_main_content覆盖——否则插件主内容区接管失效 - 🔴 必须识别三种标题变量——
$page_title/$title/$__pageTitle - 🔴 评论表单必须加
Csrf::field() - 🔴 用户输入必须过
h()转义——但文章content除外 - 🔴 PHP 标签合并,避免连续两个
<?php - 🔴 移动端
router.php未匹配必须return,不能exit - 🔴 不要硬编码 URL——用
Url::log()/Url::sort()等 - 🔴 不要修改核心文件——主题只应放
content/templates/{主题}/
第 11 章 · 常见陷阱
11.1 ⭐ 标题不显示插件名
症状:访问插件页面,浏览器标签显示站点名。
原因:主题 header.php 只读 $page_title,而插件设置的是 $__pageTitle。
修复:三变量兜底(见第 9.3 节)。
11.2 ⭐ 插件 CSS/JS 不加载
症状:插件页面样式错乱、交互失效。
原因:主题 header.php / footer.php 没有触发 layout_head_end / layout_footer_scripts 钩子。
修复:在 header.php 的 </head> 前触发 layout_head_end,footer.php 的 </body> 前触发 layout_footer_scripts。
11.3 ⭐ 插件主内容区接管失效
症状:插件想完全接管主内容区(如每日一文详情页),但仍被挤在三栏布局里,宽度异常。
原因:主题 log_list.php / echo_log.php 没有处理 layout_main_content 覆盖。
修复:在每个页面文件里加覆盖检查(见第 7.2 节)。
11.4 ⭐ 移动端页面错乱
症状:PC 上正常,手机上错乱;或切换过移动视图后 PC 也错乱。
原因:
- Cookie
litelog_view=mobile命中 - 或移动端
header.php读的变量和 PC 版不一致
排查:
- 清空
litelog_viewcookie - 检查移动端
header.php是否三变量兜底 - 用
?force_view=desktop强制 PC 视图
11.5 ⭐ 移动端 router 抢跑导致插件页面 404
症状:手机上访问插件页面,404 或显示博客首页。
原因:mobile/router.php 未匹配路由时错误地 exit(应为 return)。
修复:查看 content/backup/mobile-router-error.log,修正 router 逻辑。
11.6 ⭐ PHP 语法错误导致白屏
症状:整页白屏,无任何错误信息。
原因:视图文件出现"连续两个 <?php",或被编辑器粘贴时破坏了 PHP 标签。
修复:
php -l content/templates/xxx/header.php- 合并为一个 PHP 块(见第 5.3 节)
11.7 ⭐ 评论提交失败
症状:评论提交返回 {"ok":false,"msg":"缺少 CSRF 令牌"}。
原因:评论表单没有 Csrf::field()。
修复:在 <form> 里加 <?= Csrf::field() ?>。
11.8 ⭐ 页面标题被缓存
症状:更新插件后,浏览器标签仍显示旧标题。
原因:浏览器缓存或中间层 CDN。
修复:
- 强刷
Ctrl + Shift + R - 加 JS 强制覆盖
document.title(见第 12 章)
11.9 ⭐ 主题 CSS 覆盖插件样式
症状:插件页面的样式和预期不同。
原因:主题的通用样式(如 .button / .card)和插件类名冲突。
修复:
- 主题用带前缀的类名(如
.mytheme-btn) - 或使用 CSS Modules / Scoped 方案
11.10 ⭐ 硬编码路径导致子目录部署失败
症状:站点部署在 https://example.com/blog/ 下时,主题资源 404。
原因:硬编码 /content/templates/...。
修复:用 BLOG_URL . 'content/...' 或 TEMPLATE_URL。
第 12 章 · ⭐ 标题终极方案:JS 强制覆盖
当主题 header 硬编码站点名、或变量名不可预测时,用 JS 兜底。
12.1 主题端实现
在 header.php 的 <title> 之后立即注入:
<title><?= h($__final_title) ?></title>
<script>
(function () {
var t = <?= json_encode($__final_title, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function setTitle() {
try { if (document.title !== t) document.title = t; } catch (e) {}
}
setTitle();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setTitle);
}
setTimeout(setTitle, 100);
window.addEventListener('load', function () { setTimeout(setTitle, 200); });
})();
</script>
12.2 为什么"绝对有效"
| 场景 | 效果 |
|---|---|
| 主题 header 读对变量 | ✅ 已经对 |
| 主题 header 读错变量 | ✅ JS 覆盖 |
| 主题 header 硬编码 | ✅ JS 覆盖 |
| 插件延迟修改 title | ✅ JS 覆盖 |
| 浏览器缓存旧 title | ✅ JS 覆盖 |
12.3 ⚠️ 注意事项
json_encode参数必须包含JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT,防 XSS- 用 IIFE 包裹,避免变量污染全局
- 多次执行
setTitle覆盖不同时机的修改
第 5 部分:实战技巧
第 13 章 · 主题开发工作流
13.1 从零开发主题
1. 复制 default 主题为起点
cp -r content/templates/default content/templates/mytheme
2. 修改 header.php 头注释(Theme Name / Version)
3. 修改 css/style.css(视觉风格)
4. 逐步改造每个模板文件
5. 本地测试:
- 首页
- 分类页
- 标签页
- 文章详情
- 搜索页
- 404
- 移动端(?force_view=mobile)
6. 后台启用主题
13.2 修改现有主题
推荐流程:
1. 备份原主题(改名 + .bak 后缀)
2. 逐文件修改,每次改一个
3. 每改一个页面,访问验证
4. 特别注意:
- 三变量标题
- layout_* 钩子
- layout_main_content 覆盖
- CSRF 表单
13.3 调试技巧
F12 打开开发者工具:
| 检查项 | 位置 |
|---|---|
| 网络请求 | Network 面板 |
| 元素结构 | Elements 面板 |
| 控制台错误 | Console 面板 |
| 资源加载 | Network 面板过滤 CSS/JS |
| 移动端视图 | 设备模拟器(Ctrl+Shift+M) |
临时调试输出:
<?php
echo '<pre style="position:fixed;top:0;left:0;right:0;z-index:9999;background:#fff;padding:10px;border-bottom:2px solid red">';
var_dump($page_title, $title, $__pageTitle);
echo '</pre>';
?>
13.4 移动端测试
| 方法 | URL |
|---|---|
| 强制移动端 | ?force_view=mobile |
| 强制 PC | ?force_view=desktop |
| 切换 Cookie | 在浏览器控制台执行 document.cookie='litelog_view=mobile;path=/' |
第 14 章 · 主题与插件协同案例
14.1 案例:每日一文插件页面显示站点名
问题:访问 /daily_article/detail/5,浏览器标签显示站点名。
排查:
- 读
header.php,<title>读$page_title - 视图设置了
$page_title—— 理论正确 - 用 Cookie 检查,
litelog_view=mobile存在 - 浏览器走了
mobile/header.php,读$__pageTitle(未设置)
主题端修复:
header.php三变量兜底mobile/header.php三变量兜底- 加 JS 覆盖
document.title
14.2 案例:论坛插件 CSS 不加载
问题:论坛页面样式错乱。
排查:
- F12 Network 面板,论坛 CSS 请求 404 或 200 但未生效
- 检查页面源码,
<head>里没有论坛 CSS 的<link> - 检查主题
header.php,没触发layout_head_end钩子
主题端修复:在 header.php 的 </head> 前加:
<?php Hook::doAction('layout_head_end'); ?>
14.3 案例:插件无法完全接管主内容区
问题:每日一文首页想全宽显示卡片,但被挤在中间 700px。
排查:
- 检查
log_list.php,没有处理layout_main_content覆盖 - 插件设置的
$__main_override被丢弃
主题端修复:加覆盖检查(见第 7.2 节)。
第 6 部分:调试与诊断
第 15 章 · 错误排查方法论
15.1 症状 → 第一步
| 症状 | 第一步 |
|---|---|
| 白屏 | php -l content/templates/xxx/header.php |
| 标题不对 | 读 header.php,看 <title> 读什么变量 |
| 插件 CSS 不加载 | 检查 header.php 是否有 layout_head_end |
| 插件 JS 不加载 | 检查 footer.php 是否有 layout_footer_scripts |
| 主内容区接管失效 | 检查是否有 layout_main_content 覆盖处理 |
| 评论提交失败 | 检查评论表单是否有 Csrf::field() |
| 移动端错乱 | 清空 litelog_view cookie |
| 移动端 router 抢跑 | 读 mobile/router.php,检查未匹配分支 |
15.2 主题诊断脚本
保存为 _diag_theme.php,放网站根目录:
<?php
while (ob_get_level() > 0) { @ob_end_clean(); }
if (!defined('LITELOG_ROOT')) {
define('LITELOG_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
}
$_GET['force_view'] = 'desktop';
$_COOKIE['litelog_view'] = 'desktop';
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>主题诊断</title>
<style>
body{font-family:Consolas,monospace;font-size:13px;padding:22px;background:#0f172a;color:#e2e8f0;line-height:1.7}
h1{color:#60a5fa}
h2{color:#a78bfa;font-size:15px;margin:24px 0 10px;border-bottom:1px solid #334155;padding-bottom:5px}
.ok{color:#6ee7b7}.err{color:#fca5a5}.warn{color:#fcd34d}.info{color:#93c5fd}
pre{background:#1e293b;padding:11px;border-radius:7px;white-space:pre-wrap;word-break:break-all;margin:6px 0}
</style>
</head>
<body>
<h1>🎨 主题诊断报告</h1>
<p><?= date('Y-m-d H:i:s') ?> · PHP <?= PHP_VERSION ?></p>
<?php
function row($level, $text) {
echo '<div><span class="' . $level . '">[' . strtoupper($level) . ']</span> ' . $text . '</div>';
}
function h2($t) { echo '<h2>' . htmlspecialchars($t) . '</h2>'; }
h2('阶段 1 · 加载核心');
try {
require_once LITELOG_ROOT . 'init.php';
row('ok', 'init.php 加载完成');
} catch (Throwable $e) {
row('err', 'init.php 抛异常:' . $e->getMessage());
echo '<pre>' . htmlspecialchars($e->getFile() . ':' . $e->getLine()) . '</pre>';
exit;
}
h2('阶段 2 · 当前主题信息');
$tpl = Option::get('current_template', 'default');
row('info', '当前主题:<strong>' . htmlspecialchars($tpl) . '</strong>');
$tplDir = CONTENT_PATH . 'templates/' . $tpl . '/';
row('info', '主题目录:<code>' . htmlspecialchars($tplDir) . '</code>');
$hasMobile = is_dir($tplDir . 'mobile/');
row($hasMobile ? 'ok' : 'info', '移动端子目录:' . ($hasMobile ? '存在' : '不存在'));
h2('阶段 3 · 主题文件检查');
$required = ['header.php', 'footer.php', 'log_list.php', 'echo_log.php', 'side.php'];
$recommended = ['author.php', 'search.php', 'page.php', '404.php'];
foreach ($required as $f) {
row(is_file($tplDir . $f) ? 'ok' : 'err', '【必须】' . $f);
}
foreach ($recommended as $f) {
row(is_file($tplDir . $f) ? 'ok' : 'info', '【可选】' . $f);
}
row(is_file($tplDir . 'css/style.css') ? 'ok' : 'warn', 'css/style.css');
h2('阶段 4 · 语法检查');
foreach (['header.php', 'footer.php', 'log_list.php', 'echo_log.php'] as $f) {
$path = $tplDir . $f;
if (!is_file($path)) continue;
if (function_exists('exec')) {
$out = []; $ret = 0;
@exec('php -l ' . escapeshellarg($path) . ' 2>&1', $out, $ret);
if ($ret === 0) {
row('ok', $f . ' 语法通过');
} elseif ($ret !== 127) {
row('err', $f . ' → ' . implode(' ', $out));
}
}
}
h2('阶段 5 · 钩子检查(关键)');
$headerFile = $tplDir . 'header.php';
if (is_file($headerFile)) {
$c = file_get_contents($headerFile);
$checks = [
'layout_head_start' => 'head 起始锚点',
'layout_head_end' => 'head 结束锚点(插件 CSS 注入)',
'layout_body_start' => 'body 起始锚点(顶部横幅)',
'template_head' => '兼容老版插件',
'body_start' => '兼容老版插件',
];
foreach ($checks as $hook => $label) {
$has = strpos($c, $hook) !== false;
row($has ? 'ok' : ($hook === 'layout_head_end' ? 'err' : 'warn'),
$label . '(' . $hook . ')');
}
}
$footerFile = $tplDir . 'footer.php';
if (is_file($footerFile)) {
$c = file_get_contents($footerFile);
$checks = [
'layout_content_end' => '主内容结束锚点',
'layout_footer_scripts' => '脚本注入锚点(插件 JS)',
'layout_body_end' => 'body 结束锚点',
'body_end' => '兼容老版插件',
'template_footer' => '兼容老版插件',
];
foreach ($checks as $hook => $label) {
$has = strpos($c, $hook) !== false;
row($has ? 'ok' : ($hook === 'layout_footer_scripts' ? 'err' : 'warn'),
$label . '(' . $hook . ')');
}
}
h2('阶段 6 · 标题变量检查');
if (is_file($headerFile)) {
$c = file_get_contents($headerFile);
foreach (['$page_title', '$title', '$__pageTitle'] as $v) {
$has = strpos($c, $v) !== false;
row($has ? 'ok' : 'warn', '识别标题变量 ' . $v);
}
}
$mobileHeader = $tplDir . 'mobile/header.php';
if (is_file($mobileHeader)) {
$c = file_get_contents($mobileHeader);
foreach (['$page_title', '$title', '$__pageTitle'] as $v) {
$has = strpos($c, $v) !== false;
row($has ? 'ok' : 'warn', '【移动端】识别变量 ' . $v);
}
}
h2('阶段 7 · 主内容区覆盖检查');
foreach (['log_list.php', 'echo_log.php'] as $f) {
$path = $tplDir . $f;
if (!is_file($path)) continue;
$c = file_get_contents($path);
$has = strpos($c, 'layout_main_content') !== false;
row($has ? 'ok' : 'err', $f . ' 处理 layout_main_content');
}
h2('阶段 8 · 评论表单 CSRF');
if (is_file($tplDir . 'echo_log.php')) {
$c = file_get_contents($tplDir . 'echo_log.php');
$has = strpos($c, 'Csrf::field') !== false;
row($has ? 'ok' : 'warn', '评论表单加 Csrf::field()');
}
h2('阶段 9 · 移动端 router');
$router = $tplDir . 'mobile/router.php';
if (is_file($router)) {
$c = file_get_contents($router);
$hasReturn = strpos($c, 'return;') !== false;
row($hasReturn ? 'ok' : 'err', 'router.php 未匹配时 return(而非 exit)');
} else {
row('info', '无移动端 router');
}
echo '<h2>✅ 诊断完成</h2>';
echo '<p>请把整页内容发我。</p>';
echo '<p>⚠️ 用完请删除 _diag_theme.php</p>';
?>
</body>
</html>
15.3 输出解读
| 阶段 | 状态 | 含义 |
|---|---|---|
| 3 必须文件 err | — | 缺核心文件 |
| 4 语法 err | — | 视图有 PHP 语法错误 |
5 layout_head_end err | — | 插件 CSS 不会加载 |
5 layout_footer_scripts err | — | 插件 JS 不会加载 |
| 6 三变量 warn | — | 插件标题可能不显示 |
7 layout_main_content err | — | 插件主内容区接管失效 |
| 8 CSRF warn | — | 评论提交会失败 |
| 9 router return err | — | 移动端插件页面可能 404 |
第 16 章 · 常见错误诊断表
16.1 浏览器标签显示站点名
| 步骤 | 操作 |
|---|---|
| 1 | 读主题 header.php 的 <title> 行 |
| 2 | 检查是否三变量兜底 |
| 3 | 清空 litelog_view cookie |
| 4 | 加 JS 覆盖 document.title |
16.2 插件 CSS/JS 不加载
| 步骤 | 操作 |
|---|---|
| 1 | F12 Network 看插件资源请求 |
| 2 | 检查 header.php 是否触发 layout_head_end |
| 3 | 检查 footer.php 是否触发 layout_footer_scripts |
| 4 | 检查插件是否设置 $GLOBALS['__xxx_active'] |
16.3 主内容区接管失效
| 步骤 | 操作 |
|---|---|
| 1 | 检查页面文件是否有 layout_main_content 捕获 |
| 2 | 检查 $ctx 是否正确传递 |
| 3 | 检查 layout-single CSS 是否定义 |
16.4 移动端页面错乱
| 步骤 | 操作 |
|---|---|
| 1 | 清空 litelog_view cookie |
| 2 | 用 ?force_view=desktop 强制 PC 视图对比 |
| 3 | 检查 mobile/header.php 三变量兜底 |
| 4 | 检查移动端 CSS 是否冲突 |
16.5 白屏
| 步骤 | 操作 |
|---|---|
| 1 | php -l 每个 PHP 文件 |
| 2 | 运行 _diag_theme.php |
| 3 | 打开 display_errors |
| 4 | 读错误日志 |
第 7 部分:交付与质量
第 17 章 · 完整主题骨架
17.1 目录结构
content/templates/mytheme/
├── header.php
├── footer.php
├── log_list.php
├── echo_log.php
├── side.php
├── author.php
├── search.php
├── page.php
├── 404.php
├── css/
│ └── style.css
├── js/
│ └── codeblock.js
├── preview.jpg
└── mobile/ ← 可选
├── header.php
├── footer.php
├── log_list.php
├── echo_log.php
├── style.css
├── router.php
├── routes.php
└── views/
17.2 header.php(完整)
<?php
/**
* Theme Name: MyTheme
* Description: 极简单栏主题
* Version: 1.0.0
* Author: 你的名字
* Author URI: https://example.com
*/
if (!defined('LITELOG_ROOT')) exit('error!');
/* ★ 三变量兜底 */
$__final_title = '';
if (isset($page_title) && $page_title !== '') {
$__final_title = $page_title;
} elseif (isset($title) && $title !== '') {
$__final_title = $title;
} elseif (isset($__pageTitle) && $__pageTitle !== '') {
$__final_title = $__pageTitle;
} else {
$__final_title = Option::get('site_title', '');
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<?php Hook::doAction('layout_head_start'); ?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($__final_title) ?></title>
<script>
/* ★ JS 强制覆盖 document.title */
(function () {
var t = <?= json_encode($__final_title, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function setTitle() { try { if (document.title !== t) document.title = t; } catch (e) {} }
setTitle();
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setTitle);
setTimeout(setTitle, 100);
window.addEventListener('load', function () { setTimeout(setTitle, 200); });
})();
</script>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<link rel="icon" href="<?= BLOG_URL ?>favicon.ico">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css<?= TEMPLATE_CSS_VER ?>">
<?php Hook::doAction('template_head'); ?>
<?php Hook::doAction('layout_head_end'); ?>
</head>
<body>
<?php Hook::doAction('body_start'); ?>
<?php Hook::doAction('layout_body_start'); ?>
<header class="site-header">
<?php Hook::doAction('header_before'); ?>
<div class="container">
<?php Hook::doAction('header_inner_before'); ?>
<h1 class="logo"><a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a></h1>
<p class="desc"><?= h(Option::get('site_description')) ?></p>
<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; ?>
<?php Hook::doAction('header_nav'); ?>
<a href="<?= BLOG_URL ?>admin/">管理</a>
</nav>
<?php Hook::doAction('header_inner_after'); ?>
</div>
<?php Hook::doAction('header_after'); ?>
</header>
<?php Hook::doAction('before_content'); ?>
<main class="container">
<?php Hook::doAction('layout_content_start'); ?>
17.3 footer.php(完整)
<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php Hook::doAction('layout_content_end'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="site-footer">
<div class="container">
<?php Hook::doAction('footer_content'); ?>
<p>© <?= date('Y') ?> <?= h(Option::get('site_title')) ?> · Powered by LiteLog</p>
</div>
</footer>
<?php Hook::doAction('footer_after'); ?>
<?php Hook::doAction('template_footer'); ?>
<?php Hook::doAction('layout_footer_scripts'); ?>
<?php Hook::doAction('body_end'); ?>
<?php Hook::doAction('layout_body_end'); ?>
</body>
</html>
17.4 log_list.php / echo_log.php / side.php / author.php / search.php / page.php / 404.php
见第 5 章骨架。
17.5 css/style.css
自行设计。
17.6 移动端子目录(可选)
见第 6 章。
第 18 章 · 交付前自检清单
结构
- [ ] 目录名小写下划线
- [ ] 有
header.php/footer.php/log_list.php/echo_log.php - [ ] 有
css/style.css - [ ] 有
side.php(推荐) - [ ] 有
author.php/search.php/page.php/404.php(可选) - [ ] 有移动端
mobile/子目录(可选) - [ ] 有
preview.jpg(可选)
⭐ 钩子
- [ ]
header.php触发layout_head_start - [ ]
header.php触发layout_head_end - [ ]
header.php触发layout_body_start - [ ]
header.php触发template_head/body_start - [ ]
footer.php触发layout_content_end - [ ]
footer.php触发layout_footer_scripts - [ ]
footer.php触发layout_body_end - [ ]
footer.php触发template_footer/body_end
⭐ 标题
- [ ]
header.php识别$page_title - [ ]
header.php识别$title - [ ]
header.php识别$__pageTitle - [ ] 加 JS 覆盖
document.title - [ ] 移动端
header.php同样三变量兜底
⭐ 主内容区覆盖
- [ ]
log_list.php处理layout_main_content - [ ]
echo_log.php处理layout_main_content - [ ]
page.php处理layout_main_content - [ ]
author.php处理layout_main_content - [ ]
search.php处理layout_main_content - [ ]
404.php处理layout_main_content - [ ] CSS 定义
.layout-single
⭐ 安全
- [ ] 评论表单有
Csrf::field() - [ ] 用户输入全部过
h()转义 - [ ] 文章
content不转义 - [ ] 无硬编码 URL
- [ ] 无硬编码路径
- [ ] 无
eval/unserialize
⭐ 视图
- [ ] 每个视图
include View::resolve('header')和footer - [ ] 每个视图不自建
<!DOCTYPE html> - [ ] 每个
.php文件通过php -l - [ ] PHP 标签合并,无连续
<?php
⭐ 侧边栏
- [ ] 支持
WidgetArea::hasVisibleWidgets() - [ ] 支持
WidgetArea::render() - [ ] 兼容
left_sidebar系列 Hook
⭐ 移动端
- [ ] 移动端
header.php三变量兜底 - [ ] 移动端
router.php未匹配时return - [ ] 移动端有底部 Tab Bar(可选)
- [ ] 主内容区留出底部间距
验证
- [ ] 首页正常
- [ ] 分类页正常
- [ ] 标签页正常
- [ ] 文章详情正常
- [ ] 搜索页正常
- [ ] 404 页正常
- [ ] 评论提交成功
- [ ] 移动端
?force_view=mobile正常 - [ ] F12 Console 无报错
- [ ] Network 全 200
- [ ] 用
_diag_theme.php走查一遍
第 19 章 · 二十条铁律
| # | 铁律 |
|---|---|
| 1 | 视图不能自建 HTML 骨架 |
| 2 | 每个视图必须 include View::resolve('header') 和 footer |
| 3 | header.php 必须触发 layout_head_end |
| 4 | footer.php 必须触发 layout_footer_scripts |
| 5 | 每个页面必须处理 layout_main_content 覆盖 |
| 6 | 必须识别三种标题变量 |
| 7 | 评论表单必须加 Csrf::field() |
| 8 | 用户输入必须过 h() 转义,文章 content 除外 |
| 9 | 移动端 router.php 未匹配必须 return |
| 10 | 不要在 header.php 硬编码站点名 |
| 11 | 资源 URL 用 TEMPLATE_URL 或 BLOG_URL 拼接 |
| 12 | 不硬编码路径,考虑子目录部署 |
| 13 | 主题目录只放 content/templates/{主题}/ |
| 14 | 类名加主题前缀,避免和插件冲突 |
| 15 | 侧边栏支持 Widget + Legacy 双兼容 |
| 16 | 移动端 header.php 也要三变量兜底 |
| 17 | PHP 标签合并,避免连续两个 <?php |
| 18 | 用 ?force_view=mobile / ?force_view=desktop 测试两端 |
| 19 | 主题头注释必须完整 |
| 20 | 每个 .php 文件通过 php -l |
常见错误推理链
❌ 错误链 A:插件 CSS 不加载 → "可能插件问题" → 检查插件 → 无问题 → "放弃"
✅ 正确链:插件 CSS 不加载 → F12 Network 看请求 → 请求根本不存在 → 检查 header.php 是否有 layout_head_end → 发现没有 → 加钩子
❌ 错误链 B:标题不对 → 改 $page_title → 无效 → "改主题没用"
✅ 正确链:标题不对 → 读 header.php 的 <title> 行 → 发现只读 $page_title → 三变量兜底
❌ 错误链 C:移动端错乱 → 改 CSS → 无效 → "移动端真难搞"
✅ 正确链:移动端错乱 → 清空 litelog_view cookie → 强制 PC 视图对比 → 定位是移动端 header 的问题
❌ 错误链 D:白屏 → "可能数据库问题" → 检查 DB → 正常 → "放弃"
✅ 正确链:白屏 → php -l 每个 PHP 文件 → 找到语法错误行号 → 修复
第 8 部分:附录
附录 A · 主题 API 速查
常量
BLOG_URL
CONTENT_PATH / ADMIN_PATH / INCLUDE_PATH
TEMPLATE_URL / TEMPLATE_ROOT_URL / TEMPLATE_MOBILE_URL
TEMPLATE_CSS_VER
DB_PREFIX
视图
View::resolve($name)
View::display($name, $data)
选项
Option::get($key, '')
Option::set($key, $value)
钩子
Hook::doAction($hook, ...$args)
Hook::captureAction($hook, ...$args)
Hook::addAction($hook, $func, $priority = 10)
Hook::applyFilters($hook, $value, ...$args)
模型
LogModel::query() / getOne() / latest() / prev() / next() / count() / search()
SortModel::all() / getOne() / getByAlias() / countInSort()
TagModel::all() / getByName() / getByGid()
CommentModel::byLog() / byLogPaginated() / buildTree() / countByLog()
UserModel::getOne() / getByUsername()
URL
Url::log($gid, $alias)
Url::sort($sid, $alias)
Url::tag($name)
Url::author($uid)
Url::record($date)
小工具
WidgetArea::hasVisibleWidgets($area)
WidgetArea::render($area)
WidgetArea::AREA_LEFT / AREA_RIGHT
CSRF
Csrf::field()
Csrf::token()
辅助函数
h($s)
redirect($url)
isLoggedIn()
dateFmt($ts) / datetimeFmt($ts)
getExcerpt($html, $len)
makeAlias($str)
pagination($page, $total, $perPage)
getPageNumber()
isMobileDevice()
getCurrentTemplateName()
hasMobileTemplateDir()
附录 B · 系统钩子清单
布局锚点(推荐)
| 钩子 | 触发点 |
|---|---|
layout_head_start | <head> 最前 |
layout_head_end | </head> 前 |
layout_body_start | <body> 之后 |
layout_content_start | <main> 之后 |
layout_content_end | 内容后、</main> 前 |
layout_footer_scripts | </body> 前的脚本区 |
layout_body_end | </body> 之前 |
传统钩子(兼容)
template_head / body_start / header_before / header_inner_before / header_nav / header_inner_after / header_after / header_meta / before_content / after_content / footer_before / footer_content / footer_after / template_footer / body_end / sidebar_before / sidebar_after / left_sidebar_before / left_sidebar / left_sidebar_after / side_col_before / side_col_after
扩展点
| 钩子 | 用途 |
|---|---|
layout_main_content | 插件完全接管主内容区 |
register_widgets | 注册小工具 |
register_homepage_modes | 注册首页模式 |
附录 C · 主题文件清单
| 文件 | 必须 | 说明 |
|---|---|---|
header.php | ✅ | HTML head + 页头 |
footer.php | ✅ | 页脚 + 脚本注入 |
log_list.php | ✅ | 文章列表(首页/分类/标签/作者/归档/搜索) |
echo_log.php | ✅ | 文章详情 |
css/style.css | ✅ | 主样式 |
side.php | 推荐 | 右侧栏 |
author.php | 可选 | 作者主页 |
search.php | 可选 | 搜索结果 |
page.php | 可选 | 单页 |
404.php | 可选 | 404 页 |
js/*.js | 可选 | 自定义脚本 |
preview.jpg | 可选 | 预览图 |
mobile/* | 可选 | 移动端子主题 |
附录 D · 案例集
D.1 案例:插件 CSS 不加载
症状:启用每日一文插件,页面样式错乱。
排查:
- F12 Network 面板,每日一文 CSS 请求未发出
- 检查页面源码,
<head>里没有插件 CSS 的<link> - 读主题
header.php,</head>前没有layout_head_end钩子
修复:
<?php Hook::doAction('layout_head_end'); ?>
</head>
教训:见第 9.1 节。
D.2 案例:标题不显示插件名
症状:访问 /daily_article/detail/5,浏览器标签显示站点名。
排查:
- 读主题
header.php,<title>读$page_title - 视图设置了
$page_title—— 理论正确 - 用 Cookie 检查,
litelog_view=mobile存在 - 浏览器走了
mobile/header.php,读$__pageTitle
修复:
header.php三变量兜底mobile/header.php三变量兜底- 加 JS 覆盖
document.title
教训:见第 9.3 节。
D.3 案例:插件主内容区接管失效
症状:每日一文首页卡片被挤在 700px 宽。
排查:
- 读
log_list.php,没有处理layout_main_content覆盖 - 插件设置的
$__main_override被丢弃
修复:
<?php
$__main_override = '';
if (class_exists('Hook')) {
try {
$__main_override = trim(Hook::captureAction('layout_main_content', $ctx));
} catch (Throwable $e) { $__main_override = ''; }
}
$__hasMainOverride = ($__main_override !== '');
?>
<?php if ($__hasMainOverride): ?>
<div class="layout layout-single">
<div class="main-col" data-region="main">
<?= $__main_override ?>
</div>
</div>
<?php else: ?>
<!-- 正常三栏 -->
<?php endif; ?>
教训:见第 7.2 节。
D.4 案例:移动端插件页面 404
症状:手机上访问 /forum,显示 404 或博客首页。
排查:
- 读
mobile/router.php - 未匹配路由时错误地
exit(应为return)
修复:
/* 未匹配:return 让内核继续处理 */
return;
教训:见第 6.3 节。
D.5 案例:评论提交失败
症状:点击发表评论,返回 {"ok":false,"msg":"缺少 CSRF 令牌"}。
排查:
- 检查
echo_log.php评论表单 - 没有
Csrf::field()
修复:
<form method="post" action="<?= BLOG_URL ?>?action=comment_post">
<?= Csrf::field() ?>
<!-- ... -->
</form>
教训:见第 8.6 节。
附录 E · 给 AI 的指令模板
E.1 从零开发主题
请按 LiteLog 主题开发手册 v1.0 开发一个 XXX 风格的主题:
【附件:LiteLog 主题开发手册 v1.0 完整版.md】
需求:
- 风格:……
- 布局:三栏 / 单栏 / 其它
- 特殊功能:……
严格遵守本手册所有条款,特别是:
- 第 5 章「模板文件详解」的骨架
- 第 7.2 节「主内容区覆盖」
- 第 9 章「插件兼容」(钩子 + 三变量标题 + layout_main_content)
- 第 10 章「十条主题红线」
- 第 17 章「完整主题骨架」
- 第 18 章「交付前自检清单」
不要修改任何核心文件。完成后给出完整文件列表和每个文件的完整代码。
E.2 排查插件样式不加载
我的主题在使用 XXX 插件时,插件 CSS 不加载。
【附件:LiteLog 主题开发手册 v1.0 完整版.md】
主题 header.php 代码:……
主题 footer.php 代码:……
F12 Network 面板:……
请按第 9.1 节排查。
E.3 排查标题不对
我的主题在访问插件页面时,浏览器标签显示站点名,不是插件标题。
【附件:LiteLog 主题开发手册 v1.0 完整版.md】
主题 header.php 的 <title> 那行代码:……
主题 mobile/header.php 的 <title> 那行代码:……
插件视图顶部代码:……
请按第 9.3 节排查。
E.4 排查主内容区接管失效
我的主题在使用每日一文插件时,插件首页的卡片被挤在中间。
【附件:LiteLog 主题开发手册 v1.0 完整版.md】
主题 log_list.php 代码:……
主题 CSS 的 .layout 定义:……
请按第 7.2 节排查。
E.5 排错通用模板
我的主题出现以下症状:……
【附件:LiteLog 主题开发手册 v1.0 完整版.md】
主题目录结构:……
相关文件代码:……
浏览器 Console / Network 信息:……
请按以下顺序排查:
1. 先检查钩子是否齐全(第 9.1 节)
2. 再检查标题三变量(第 9.3 节)
3. 再检查主内容区覆盖(第 7.2 节)
4. 给出最小修改方案
5. 给出验证方法
如果需要诊断脚本,请直接给我一个完整的 `_diag_theme.php`。
附录 F · 更新日志
v1.0(2026-10-13)
初版发布,包含:
- 8 大部分
- 19 章
- 6 个附录
- 20 条铁律
- 5 个真实案例
- 主题诊断脚本
- 完整骨架
基于:LiteLog 内核 v1.2 + default 主题 v1.3.1 + 每日一文/论坛插件两轮修复战暴露出的主题层全部问题。
收尾:一句话总结
📌 "钩子齐全、三变量标题、主内容区覆盖、CSRF 表单、移动端 return。"
主题核心五原则
① 每个视图 includeheader/footer——不自建 HTML 骨架 ② **每个布局位置触发layout_*钩子**——让插件能注入 ③ 识别三种标题变量——$page_title/$title/$__pageTitle④ 处理layout_main_content覆盖——让插件能接管主内容区 ⑤ 评论表单加Csrf::field()——否则评论全废
主题三大安全底线
⑥ 用户输入过h()转义——但文章content除外 ⑦ 不硬编码 URL / 路径——用常量拼接 ⑧ 移动端router.php未匹配必须return
主题两个常用技巧
⑨ JS 覆盖document.title——标题终极方案 ⑩?force_view=mobile|desktop——两端对比测试
掌握这 10 条 + 二十条铁律 + 一个诊断脚本,99% 的主题能一次跑通。
本手册基于 LiteLog 内核 v1.2 + default 主题 v1.3.1 整理。
若内核继续更新,请同步更新第 4 章「主题 API」、第 7 章「布局系统」、第 9 章「插件兼容」、第 10 章「十条主题红线」、第 19 章「二十条铁律」五个章节。