LiteLog 主题开发规范 v1.0

2026-10-05 分类:Lite 作者:zhangwei 浏览 48

只需阅读这一个文件,就能开发出一个完整可用的 LiteLog 主题。
本文档覆盖目录结构、文件约定、可用变量、API 函数、钩子清单、静态资源、完整示例、常见问题。


目录

  1. 快速开始
  2. 目录结构
  3. 头部注释规范
  4. 必需文件清单
  5. 可用变量速查
  6. 可用 API 速查
  7. 可用钩子速查
  8. 模板路由说明
  9. 静态资源引用
  10. 完整示例
  11. 开发规范
  12. 调试技巧
  13. 常见问题

一、快速开始

最小可运行主题(4 个文件)

在 content/templates/mytheme/ 下创建:

header.php

<?php
/**
 * Theme Name: My Theme
 * Description: 我的第一个主题
 * Version: 1.0.0
 * Author: 你的名字
 */
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
</head>
<body>
<header>
  <a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
</header>
<main>

log_list.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
include View::resolve('header');
?>
<?php foreach ($logs as $log): ?>
  <article>
    <h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
    <p><?= h($log['excerpt']) ?></p>
  </article>
<?php endforeach; ?>
<?= $pageHtml ?? '' ?>
<?php include View::resolve('footer'); ?>

echo_log.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<article>
  <h1><?= h($log['title']) ?></h1>
  <div><?= $log['content'] ?></div>
</article>
<?php include View::resolve('footer'); ?>

footer.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<footer>&copy; <?= date('Y') ?> <?= h(Option::get('site_title')) ?></footer>
</body>
</html>

启用:后台 → 主题 → 找到 "My Theme" → 点"启用此主题"。


二、目录结构

标准结构

content/templates/主题名/
├── header.php          ← 必需,页头
├── footer.php          ← 必需,页尾
├── log_list.php        ← 必需,文章列表(首页/分类/标签/作者/归档)
├── echo_log.php        ← 必需,文章详情
├── page.php            ← 可选,自定义页面
├── 404.php             ← 可选,404 页面
├── side.php            ← 可选,侧边栏
├── module.php          ← 可选,公共模块
├── plugins.php         ← 可选,主题级插件调用
├── options.php         ← 可选,主题后台设置项
├── callback.php        ← 可选,主题生命周期回调
├── preview.jpg         ← 可选,主题预览图(900×600 推荐)
├── css/
│   └── style.css
├── js/
│   └── theme.js
└── images/
    └── logo.png

命名规则

项规则示例
目录名小写字母、数字、下划线、短横线nebula、my-theme
PHP 文件与内核约定一致(见上文)header.php
CSS/JS建议放 css/、js/ 目录css/style.css

禁止事项

  • ❌ 不要用中文、空格命名
  • ❌ 不要修改 LiteLog 内核文件
  • ❌ 不要把 index.php 放进主题目录

三、头部注释规范

header.php 最顶部必须包含主题信息:

<?php
/**
 * Theme Name: 主题显示名称
 * Description: 一句话描述主题风格
 * Version: 1.0.0
 * Author: 作者名
 * Author URI: https://example.com
 */

字段说明

字段必填说明
Theme Name✅后台显示的名称(也接受 Name)
Description否主题描述
Version否版本号
Author否作者名
Author URI否作者主页(也接受 Author URL)

格式要求

  • 冒号用英文 :
  • 注释块必须在文件最前面
  • 用 /** ... / 或 / ... */
  • 描述中不要含 */

四、必需文件清单

4.1 header.php —— 页头

职责:<!DOCTYPE html> 到主内容区之前的所有内容。

必须包含:

  • if (!defined('LITELOG_ROOT')) exit('error!');(安全判断)
  • 顶部注释头
  • HTML <head>(title、meta、CSS 引用)
  • 顶部导航区
  • <body> 起始

核心变量(可选注入):

变量说明
$page_title页面标题,模板可在 include 前自行赋值
$log文章详情页才有,其他页面未定义

核心钩子:

  • Hook::doAction('header_meta') —— meta 标签之后
  • Hook::doAction('template_head') —— <head> 内 CSS 之后
  • Hook::doAction('header_after') —— </header> 之后
  • Hook::doAction('before_content') —— 主内容区之前

推荐模板:

<?php
/**
 * Theme Name: Your Theme
 * Description: ...
 * Version: 1.0.0
 * Author: ...
 */
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<?php Hook::doAction('template_head'); ?>
</head>
<body>
<header class="site-header">
  <a href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
  <nav>
    <a href="<?= BLOG_URL ?>">首页</a>
    <?php foreach (SortModel::all() as $s): ?>
      <a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a>
    <?php endforeach; ?>
  </nav>
</header>
<?php Hook::doAction('header_after'); ?>
<?php Hook::doAction('before_content'); ?>
<main class="container">

4.2 footer.php —— 页尾

职责:</main> 之后的收尾。

核心钩子:

  • Hook::doAction('after_content') —— 主内容区之后
  • Hook::doAction('footer_before') —— <footer> 之前
  • Hook::doAction('template_footer') —— <footer> 内部,脚本之前

推荐模板:

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="site-footer">
  &copy; <?= date('Y') ?> <?= h(Option::get('site_title')) ?>
</footer>
<?php Hook::doAction('template_footer'); ?>
</body>
</html>

4.3 log_list.php —— 文章列表

职责:首页、分类页、标签页、作者页、归档页共用。

上下文变量:

变量说明
$logs数组,文章列表,见 5.1
$pageHtml字符串,分页 HTML(如为空说明只有一页)
$pageType字符串,index / sort / tag / author / record
$sort分类页才有,见 5.2
$tag标签页才有,见 5.3
$author作者页才有,见 5.4
$recordYear / $recordMonth归档页才有

核心钩子:

  • Hook::doAction('list_page_before', $pageType)
  • Hook::doAction('list_item_before', $log, $pageType)(循环内)
  • Hook::doAction('list_item_after', $log, $pageType)(循环内)
  • Hook::doAction('list_page_after', $pageType)

推荐模板:

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
if (!empty($sort))       $page_title = $sort['sortname'] . ' - ' . $page_title;
if (!empty($tag))        $page_title = '标签:' . $tag['tagname'] . ' - ' . $page_title;
if (!empty($author))     $page_title = ($author['nickname'] ?: $author['username']) . ' 的文章';
if (!empty($recordYear)) $page_title = "{$recordYear}年{$recordMonth}月归档";
include View::resolve('header');
?>
<div class="layout">
  <div class="main-col">
    <?php Hook::doAction('list_page_before', $pageType ?? 'index'); ?>

    <?php if (empty($logs)): ?>
      <p class="empty">暂无文章</p>
    <?php else: foreach ($logs as $log): ?>
      <?php Hook::doAction('list_item_before', $log, $pageType ?? 'index'); ?>
      <article class="post-item">
        <h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
        <div class="meta">
          <span><?= h($log['date_fmt']) ?></span>
          <span>浏览 <?= (int)$log['views'] ?></span>
          <span>评论 <?= (int)$log['comnum'] ?></span>
        </div>
        <p class="excerpt"><?= h($log['excerpt']) ?></p>
        <a href="<?= h($log['url']) ?>">阅读全文 →</a>
      </article>
      <?php Hook::doAction('list_item_after', $log, $pageType ?? 'index'); ?>
    <?php endforeach; endif; ?>

    <?= $pageHtml ?? '' ?>

    <?php Hook::doAction('list_page_after', $pageType ?? 'index'); ?>
  </div>
  <aside class="side-col">
    <?php include View::resolve('side'); ?>
  </aside>
</div>
<?php include View::resolve('footer'); ?>

4.4 echo_log.php —— 文章详情

上下文变量:

变量说明
$log数组,文章对象,见 5.1,含额外字段
$prev数组或 null,上一篇(含 gid、title、url)
$next数组或 null,下一篇
$comments数组,评论树(含 children),见 5.5

$log 的额外字段(控制器补充):

字段说明
url文章完整 URL
date_fmt格式化的日期
tags标签数组(每项含 tid、tagname)
sortname分类名
sorturl分类页 URL
author_name作者昵称

核心钩子:

  • Hook::doAction('log_view_before', $log)
  • Hook::doAction('log_content_before', $log)
  • Hook::doAction('log_content_after', $log)
  • Hook::doAction('log_view_after', $log)
  • Hook::doAction('comment_display_before', $comment)(每条评论)
  • Hook::doAction('comment_display_after', $comment)
  • Hook::doAction('comment_form_before', $log)
  • Hook::doAction('comment_form_fields', $log)
  • Hook::doAction('comment_form_after', $log)

推荐模板:

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<div class="layout">
  <div class="main-col">
    <?php Hook::doAction('log_view_before', $log); ?>

    <article class="post-single">
      <h1><?= h($log['title']) ?></h1>
      <div class="meta">
        <span><?= h($log['date_fmt']) ?></span>
        <?php if ($log['sortname']): ?>
          <span>分类:<a href="<?= h($log['sorturl']) ?>"><?= h($log['sortname']) ?></a></span>
        <?php endif; ?>
        <span>作者:<?= h($log['author_name']) ?></span>
        <span>浏览 <?= (int)$log['views'] ?></span>
      </div>
      <?php if ($log['tags']): ?>
        <div class="tags">
          <?php foreach ($log['tags'] as $t): ?>
            <a href="<?= Url::tag($t['tagname']) ?>">#<?= h($t['tagname']) ?></a>
          <?php endforeach; ?>
        </div>
      <?php endif; ?>

      <div class="content">
        <?php Hook::doAction('log_content_before', $log); ?>
        <?= $log['content'] ?>
        <?php Hook::doAction('log_content_after', $log); ?>
      </div>
    </article>

    <?php Hook::doAction('log_view_after', $log); ?>

    <nav class="post-nav">
      <?php if ($prev): ?><a href="<?= h($prev['url']) ?>">← <?= h($prev['title']) ?></a><?php endif; ?>
      <?php if ($next): ?><a href="<?= h($next['url']) ?>"><?= h($next['title']) ?> →</a><?php endif; ?>
    </nav>

    <section id="comment" class="comment-section">
      <h3>评论 (<?= CommentModel::countByLog($log['gid']) ?>)</h3>
      <?php $render = function ($list, $depth = 0) use (&$render) {
          foreach ($list as $c) {
              Hook::doAction('comment_display_before', $c);
          ?>
            <div class="comment" style="margin-left:<?= $depth * 24 ?>px">
              <strong><?= h($c['poster']) ?></strong>
              <span><?= datetimeFmt($c['date']) ?></span>
              <div><?= nl2br(h($c['comment'])) ?></div>
            </div>
          <?php
              Hook::doAction('comment_display_after', $c);
              if (!empty($c['children'])) $render($c['children'], $depth + 1);
          }
      };
      $render($comments); ?>

      <?php if ($log['allow_remark'] !== 'n'): ?>
        <?php Hook::doAction('comment_form_before', $log); ?>
        <form method="post" action="<?= BLOG_URL ?>?action=comment_post">
          <input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
          <input type="text" name="poster" placeholder="昵称 *" required>
          <input type="email" name="email" placeholder="邮箱">
          <input type="url" name="url" placeholder="网站">
          <textarea name="comment" rows="5" placeholder="说点什么..." required></textarea>
          <?php Hook::doAction('comment_form_fields', $log); ?>
          <button type="submit">发表评论</button>
        </form>
        <?php Hook::doAction('comment_form_after', $log); ?>
      <?php endif; ?>
    </section>
  </div>
  <aside class="side-col">
    <?php include View::resolve('side'); ?>
  </aside>
</div>
<?php include View::resolve('footer'); ?>

4.5 可选文件

文件用途触发时机
page.php自定义页面(关于、留言板)通过 View::display('page', ['title'=>..., 'content'=>...]) 主动调用
404.php404 页面文章/分类不存在时自动调用
side.php侧边栏log_list.php / echo_log.php 中 include View::resolve('side')
module.php公共小组件(如"最新文章")主题内 include View::resolve('module')
plugins.php主题级插件调用(启用后自动加载)内核 loadActivePlugins() 检测后 require
options.php主题后台设置项后台主题管理页
callback.php生命周期回调主题启用/停用时

五、可用变量速查

5.1 文章对象 $log

字段类型说明
gidint文章 ID
titlestring标题
dateint时间戳
contentstring渲染后的 HTML(直接输出,不要 h())
content_mdstringMarkdown 源码(一般不用)
excerptstring摘要(可能为空)
aliasstringURL 别名
authorint作者 UID
sortidint分类 ID
typestringblog / page
viewsint浏览数
comnumint评论数
topstring是否置顶:y / n
hidestring是否隐藏:y / n
allow_remarkstring是否允许评论
以下字段仅详情页有
urlstring完整 URL
date_fmtstringY-m-d 格式
tagsarray标签数组(每项含 tid、tagname)
sortnamestring分类名
sorturlstring分类页 URL
author_namestring作者昵称

用法:

<?= h($log['title']) ?>           // 标题需要转义
<?= $log['content'] ?>            // 内容直接输出(已渲染)
<?= (int)$log['views'] ?>         // 数字用 (int) 转

5.2 分类对象 $sort

字段类型说明
sidint分类 ID
sortnamestring分类名
aliasstringURL 别名
pidint父分类 ID(0 为顶级)
descriptionstring描述

5.3 标签对象 $tag

字段类型说明
tidint标签 ID
tagnamestring标签名

5.4 用户对象 $author

字段类型说明
uidint用户 ID
usernamestring用户名
nicknamestring昵称
emailstring邮箱
rolestringadmin / writer / visitor

5.5 评论对象 $comment

字段类型说明
cidint评论 ID
gidint所属文章 ID
pidint父评论 ID(0 为顶级)
dateint时间戳
posterstring昵称
commentstring评论内容(未渲染,需要 h() + nl2br())
emailstring邮箱
urlstring网站
childrenarray子评论数组(控制器构建,含嵌套)

5.6 全局配置

通过 Option::get() 读取:

键名说明默认
site_title站点名称My LiteLog
site_description站点描述—
site_key站点关键词—
index_lognum首页文章数10
current_template当前主题名default
url_rewrite伪静态开关n

六、可用 API 速查

6.1 常量

常量示例值说明
BLOG_URLhttps://example.com/站点根 URL,末尾带斜杠
TEMPLATE_URLhttps://example.com/content/templates/nebula/当前主题 URL,末尾带斜杠
CONTENT_PATH/www/site/content/content 目录绝对路径
DB_PREFIXlt_数据表前缀

6.2 通用函数

函数说明
h($str)HTML 转义输出(所有用户输入都要经过)
redirect($url)302 跳转并退出
makeAlias($str)生成 URL 友好字符串
dateFmt($ts)时间戳 → Y-m-d
datetimeFmt($ts)时间戳 → Y-m-d H:i
stripTags($html, $len)去标签 + 截断
getExcerpt($html, $len)提取摘要
isLoggedIn()是否已登录
checkLogin()未登录则跳转登录页
pagination($page, $total, $perPage)生成分页 HTML
getPageNumber()获取当前页码(?page=)

6.3 URL 生成

方法说明示例输出
Url::log($gid, $alias)文章 URLhttps://example.com/post/123.html
Url::sort($sid, $alias)分类 URLhttps://example.com/sort/tech
Url::tag($name)标签 URLhttps://example.com/tag/php
Url::author($uid)作者 URLhttps://example.com/author/1
Url::record($date)归档 URLhttps://example.com/record/2026-10

6.4 数据模型

LogModel(文章)

LogModel::query($page, $perPage, $cond, &$total);  // 分页查询
LogModel::count();                                  // 总文章数
LogModel::getOne($gid);
LogModel::getByAlias($alias);
LogModel::latest($limit = 10);                     // 最新 N 篇
LogModel::prev($gid);
LogModel::next($gid);

SortModel(分类)

SortModel::all();                        // 所有分类
SortModel::getOne($sid);
SortModel::getByAlias($alias);
SortModel::countInSort($sid);            // 分类下的文章数

TagModel(标签)

TagModel::all();
TagModel::getByName($name);
TagModel::getByGid($gid);

CommentModel(评论)

CommentModel::byLog($gid);
CommentModel::countByLog($gid);
CommentModel::latest($limit = 10);

UserModel(用户)

UserModel::getOne($uid);
UserModel::getByUsername($username);

LinkModel(友链)

LinkModel::all();

6.5 配置:Option

Option::get('site_title', '默认值');  // 读取(带默认)
Option::set('key', 'value');          // 写入
Option::all();                         // 所有 autoload=y 的配置

6.6 视图:View

// 解析主题文件路径(自动回退到 default 主题)
$path = View::resolve('header');

// 渲染模板并传入数据
View::display('page', ['title' => '关于', 'content' => '...']);

6.7 数据库

global $DB;
$rows = $DB->fetchAll("SELECT * FROM " . DB_PREFIX . "blog WHERE hide='n' LIMIT 10");
$row  = $DB->fetchOne("SELECT * FROM " . DB_PREFIX . "blog WHERE gid=?", [$gid]);
$cnt  = $DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "comment");

七、可用钩子速查

完整钩子清单见《LiteLog 插件开发规范》附录,此处只列主题文件里必须调用的钩子。

7.1 header.php 中

<?php Hook::doAction('header_meta'); ?>       <!-- <meta> 之后 -->
<?php Hook::doAction('template_head'); ?>     <!-- CSS 之后 -->
<?php Hook::doAction('header_after'); ?>      <!-- </header> 之后 -->
<?php Hook::doAction('before_content'); ?>    <!-- 主内容区之前 -->

7.2 footer.php 中

<?php Hook::doAction('after_content'); ?>     <!-- 主内容区之后 -->
<?php Hook::doAction('footer_before'); ?>     <!-- <footer> 之前 -->
<?php Hook::doAction('template_footer'); ?>   <!-- <footer> 内部 -->

7.3 log_list.php 中

<?php Hook::doAction('list_page_before', $pageType); ?>
  <?php Hook::doAction('list_item_before', $log, $pageType); ?>
  <?php Hook::doAction('list_item_after',  $log, $pageType); ?>
<?php Hook::doAction('list_page_after',  $pageType); ?>

7.4 echo_log.php 中

<?php Hook::doAction('log_view_before', $log); ?>
<?php Hook::doAction('log_content_before', $log); ?>
<?php Hook::doAction('log_content_after', $log); ?>
<?php Hook::doAction('log_view_after', $log); ?>

<!-- 评论循环内 -->
<?php Hook::doAction('comment_display_before', $comment); ?>
<?php Hook::doAction('comment_display_after',  $comment); ?>

<!-- 评论表单 -->
<?php Hook::doAction('comment_form_before', $log); ?>
<?php Hook::doAction('comment_form_fields', $log); ?>
<?php Hook::doAction('comment_form_after', $log); ?>

7.5 side.php 中

<?php Hook::doAction('sidebar_before'); ?>
<?php Hook::doAction('sidebar_after'); ?>

7.6 自定义钩子(主题自己提供)

主题可以提供额外钩子给插件用:

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

约定:自定义钩子名建议 主题名_xxx 前缀,避免与内核冲突。


八、模板路由说明

8.1 哪些 URL 走哪个模板

URL 类型触发控制器使用模板
/(首页)LogController::displaylog_list.php
/post/123.htmlLogController::displayContentecho_log.php
/sort/techSortController::displaylog_list.php
/tag/phpTagController::displaylog_list.php
/author/1AuthorController::displaylog_list.php
/record/2026-10RecordController::displaylog_list.php
不存在—404.php

判断当前页类型:在 log_list.php 里读 $pageType:

<?php if (($pageType ?? '') === 'index'): ?>
  <!-- 首页专属内容 -->
<?php endif; ?>

8.2 自定义页面

用 View::display('page', [...]) 主动调用:

View::display('page', [
    'title'   => '关于我',
    'content' => '<p>这是关于页面</p>',
]);

8.3 主题切换时的回退

如果当前主题缺某个文件(如 page.php),View::resolve() 自动回退到 default 主题的同名文件。所以你的主题不需要实现所有文件。


九、静态资源引用

9.1 CSS / JS

方法 1:用 TEMPLATE_URL(推荐)

<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<script src="<?= TEMPLATE_URL ?>js/theme.js"></script>

方法 2:用相对路径

<link rel="stylesheet" href="content/templates/mytheme/css/style.css">

禁止:直接用绝对路径(如 /content/templates/xxx/),会导致装在子目录时失效。

9.2 图片

<img src="<?= TEMPLATE_URL ?>images/logo.png" alt="Logo">

9.3 用户上传

<img src="<?= BLOG_URL ?>content/uploads/2026/10/xxx.jpg">

或直接输出文章内容里的 <img>($log['content'] 已包含完整 URL)。


十、完整示例

最小但完整的主题:单栏布局 + 深色模式 + 阅读进度条。

content/templates/demo/header.php

<?php
/**
 * Theme Name: Demo
 * Description: 最小可用的 LiteLog 主题示例
 * Version: 1.0.0
 * Author: 教程
 */
if (!defined('LITELOG_ROOT')) exit('error!');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($page_title ?? Option::get('site_title')) ?></title>
<meta name="keywords" content="<?= h(Option::get('site_key')) ?>">
<meta name="description" content="<?= h(Option::get('site_description')) ?>">
<?php Hook::doAction('header_meta'); ?>
<link rel="stylesheet" href="<?= TEMPLATE_URL ?>css/style.css">
<?php Hook::doAction('template_head'); ?>
</head>
<body>
<div class="wrap">
  <header class="head">
    <a class="brand" href="<?= BLOG_URL ?>"><?= h(Option::get('site_title')) ?></a>
    <nav class="nav">
      <a href="<?= BLOG_URL ?>">首页</a>
      <?php foreach (SortModel::all() as $s): ?>
        <a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a>
      <?php endforeach; ?>
    </nav>
  </header>
<?php Hook::doAction('header_after'); ?>
<?php Hook::doAction('before_content'); ?>
<main class="main">

content/templates/demo/footer.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
</main>
<?php Hook::doAction('after_content'); ?>
<?php Hook::doAction('footer_before'); ?>
<footer class="foot">
  &copy; <?= date('Y') ?> <?= h(Option::get('site_title')) ?>
</footer>
</div>
<?php Hook::doAction('template_footer'); ?>
</body>
</html>

content/templates/demo/log_list.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = Option::get('site_title');
if (!empty($sort))   $page_title = $sort['sortname'] . ' - ' . $page_title;
if (!empty($tag))    $page_title = '标签:' . $tag['tagname'] . ' - ' . $page_title;
if (!empty($author)) $page_title = ($author['nickname'] ?: $author['username']) . ' 的文章';
include View::resolve('header');
?>
<div class="layout">
  <div class="col-main">
    <?php if (empty($logs)): ?>
      <p>暂无文章</p>
    <?php else: foreach ($logs as $log): ?>
      <article class="item">
        <h2><a href="<?= h($log['url']) ?>"><?= h($log['title']) ?></a></h2>
        <div class="meta"><?= h($log['date_fmt']) ?> · <?= (int)$log['views'] ?> 浏览</div>
        <p class="excerpt"><?= h($log['excerpt']) ?></p>
        <a href="<?= h($log['url']) ?>">阅读全文 →</a>
      </article>
    <?php endforeach; endif; ?>
    <?= $pageHtml ?? '' ?>
  </div>
  <aside class="col-side">
    <?php include View::resolve('side'); ?>
  </aside>
</div>
<?php include View::resolve('footer'); ?>

content/templates/demo/echo_log.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<?php
$page_title = $log['title'] . ' - ' . Option::get('site_title');
include View::resolve('header');
?>
<div id="progress" style="position:fixed;top:0;left:0;height:3px;width:0;background:#2563eb;z-index:99"></div>
<article class="single">
  <h1><?= h($log['title']) ?></h1>
  <div class="meta">
    <?= h($log['date_fmt']) ?> · <?= h($log['author_name']) ?> · <?= (int)$log['views'] ?> 浏览
  </div>
  <div class="content"><?= $log['content'] ?></div>
</article>
<nav class="pnav">
  <?php if ($prev): ?><a href="<?= h($prev['url']) ?>">← <?= h($prev['title']) ?></a><?php endif; ?>
  <?php if ($next): ?><a href="<?= h($next['url']) ?>"><?= h($next['title']) ?> →</a><?php endif; ?>
</nav>
<section class="comments">
  <h3>评论 (<?= CommentModel::countByLog($log['gid']) ?>)</h3>
  <?php $render = function ($list, $d = 0) use (&$render) {
      foreach ($list as $c) { ?>
        <div class="comment" style="margin-left:<?= $d * 24 ?>px">
          <b><?= h($c['poster']) ?></b> <small><?= datetimeFmt($c['date']) ?></small>
          <div><?= nl2br(h($c['comment'])) ?></div>
        </div>
      <?php if (!empty($c['children'])) $render($c['children'], $d + 1);
      }
  }; $render($comments); ?>

  <?php if ($log['allow_remark'] !== 'n'): ?>
  <form method="post" action="<?= BLOG_URL ?>?action=comment_post">
    <input type="hidden" name="gid" value="<?= (int)$log['gid'] ?>">
    <input type="text" name="poster" placeholder="昵称 *" required>
    <input type="email" name="email" placeholder="邮箱">
    <textarea name="comment" rows="5" placeholder="说点什么..." required></textarea>
    <button type="submit">发表评论</button>
  </form>
  <?php endif; ?>
</section>
<script>
(function () {
  var bar = document.getElementById('progress');
  if (!bar) return;
  var content = document.querySelector('.content');
  if (!content) return;
  function update() {
    var start = content.offsetTop;
    var end = start + content.offsetHeight - window.innerHeight;
    var pct = end <= start ? 100 : Math.min(100, Math.max(0, (window.scrollY - start) / (end - start) * 100));
    bar.style.width = pct + '%';
  }
  window.addEventListener('scroll', update, { passive: true });
  update();
})();
</script>
<?php include View::resolve('footer'); ?>

content/templates/demo/side.php

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<div class="widget">
  <h4>最新文章</h4>
  <ul>
    <?php foreach (LogModel::latest(5) as $l): ?>
      <li><a href="<?= Url::log($l['gid'], $l['alias']) ?>"><?= h($l['title']) ?></a></li>
    <?php endforeach; ?>
  </ul>
</div>
<div class="widget">
  <h4>分类</h4>
  <ul>
    <?php foreach (SortModel::all() as $s): ?>
      <li><a href="<?= Url::sort($s['sid'], $s['alias']) ?>"><?= h($s['sortname']) ?></a></li>
    <?php endforeach; ?>
  </ul>
</div>

content/templates/demo/css/style.css

* { box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", sans-serif; margin: 0; color: #333; background: #f6f7f9; line-height: 1.7; }
a { color: #2563eb; text-decoration: none; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; }
.head { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid #eee; }
.brand { font-size: 20px; font-weight: 700; color: #111; }
.nav a { margin-left: 16px; color: #555; font-size: 14px; }
.main { padding: 24px 0; }
.layout { display: flex; gap: 24px; }
.col-main { flex: 1; min-width: 0; }
.col-side { width: 260px; flex-shrink: 0; }
.item, .single, .widget, .comments { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.item h2 { margin: 0 0 8px; font-size: 18px; }
.meta { color: #888; font-size: 13px; margin-bottom: 8px; }
.excerpt { color: #555; margin: 0 0 10px; }
.single h1 { margin: 0 0 10px; font-size: 24px; }
.content { font-size: 16px; }
.content img { max-width: 100%; height: auto; }
.content pre { background: #1e293b; color: #e2e8f0; padding: 14px 18px; border-radius: 8px; overflow-x: auto; }
.content code:not(pre code) { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; color: #db2777; font-size: 0.9em; }
.pnav { display: flex; justify-content: space-between; margin-bottom: 16px; font-size: 14px; }
.comments input, .comments textarea { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 8px; font-family: inherit; }
.comments button { background: #2563eb; color: #fff; border: 0; padding: 9px 20px; border-radius: 6px; cursor: pointer; }
.widget h4 { margin: 0 0 10px; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 6px; }
.widget ul { list-style: none; padding: 0; margin: 0; font-size: 14px; }
.widget li { margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.foot { text-align: center; color: #999; font-size: 13px; padding: 24px 0; }
@media (max-width: 768px) { .layout { flex-direction: column; } .col-side { width: auto; } }

启用:后台 → 主题 → "Demo" → 启用此主题。


十一、开发规范

11.1 命名规范

类型规则示例
主题目录小写字母 + 短横线nebula、my-theme
CSS class小写 + 短横线.post-card-title
JS 变量小驼峰readProgress
PHP 函数大驼峰 + 下划线前缀Demo_getLatest()

11.2 安全规范

  • ✅ 所有用户可控输出加 h():echo h($log['title'])
  • ✅ $log['content'] 例外,它已经是渲染后的安全 HTML
  • ✅ 打开 header.php 时立刻 if (!defined('LITELOG_ROOT')) exit('error!');
  • ✅ 评论内容用 nl2br(h($c['comment']))(先转义后换行)
  • ❌ 不要直接 echo $log['excerpt'](未转义)
  • ❌ 不要在模板里做数据库写操作

11.3 性能规范

  • 首页循环里不要为每篇文章查一次标签/分类,模型已批量提供
  • 侧边栏的 LogModel::latest() 加 limit
  • 图片用 loading="lazy"
  • CSS / JS 放 </body> 前,避免阻塞

11.4 兼容规范

  • 不修改内核文件(include/、admin/)
  • 不覆盖 default 主题的文件
  • 主内容区必须包在 <main> 里(before_content / after_content 钩子位置依赖)
  • 主题文件可以缺 page.php / 404.php / side.php,内核会自动回退到 default

11.5 代码块样式(推荐)

复制 content/templates/default/css/style.css 里的 .content pre 段落(保证代码块深色样式一致)。并在 footer.php 底部引入前台代码块复制脚本:

<script src="<?= TEMPLATE_URL ?>js/codeblock.js"></script>

codeblock.js 的内容可从 default 主题拷贝。


十二、调试技巧

12.1 打开错误显示

编辑 init.php,临时改:

error_reporting(E_ALL);
ini_set('display_errors', 1);

调试完务必改回:

error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_WARNING);
ini_set('display_errors', 0);

12.2 快速查看变量

在模板任意位置插入:

<pre><?php var_dump($log); exit; ?></pre>

12.3 检查当前页类型

在 header.php 或 log_list.php 加:

<!-- pageType: <?= $pageType ?? 'n/a' ?> -->
<!-- sort: <?= !empty($sort) ? 'yes' : 'no' ?> -->

12.4 检查钩子是否被调用

在 footer.php 里加:

<?php Hook::doAction('template_footer'); ?>
<!-- template_footer 已触发 -->

12.5 F12 排查

  • Console:JS 报错、资源 404
  • Network:CSS / JS 是否加载成功
  • Elements:DOM 结构是否符合预期

十三、常见问题

Q1:启用主题后前台白屏

  1. 打开错误显示(见 12.1),看报错
  2. 检查 header.php 顶部是否漏了 if (!defined('LITELOG_ROOT')) exit('error!');
  3. 检查 $page_title 是否在使用前赋值(未定义会 notice,但不会白屏)
  4. 检查是否引用了内核不存在的方法(如 LogModel::foo())

Q2:文章列表为空

  • $logs 可能真的是空数组(站点没有文章)
  • 用 var_dump($logs) 打印看看
  • 检查 hide='n' 的文章是否存在

Q3:分页不显示

  • $pageHtml 为空说明总文章数 ≤ 每页数(Option::get('index_lognum'))
  • 打印 count($logs) 和 Option::get('index_lognum') 对比

Q4:代码块没样式 / 复制按钮没出来

  • 检查是否引入 codeblock.js
  • 检查 CSS 里是否有 .content pre { ... } 段落
  • 打开 F12 → Network,看 JS 是否 404

Q5:评论提交后 404

  • 评论表单的 action 必须是 <?= BLOG_URL ?>?action=comment_post
  • method 必须是 post
  • 必须有 <input type="hidden" name="gid" value="...">

Q6:主题切换后还是旧样式

  • 强制刷新 Ctrl + Shift + R
  • 或 F12 → Network → Disable cache
  • 或清空浏览器缓存

Q7:想给主题加后台设置项

在主题目录新建 options.php:

<?php if (!defined('LITELOG_ROOT')) exit('error!'); ?>
<div class="card">
  <div class="card-title">主题设置</div>
  <form method="post" class="form">
    <label>主题主色</label>
    <input type="text" name="theme_color" value="<?= h(Option::get('theme_color', '#2563eb')) ?>">
    <button class="btn" type="submit">保存</button>
  </form>
</div>

在 admin/index.php 的 theme_activate 或新增路由里处理保存,或者用 钩子 option_save 监听。具体实现参考插件开发规范。

Q8:怎么让主题支持插件?

主题只要在合适的 HTML 位置主动调用钩子即可,插件会自动生效。见 第七节。

Q9:怎么让主题有自己的专属钩子?

在主题任意位置加:

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

插件监听:

Hook::addAction('mytheme_header_banner', 'MyPlugin_renderBanner');

约定用 主题名_xxx 前缀,避免与内核冲突。

Q10:主题想要深色/浅色切换

在 header.php 加一个按钮:

<button id="theme-toggle">切换模式</button>

在主题 JS 里:

(function () {
    var KEY = 'theme_mode';
    var saved = localStorage.getItem(KEY);
    if (saved === 'light') document.body.classList.add('light');

    var btn = document.getElementById('theme-toggle');
    if (btn) btn.addEventListener('click', function () {
        document.body.classList.toggle('light');
        localStorage.setItem(KEY, document.body.classList.contains('light') ? 'light' : 'dark');
    });
})();

CSS 里定义:

body { background: #0a0e1a; color: #e6ecf5; }
body.light { background: #f6f7f9; color: #333; }

结语

开发一个 LiteLog 主题的步骤:

  1. content/templates/ 下建目录(英文小写)
  2. 创建 header.php(顶部写 Theme Name 注释头)
  3. 创建 footer.php、log_list.php、echo_log.php
  4. 按需补 side.php、page.php、404.php
  5. 放 css/style.css 和 js/theme.js
  6. 后台 → 主题 → 启用
  7. 前台刷新查看

记住三个原则:

  • 所有用户可控输出加 h() —— 安全
  • 主内容区包 <main> —— 钩子位置依赖
  • 静态资源用 TEMPLATE_URL —— 子目录兼容

调试三件套:

  • 临时开 display_errors
  • var_dump() + exit
  • F12 看 Console / Network

这就是 LiteLog 主题开发的全部。 有问题优先查本文档,其次查《LiteLog 插件开发规范》(那里有完整的 47 个钩子清单)。

评论 (0)

还没有评论,来说点什么吧~

发表评论

正在回复
已有账号?登录后评论