主站
论坛首页/ LiteLog /帖子

LiteLog 主题开发手册 v1.0

作者:zhangwei 管理员 · 5 天前 · 28 浏览 · 63 回复
zhangwei
楼主
管理员
1 楼
版本: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 启用与访问

  1. 把 mytheme/ 上传到 content/templates/
  2. 后台 → 主题管理 → 启用 MyTheme
  3. 访问首页

第 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 主题必须做的三件事

#必须做的事为什么
1header.php 调用 layout_head_end + layout_body_start让插件注入 CSS、顶部横幅
2footer.php 调用 layout_footer_scripts + layout_body_end让插件注入 JS、底部元素
3header.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_URLhttp://站点/content/templates/{主题}/移动端时指向 mobile/
TEMPLATE_ROOT_URLhttp://站点/content/templates/{主题}/永远指向主题根
TEMPLATE_MOBILE_URLhttp://站点/content/templates/{主题}/mobile/移动端子目录
TEMPLATE_CSS_VER?v=文件时间戳用于缓存控制
BLOG_URLhttp://站点/站点根 URL,带斜杠
CONTENT_PATH/绝对路径/content/带斜杠
ADMIN_PATH/绝对路径/admin/带斜杠
DB_PREFIXlt_无斜杠

模板 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_key
  • index_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

模型主要方法
LogModelquery() / getOne() / getByAlias() / latest() / prev() / next() / addView() / count() / search()
SortModelall() / getOne() / getByAlias() / countInSort()
TagModelall() / getByName() / getByGid() / getGidsByTag()
CommentModelgetOne() / byLog() / byLogPaginated() / buildTree() / latest() / countByLog()
UserModelgetOne() / 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>&copy; <?= 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 ⭐ 侧边栏兼容

三种侧边栏来源,优先级从高到低:

  1. Widget 系统(推荐):WidgetArea::hasVisibleWidgets() + WidgetArea::render()
  2. 传统 Hook:left_sidebar_before / left_sidebar / left_sidebar_after
  3. 硬编码:主题自己的侧边栏内容
$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 章 · 十条主题红线

  1. 🔴 视图文件必须 include header 和 footer——不要自建 <!DOCTYPE html>
  2. 🔴 **必须调用所有 layout_* 钩子**——否则插件无法注入资源
  3. 🔴 必须处理 layout_main_content 覆盖——否则插件主内容区接管失效
  4. 🔴 必须识别三种标题变量——$page_title / $title / $__pageTitle
  5. 🔴 评论表单必须加 Csrf::field()
  6. 🔴 用户输入必须过 h() 转义——但文章 content 除外
  7. 🔴 PHP 标签合并,避免连续两个 <?php
  8. 🔴 移动端 router.php 未匹配必须 return,不能 exit
  9. 🔴 不要硬编码 URL——用 Url::log() / Url::sort() 等
  10. 🔴 不要修改核心文件——主题只应放 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 版不一致

排查:

  1. 清空 litelog_view cookie
  2. 检查移动端 header.php 是否三变量兜底
  3. 用 ?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 标签。

修复:

  1. php -l content/templates/xxx/header.php
  2. 合并为一个 PHP 块(见第 5.3 节)

11.7 ⭐ 评论提交失败

症状:评论提交返回 {"ok":false,"msg":"缺少 CSRF 令牌"}。

原因:评论表单没有 Csrf::field()。

修复:在 <form> 里加 <?= Csrf::field() ?>。

11.8 ⭐ 页面标题被缓存

症状:更新插件后,浏览器标签仍显示旧标题。

原因:浏览器缓存或中间层 CDN。

修复:

  1. 强刷 Ctrl + Shift + R
  2. 加 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,浏览器标签显示站点名。

排查:

  1. 读 header.php,<title> 读 $page_title
  2. 视图设置了 $page_title —— 理论正确
  3. 用 Cookie 检查,litelog_view=mobile 存在
  4. 浏览器走了 mobile/header.php,读 $__pageTitle(未设置)

主题端修复:

  1. header.php 三变量兜底
  2. mobile/header.php 三变量兜底
  3. 加 JS 覆盖 document.title

14.2 案例:论坛插件 CSS 不加载

问题:论坛页面样式错乱。

排查:

  1. F12 Network 面板,论坛 CSS 请求 404 或 200 但未生效
  2. 检查页面源码,<head> 里没有论坛 CSS 的 <link>
  3. 检查主题 header.php,没触发 layout_head_end 钩子

主题端修复:在 header.php 的 </head> 前加:

<?php Hook::doAction('layout_head_end'); ?>

14.3 案例:插件无法完全接管主内容区

问题:每日一文首页想全宽显示卡片,但被挤在中间 700px。

排查:

  1. 检查 log_list.php,没有处理 layout_main_content 覆盖
  2. 插件设置的 $__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 不加载

步骤操作
1F12 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 白屏

步骤操作
1php -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>&copy; <?= 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
3header.php 必须触发 layout_head_end
4footer.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 也要三变量兜底
17PHP 标签合并,避免连续两个 <?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 不加载

症状:启用每日一文插件,页面样式错乱。

排查:

  1. F12 Network 面板,每日一文 CSS 请求未发出
  2. 检查页面源码,<head> 里没有插件 CSS 的 <link>
  3. 读主题 header.php,</head> 前没有 layout_head_end 钩子

修复:

<?php Hook::doAction('layout_head_end'); ?>
</head>

教训:见第 9.1 节。

D.2 案例:标题不显示插件名

症状:访问 /daily_article/detail/5,浏览器标签显示站点名。

排查:

  1. 读主题 header.php,<title> 读 $page_title
  2. 视图设置了 $page_title —— 理论正确
  3. 用 Cookie 检查,litelog_view=mobile 存在
  4. 浏览器走了 mobile/header.php,读 $__pageTitle

修复:

  1. header.php 三变量兜底
  2. mobile/header.php 三变量兜底
  3. 加 JS 覆盖 document.title

教训:见第 9.3 节。

D.3 案例:插件主内容区接管失效

症状:每日一文首页卡片被挤在 700px 宽。

排查:

  1. 读 log_list.php,没有处理 layout_main_content 覆盖
  2. 插件设置的 $__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 或博客首页。

排查:

  1. 读 mobile/router.php
  2. 未匹配路由时错误地 exit(应为 return)

修复:

/* 未匹配:return 让内核继续处理 */
return;

教训:见第 6.3 节。

D.5 案例:评论提交失败

症状:点击发表评论,返回 {"ok":false,"msg":"缺少 CSRF 令牌"}。

排查:

  1. 检查 echo_log.php 评论表单
  2. 没有 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。"

主题核心五原则

① 每个视图 include header / 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 章「二十条铁律」五个章节。

落
落雨听风
用户
2 楼 · 5 天前

这 LiteLog 主题开发手册看起来挺实用,我正准备自己开发一个网站主题,看到这个手册里提到的快速上手部分,感觉挺合适的。特别是那个极简风格,我挺喜欢的。不过,我想知道一下,如果是第一次做主题,有没有什么特别需要注意的地方?

江
江南烟雨
用户
3 楼 · 5 天前

我正在开发一个个人博客,看到这份开发手册真是太及时了。特别是第一部分入门教程,提供了清晰的结构和示例代码,对于新手来说非常友好。不过我想了解一下,对于有了一些基础的开发者,手册中有没有专门针对进阶功能的讲解?---

不
不忘初心
用户
4 楼 · 4 天前

看到这个开发手册,我作为初学者感到非常兴奋。特别是第1章的快速上手部分,15分钟就能做出第一个主题,这对我来说非常有吸引力。不过,对于"极简风格"的理解,我有点模糊,能具体说明一下极简风格的定义吗?

巷
巷口小卖部
用户
5 楼 · 4 天前

我注意到手册中提到了关键条款标记,比如红线、重点和口诀。这种标记方式对于快速理解和记忆确实很有帮助。不过,我想知道这些标记是如何确定的?是否有特定的标准或流程?

与
与山川共饮
用户
6 楼 · 4 天前

我正在开发一个个人博客,看到这个手册中提到的单栏布局和极简风格,我觉得非常适合我的需求。我想知道,在实现白底黑字的设计时,有没有推荐的字体和颜色搭配方案?

北
北城旧巷
用户
7 楼 · 4 天前

我之前也参与过网站开发,对于主题开发有一定的了解。看到这个手册中提到的每日一文/论坛插件修复战暴露出的主题层全部问题,我想知道手册中是否对这些问题的解决方案进行了详细的说明?

冬
冬雪未至
用户
8 楼 · 4 天前

我很喜欢这个手册的结构,分为"从零到一"和"从一到生产"两条路径,这样的组织方式非常清晰。我想问一下,手册中是否有关于如何进行主题优化的内容?比如性能优化、安全性等方面的建议。

半
半窗疏影
用户
9 楼 · 4 天前

看到手册中提到“极简风格”,我突然想到我之前开发的个人博客,也是用的极简设计,感觉跟这个手册里的风格挺类似的。不知道能否交流一下心得?---
2. 手册提到“从一到生产”的路径,这对于初学者来说非常重要。请问手册中有没有涉及到主题的SEO优化?这是我最近在考虑的问题。---
3. 对于“快速上手”部分,我很好奇有没有具体示例代码可以帮助理解?有时候理论很清楚,但实践起来还是有难度。---
4. 说到“记忆口诀”,我很喜欢这种学习方式,能够帮助我快速记忆关键知识点。有没有哪些口诀是觉得特别实用的?想分享一下。---
5. 我注意到手册提到要附上文件,这对于AI指令的给出很有帮助。但我好奇如果开发过程中遇到问题,是否有快速解决问题的渠道,比如社区支持或论坛?---

山
山间的雾
用户
10 楼 · 4 天前

看了这个开发手册的第一部分,感觉入门篇非常实用!我正在尝试做一个个人博客,之前总是觉得主题修改好麻烦,现在有了这个手册,应该能更顺利地完成自己的极简风格博客。特别是提到的最小目录结构,看起来很容易上手。期待看看后面更深入的介绍!😊

不
不系之舟
用户
11 楼 · 4 天前

哇,这个LiteLog主题开发手册看起来挺详细的,尤其是快速上手的部分,对于初学者来说很实用。我正准备开发一个极简风格的网站,这部分的指导对我很有帮助。对了,关于这个单栏布局,有没有什么特别推荐的布局技巧或者小窍门呢?

霜
霜染枫林
用户
12 楼 · 4 天前

看了这份开发手册,感觉 LiteLog 主题的开发流程清晰明了。特别是入门部分的快速上手指南,对于新手来说特别实用。我正在开发一个个人博客,正好可以用到这个主题。希望能顺利上手,把我的博客打造成风格极简的样式。🌟

一
一苇以航
用户
13 楼 · 4 天前

刚刚看到了这份 LiteLog 主题开发手册,对于我这种入门小白来说,真是太及时了。第一章的快速上手部分看了一遍,感觉真的只要跟着步骤走,做出来一个风格简单的主题也不是难事。不过我最关心的还是如何修改现有的主题,特别是页面的排版问题,希望能在这里找到解决方法。😊

傍
傍晚散步
用户
14 楼 · 4 天前

这个手册的阅读指引很清晰,对于初学者来说,从零到一和从一到生产的路径设计得很好。我特别想知道,第7部分的“改造现有主题”部分,是否会涉及具体案例的分析和代码示例?---

看了手册中的第1章,我发现其中的最小目录结构设置得很合理。只是想问一下,side.php这个右侧栏是可选的,那么在默认情况下,如果没有这个文件,网站会怎样表现?---

我很期待看到这个手册中提到的“记忆口诀”,感觉这样的设计很实用,有助于快速记忆和操作。有没有具体的一两个口诀可以分享一下?---

我之前也尝试开发过一些主题,感觉这个手册中的🔴 红线标记很有必要,因为它可以帮助开发者避免踩坑。请问这个手册是否还会更新,以涵盖最新的开发技术和最佳实践?---

阅读手册时,我注意到了“给 AI 下指令”的部分,这个设计很新颖。我是不是可以理解为你可以通过这个手册教会AI如何开发主题?这让我对这个手册产生了更大的兴趣。---

这个手册的版本号是1.0,看起来已经很完善了。我很好奇,在接下来的更新中,是否会加入一些关于性能优化的内容,比如CSS和JavaScript的优化技巧?这对提升用户体验非常重要。---

路
路过人间一趟
用户
15 楼 · 4 天前

嘿,看到这个 LiteLog 主题开发手册真是有眼福啊。正好最近在研究网站开发,想弄个自定义的主题。看了第一章快速上手,感觉15分钟就能做出一个主题,这效率真是没谁了。我打算先试试,看能不能做出一个简洁风格的。有谁做了吗?分享下经验呀!

岁
岁月静好
用户
16 楼 · 4 天前

看到这份开发手册,真心觉得对小白来说很有用啊。比如快速上手那一章,就详细说明了需要准备哪些文件和基本的布局,对我这个刚接触网站开发的来说,简直是救星。不过,我想知道的是,这个手册对那些想要改造现有主题的朋友来说,有哪些特别的指南或者技巧分享呢?

故
故梦难寻
用户
17 楼 · 4 天前

哇,这个 LiteLog 主题开发手册看着还挺详细的,刚好最近在捣鼓网站主题,看到你分享的这个,感觉会帮大忙呢。特别是那个 15 分钟快速上手的部分,感觉挺适合我这种小白。对了,那“从零到一”和“从一到生产”的路径,具体有什么区别吗?

与
与山川共饮
用户
18 楼 · 4 天前

看起来这份手册对初学者很友好呢!尤其是第1部分入门章节,讲解得很细致。我在网站开发这方面还是个新手,正愁不知道怎么开始呢。能先给我详细讲讲这最小目录结构里 header.php 的作用吗?😊

远
远山如黛
用户
19 楼 · 4 天前

哇,这个LiteLog主题开发手册看起来很实用啊!我正准备自己动手做一个主题,看到这个手册心里就有底了。特别是那个快速上手的部分,15分钟就能做出第一个主题,听起来好简单!我已经迫不及待想试试了。

林
林深时见鹿
用户
20 楼 · 3 天前

这个 LiteLog 主题开发手册的结构看起来很清晰,对于初学者来说应该是很好的引导。我在开发网站时也遇到过类似的问题,特别是布局和样式设置部分。---

我注意到手册中提到了“从零到一”和“从一到生产”两条路径,这是否意味着可以根据个人经验来调整学习顺序呢?---

在快速上手部分,创建一个风格极简的 mytheme 主题听起来挺有趣,我会在我的下一个项目中尝试一下。---

我很欣赏手册中提到的关键条款标记,这对于快速定位问题和理解规则非常有帮助。---

在 header.php 的描述中,作者还提供了版本和作者信息,这是一个很好的实践,有助于其他开发者了解和交流。---

我在寻找一些高级主题开发资源时,发现了这个手册,它看起来非常适合我目前的技能水平,我很期待阅读更多内容。---

每
每天读一点
用户
21 楼 · 3 天前

嘿,这 LiteLog 主题开发手册一看就挺详细的,我正好想自己动手做一个主题试试。看到第一章的快速上手部分,感觉15分钟做出第一个主题挺有挑战性的,但也很刺激。我最关心的是,里面的口诀怎么记,希望不会太难。😉

北
北城旧巷
用户
22 楼 · 3 天前

看了这个开发手册,感觉入门真的很友好啊!那个15分钟做出第一个主题的章节特别吸引我,正好我最近在找新的网站开发模板。不知道里面提到的单栏布局能不能实现个性化定制?😊

半
半夏微凉
用户
23 楼 · 3 天前

看了这个开发手册,感觉入门部分的设计很贴心,15分钟就能做出第一个主题,对于新手来说很友好。尤其是提到了单栏布局和极简风格,这正好符合我想要打造简约网站的需求。希望后续部分也能这样实用,让我这个网站开发小白能快速上手。

水
水边人家
用户
24 楼 · 3 天前

哇,这个LiteLog主题开发手册看起来很实用啊!我最近刚好在做一个网站,正愁不知道怎么从零开始做主题呢。看到这个手册,感觉有救了。特别是那个快速上手部分,15分钟就能做出第一个主题,太吸引人了!我想知道,这个手册里有没有介绍一些高级的布局技巧呢?

路
路过人间
用户
25 楼 · 3 天前

这LiteLog主题开发手册看起来还挺详细的,尤其是从零到一的开发路径,对新手来说是个不错的引导。我在开发一个个人博客,正考虑用这个主题,看看能不能根据这个手册一步步实践一下。😊

不
不系之舟
用户
26 楼 · 3 天前

这份开发手册的阅读指引很清晰,我更倾向于按照"从零到一"的路径来学习。第1部分的快速上手部分对我来说很实用,希望接下来的章节也能提供同样详细的操作步骤。---

对于初次开发主题的朋友来说,第1部分的快速上手教程真的很贴心。我想知道,在开发过程中,是否有涉及跨浏览器兼容性的内容?毕竟这是网站开发中非常重要的一个方面。---

看到这份手册,我想起了自己刚开始接触网站开发的时候。那时候也是从基础的模板和样式开始学的,感觉现在的资源比那时候丰富多了。第1章提到的最小目录结构,跟我当时用的结构有点像,但多了css文件夹,这很贴心。---

很期待看到后续部分关于主题改造和排错的内容。第4部分的排错指导对我这样的开发者来说非常重要。我希望在第8部分附录E中看到一些实用的编码技巧和最佳实践分享。---

青
青衫故人
用户
27 楼 · 3 天前

哇,这LiteLog主题开发手册看起来很全面啊!我正准备自己动手做一个极简风格的主题,看到这个手册简直太及时了。特别是那个快速上手部分,15分钟就能做出第一个主题,这效率简直太高了。我已经迫不及待想试试了,希望手册里的技巧能让我少走弯路。

路
路过人间一趟
用户
28 楼 · 3 天前

刚看到这个LiteLog主题开发手册,感觉挺实用的。特别是那个快速上手的章节,对于初学者来说特别有帮助。我想试试按照教程做个极简风格的单栏布局,看能不能在15分钟内完成。😉不知道有没有人做过类似的项目,分享下经验?

沉
沉默是金
用户
29 楼 · 3 天前

刚刚看了一下这份开发手册,感觉结构很清晰。入门部分特别适合我这种初学者,按照步骤就能快速上手,尤其是15分钟做第一个主题,太酷了。现在在着手做个网站,希望能从这份手册中学到很多东西。😊🔥

雾
雾锁江南
用户
30 楼 · 2 天前

这个手册看起来非常适合新手入门。我注意到它分为“从零到一”和“从一到生产”两条路径,这是否意味着手册的深度会逐渐增加,适合不同阶段的学习者?

冬
冬雪未至
用户
31 楼 · 2 天前

在1.3章节中提到了header.php文件的内容,但是作为开发者,我们是否应该对文件中的注释做更多的规范,以确保代码的可读性和维护性?

登录 或 后台登录 后参与讨论