逗号CMS 模板开发手册

面向模板开发者的完整技术文档 · 适用于 V6.8 及以上版本

PHP + MySQL 纯静态模板 响应式支持

概述

逗号CMS 采用「无框架 + 纯PHP模板」的设计,模板文件直接通过 include 引入,无模板引擎、无编译过程。开发模板只需掌握基础 PHP 语法和函数库调用即可。

设计原则

  • 零依赖:模板不依赖任何第三方PHP框架,纯原生PHP
  • 函数驱动:所有数据通过 /functions.php 中的函数获取
  • 安全优先:模板内容经过安全校验(is_safe_template_code),禁止危险函数调用
  • 响应式:一套模板适配 PC + 手机端
  • 缓存友好:模板输出可被静态缓存,可自定义缓存粒度

模板引擎

逗号CMS 不使用任何模板引擎(如 Smarty、Twig),模板文件本身就是 PHP 文件,直接在 <?php ?> 中编写逻辑,在 <?= ?> 或 echo 中输出内容。

推荐使用 htmlspecialchars() 对所有动态输出进行转义,避免 XSS 攻击。

目录结构

一个完整的逗号CMS 模板需要包含以下文件:

/templates/
└── yourtheme/                 ← 模板目录(英文小写)
    ├── header.php             ← 公共头部【必需】
    ├── footer.php             ← 公共底部【必需】
    ├── index.php              ← 首页【必需】
    ├── article.php            ← 文章详情页【必需】
    ├── category.php           ← 分类列表页 + 搜索页【必需】
    ├── search_rs.php          ← 热搜标签聚合页【可选】
    ├── product_list.php       ← 产品列表页【可选】
    ├── product_detail.php     ← 产品详情页【可选】
    ├── style.css              ← 主样式表【必需】
    ├── images/                ← 模板图片目录【可选】
    ├── js/                    ← 模板JS目录【可选】
    ├── route.php              ← 前端路由文件【必需,无特殊需求直接用默认模板下的route.php即可】
    └── upgrade.php            ← 安装升级脚本【可选,安装后自动删除】

目录命名规范

  • 目录名只能包含 a-z A-Z 0-9 _ -
  • 建议使用英文小写,如 news、geju、blog
  • 不要使用 admin、set、cache、uploads 等系统目录名

文件命名规范

  • 核心文件命名固定:header.php、footer.php、index.php、article.php、category.php
  • 可选文件:search_rs.php、product_list.php、product_detail.php
  • 自定义页面不要覆盖以上文件名

执行流程

请求生命周期

用户访问 URL
    ↓
根目录 index.php
    ↓
安全检测(SQL注入 / XSS / 命令执行 / 黑名单 / 频率限制)
    ↓
访问日志记录(register_shutdown_function)
    ↓
加载主题缓存 cache/active_theme.php → 确定 TEMPLATE_PATH
    ↓
加载前端路由缓存 cache/front_route_cache.php
    ↓
parse_rewrite_url($uri, $setting, $db, $prefix) → $action
    ↓
route_dispatch($action, $db, $prefix, $setting, $template_path)
    ↓
include $template_path . 'header.php'
include $template_path . 'xxx.php'
include $template_path . 'footer.php'
    ↓
输出 HTML(如开启缓存则写入 cache/xxx.html)

action 与模板文件对应关系

action模板文件URL 示例
indexindex.php/
listcategory.php/list.html、/list-2.html
categorycategory.php/category-1.html
articlearticle.php/article-123.html
searchcategory.php/rs/关键词
search_rs_indexsearch_rs.php/rs
product_listproduct_list.php/product.html
product_detailproduct_detail.php/product-123.html
ubb_indexset/ubb/index.php/ubb
ubb_pageset/ubb/page.php/ubb/{slug}
404set/404.php任意未知 URL

全局变量

在 header.php、footer.php 和各类页面模板中,可以直接使用以下变量:

变量类型说明
$actionstring当前 action(index / list / category / article / search / ...)
$dbmysqli数据库连接对象
$prefixstring数据表前缀(默认 douhao_)
$settingarray网站设置数组
$seoarray当前页面 SEO 数组(title / keywords / description)
$template_pathstring当前模板目录绝对路径(带尾部斜杠)
TEMPLATE_PATH常量同 $template_path
ROOT_PATH常量网站根目录绝对路径
IMG_PATH常量图片上传目录 /set/img/
CACHE_PATH常量缓存目录 /cache/
$setting 包含:site_name、logo、icp、contact_qq、contact_email、contact_wechat_qrcode、contact_phone、contact_address、contact_worktime、list_mode、hot_views、readmore_enabled、hot_search_limit、stat_code 等。

核心函数库

所有函数定义在 /functions.php,模板中直接调用即可。

网站设置类

get_setting()                    // 返回全部设置数组
get_setting('site_name')         // 返回单个设置值
get_seo('index')                 // 首页 SEO
get_seo('category', ['category_name' => '分类名'])
get_seo('article', [
    'article_title' => '标题',
    'article_description' => '摘要',
    'category_name' => '分类名'
])
get_seo('search', ['keyword' => '关键词'])
get_seo('product_list', ['category_name' => '产品分类'])
get_seo('product_detail', ['product_title' => '产品名'])

分类数据

get_categories()                 // 返回所有分类数组
get_category_counts()            // 返回 [分类ID => 文章数] 映射

文章数据

get_articles($where, $limit, $order)

get_articles('', 10)                           // 最新10篇
get_articles('is_recommend = 1', 10)           // 推荐10篇
get_articles('is_hot = 1', 10, 'views DESC')   // 热门10篇
get_articles('category_id = 1', 10)            // 分类1的10篇
get_articles('', '0, 10')                      // 分页(第1页10条)
get_articles('', '10, 10')                     // 分页(第2页10条)

get_article($id)                 // 获取单篇文章
get_article_count($where)        // 统计文章数量
update_views($id)                // 更新阅读量 +1

get_prev_next_article($id)       // 上一篇/下一篇
get_article_content_with_readmore($content, $article_id)

产品数据

get_products($where, $limit, $order)

get_products('', 10)
get_products('is_recommend = 1', 10)
get_products('category_id = 1', 12)

URL 生成

build_url('article', $id)          // /article-123.html
build_url('category', $id)         // /category-1.html
build_url('product', $id)          // /product-123.html
build_url('product_category', $id) // /product-category-1.html

build_url_id('article', $id)       // 只返回 ID 或 hash/md5

广告系统

get_ad_position('banner')          // 轮播图广告位
get_ad_position('header_banner')   // 顶部横幅广告位
get_ad_position('footer_float')    // 底部悬浮广告位
get_ad_position('article_readmore')// 文章页阅读全文广告位
get_ad_position('自定义标识')       // 自定义广告位

搜索与热搜

split_search_keywords($keyword)         // 分词
search_articles_by_keywords($keywords, $page, $page_size)

get_hot_searches($limit)                // 获取已发布热搜词
record_hot_search($keyword)             // 记录搜索词(自动 +1 热度)
is_hot_search_published($keyword)       // 判断热搜词是否已发布
get_latest_search($limit)               // 获取最新搜索词

分页

pagination($total, $page_size, $current_page, $base_url)

$base_url = "/list.html?";
echo pagination($total, 20, $page, $base_url);

辅助函数

douhao_logo()              // 输出网站 LOGO HTML
douhao_site_name()         // 返回网站名称
douhao_icp()               // 返回 ICP 备案链接 HTML
douhao_list_mode()         // 返回图文列表模式(1 或 2)

safe_html($html)           // HTML 安全过滤(保留富文本标签)

check_sensitive_words($content)          // 检查敏感词
get_sensitive_words_array()              // 敏感词数组

auto_assign_image($category_id, $folder, $article_id)  // 随机配图

detect_spider($user_agent)     // 识别蜘蛛
detect_terminal($user_agent)   // 识别终端
detect_source($referer)        // 识别来源

header.php 开发指南

header.php 是所有页面的公共头部,负责输出 <!DOCTYPE>、<head>、顶部导航等。

必需结构

<?php
/**
 * 逗号CMS - 模板头部
 */
if (!isset($seo) || empty($seo['title'])) {
    $seo = get_seo($action == 'index' ? 'index' : 'article');
}
$setting = get_setting();
$categories = get_categories();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo htmlspecialchars($seo['title']); ?></title>
    <meta name="keywords" content="<?php echo htmlspecialchars($seo['keywords']); ?>">
    <meta name="description" content="<?php echo htmlspecialchars($seo['description']); ?>">
    <link rel="stylesheet" href="/set/webfonts/all.min.css">
    <link rel="stylesheet" href="/set/css/ass.css">
    <link rel="stylesheet" href="/templates/yourtheme/style.css">
</head>
<body>
<header class="douhao-header">
    <div class="douhao-header-inner">
        <div class="douhao-logo">
            <a href="/"><?php echo douhao_logo(); ?></a>
        </div>
        <nav class="douhao-nav">
            <ul>
                <li><a href="/">首页</a></li>
                <?php foreach ($categories as $cat): ?>
                <li><a href="<?php echo build_url('category', $cat['id']); ?>">
                    <?php echo htmlspecialchars($cat['name']); ?>
                </a></li>
                <?php endforeach; ?>
            </ul>
        </nav>
    </div>
</header>
<?php echo get_ad_position('header_banner'); ?>

菜单高亮

判断当前页面是否在某菜单下,可用 $action 和 $category_id:

<?php
$current_cat = 0;
if (isset($category_id) && $category_id > 0) {
    $current_cat = intval($category_id);
} elseif ($action == 'article' && isset($_GET['id'])) {
    $tmp = get_article(intval($_GET['id']));
    if ($tmp) $current_cat = intval($tmp['category_id']);
}
?>

<li class="<?php echo $current_cat == $cat['id'] ? 'active' : ''; ?>">
    <a href="<?php echo build_url('category', $cat['id']); ?>">
        <?php echo htmlspecialchars($cat['name']); ?>
    </a>
</li>

index.php 开发指南

首页模板,可调用任意推荐、热门、最新、分类文章。

常用数据获取

<?php
$seo = get_seo('index');
$recommend = get_articles('is_recommend = 1', $seo['recommend_count']);
$latest = get_articles('', $seo['latest_count']);
$hot = get_articles('is_hot = 1', $seo['hot_count'], 'views DESC');
$categories = get_categories();
?>

推荐位循环

<?php foreach ($recommend as $item): ?>
<article class="douhao-card">
    <a href="<?php echo build_url('article', $item['id']); ?>">
        <img src="<?php echo htmlspecialchars($item['image']); ?>" 
             alt="<?php echo htmlspecialchars($item['title']); ?>">
        <h3><?php echo htmlspecialchars($item['title']); ?></h3>
        <p><?php echo htmlspecialchars(mb_substr(strip_tags($item['description']), 0, 80)); ?></p>
    </a>
</article>
<?php endforeach; ?>

分类区块

<?php foreach ($categories as $cat):
    $cat_articles = get_articles("category_id = " . intval($cat['id']), 6);
    if (empty($cat_articles)) continue;
?>
<section class="douhao-cat-block">
    <h2><a href="<?php echo build_url('category', $cat['id']); ?>">
        <?php echo htmlspecialchars($cat['name']); ?>
    </a></h2>
    <ul>
        <?php foreach ($cat_articles as $art): ?>
        <li><a href="<?php echo build_url('article', $art['id']); ?>">
            <?php echo htmlspecialchars($art['title']); ?>
        </a></li>
        <?php endforeach; ?>
    </ul>
</section>
<?php endforeach; ?>

article.php 开发指南

文章详情页模板,展示文章内容、上一篇下一篇、相关文章等。

基础结构

<?php
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$article = get_article($id);

if (!$article) {
    echo '<div class="douhao-empty">文章不存在</div>';
    return;
}

$category_name = '';
foreach (get_categories() as $cat) {
    if ($cat['id'] == $article['category_id']) {
        $category_name = $cat['name'];
        break;
    }
}
$category_id = intval($article['category_id']);

$nav = get_prev_next_article($id);
$related = get_articles("category_id = " . intval($article['category_id']) . " AND id != $id", 6);
?>

正文输出

<div class="douhao-entry">
    <?php echo safe_html($article['content']); ?>
</div>
正文必须用 safe_html() 输出,它会保留富文本标签(p/img/h2/ul 等),同时过滤 script、iframe、on* 事件等危险内容。

上一篇 / 下一篇

<?php if ($nav['prev']): ?>
<a href="<?php echo build_url('article', $nav['prev']['id']); ?>">
    上一篇:<?php echo htmlspecialchars($nav['prev']['title']); ?>
</a>
<?php endif; ?>

<?php if ($nav['next']): ?>
<a href="<?php echo build_url('article', $nav['next']['id']); ?>">
    下一篇:<?php echo htmlspecialchars($nav['next']['title']); ?>
</a>
<?php endif; ?>

阅读全文功能

<?php
$content_data = get_article_content_with_readmore($article['content'], $id);
?>

<?php if ($content_data['is_truncated']): ?>
    <div class="douhao-preview">
        <?php echo safe_html($content_data['preview']); ?>
    </div>
    <button onclick="showFull()">阅读全文</button>
    <div id="fullContent" style="display:none;">
        <?php echo safe_html($content_data['full']); ?>
    </div>
<?php else: ?>
    <?php echo safe_html($content_data['full']); ?>
<?php endif; ?>

category.php 开发指南

category.php 同时承担分类列表页和搜索页两种场景。

判断当前是分类页还是搜索页

<?php
$is_search = (isset($_GET['action']) && $_GET['action'] === 'search')
          || (isset($action) && $action === 'search' && !empty($_GET['keyword']));
?>

分类列表数据

<?php
$category_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$page_size = 20;

if ($category_id > 0) {
    $where = "category_id = $category_id";
    $total = get_article_count($where);
    $base_url = build_url('category', $category_id) . '?';
} else {
    $where = '';
    $total = get_article_count();
    $base_url = "/list.html?";
}

$offset = ($page - 1) * $page_size;
$articles = get_articles($where, "$offset, $page_size");
$pagination = pagination($total, $page_size, $page, $base_url);
?>

搜索页数据

<?php
if ($is_search && !empty($keyword)) {
    $keywords = split_search_keywords($keyword);
    $result = search_articles_by_keywords($keywords, $page, $page_size);
    $articles = $result['articles'];
    $total = $result['total'];
    $pagination = pagination($total, $page_size, $page, "/rs/" . rawurlencode($keyword) . "?");
}
?>

标题高亮

<?php
function highlight_title($title, $keywords) {
    if (empty($keywords)) return htmlspecialchars($title);
    $escaped = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
    $parts = [];
    foreach ($keywords as $kw) {
        $parts[] = preg_quote(htmlspecialchars($kw, ENT_QUOTES, 'UTF-8'), '/');
    }
    $pattern = '/(' . implode('|', $parts) . ')/u';
    return preg_replace($pattern, '<em class="hit">$1</em>', $escaped);
}
?>

search_rs.php 开发指南

热搜标签聚合页(/rs),展示所有已发布的热搜词。

<?php
$hot_tags = get_hot_searches(200);  // 最多 200 个
?>

<div class="douhao-tags-page">
    <?php foreach ($hot_tags as $tag): ?>
    <a href="/rs/<?php echo rawurlencode($tag['keyword']); ?>">
        <?php echo htmlspecialchars($tag['keyword']); ?>
        <span><?php echo intval($tag['search_count']); ?></span>
    </a>
    <?php endforeach; ?>
</div>
get_hot_searches() 只返回 status = 1 的已发布热搜词,按 is_top DESC, sort ASC, search_count DESC 排序。

产品页开发指南

product_list.php

<?php
$category_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$page_size = 12;

if ($category_id > 0) {
    $where = "WHERE category_id = $category_id";
    $total = $db->query("SELECT COUNT(*) as t FROM {$prefix}product $where")->fetch_assoc()['t'];
} else {
    $where = '';
    $total = $db->query("SELECT COUNT(*) as t FROM {$prefix}product")->fetch_assoc()['t'];
}

$offset = ($page - 1) * $page_size;
$products = $db->query("SELECT * FROM {$prefix}product $where ORDER BY id DESC LIMIT $offset, $page_size");
?>

product_detail.php

<?php
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$product = $db->query("SELECT p.*, c.name as category_name 
                       FROM {$prefix}product p 
                       LEFT JOIN {$prefix}product_category c ON p.category_id = c.id 
                       WHERE p.id = $id")->fetch_assoc();

$db->query("UPDATE {$prefix}product SET views = views + 1 WHERE id = $id");
$gallery = json_decode($product['gallery'], true);
?>

广告系统集成

在模板中调用广告位,只需一行代码:

<?php echo get_ad_position('banner'); ?>
<?php echo get_ad_position('header_banner'); ?>
<?php echo get_ad_position('footer_float'); ?>
<?php echo get_ad_position('article_readmore'); ?>
<?php echo get_ad_position('自定义标识'); ?>

系统内置广告位

标识名称展示模式
banner网站轮播图轮播
header_banner网站横幅随机
footer_float网站底部悬浮随机
article_readmore文章页阅读全文随机
模板如需新增广告位,可在后台「广告管理 → 广告位管理」创建自定义广告位,然后用 get_ad_position('自定义标识') 调用。自定义广告位默认使用「随机模式」。

热搜标签系统

获取热搜词

<?php
$setting = get_setting();
$limit = intval($setting['hot_search_limit'] ?? 10);
$hot = get_hot_searches($limit);
?>

<div class="douhao-hot-tags">
    <?php foreach ($hot as $tag): ?>
    <a href="/rs/<?php echo rawurlencode($tag['keyword']); ?>">
        <?php echo htmlspecialchars($tag['keyword']); ?>
        <span><?php echo intval($tag['search_count']); ?></span>
    </a>
    <?php endforeach; ?>
</div>

数据表字段

字段说明
id主键
keyword搜索词
search_count搜索次数(热度)
status0=待审,1=已发布
is_top0=普通,1=置顶
sort排序,越小越靠前

留言本集成

留言本首页和详情页由系统自动处理,模板无需提供文件,只需在导航中加链接:

<a href="/ubb">留言本</a>
/ubb 会自动加载 set/ubb/index.php(列表页)和 set/ubb/page.php(详情页),两者共用你模板的 header.php 和 footer.php。

在模板中查询留言页

<?php
$db = get_db();
$prefix = $GLOBALS['db_config']['prefix'];
$pages = $db->query("SELECT id, name, slug FROM {$prefix}ubb_pages WHERE status = 1 ORDER BY sort ASC");
while ($p = $pages->fetch_assoc()):
?>
<a href="/ubb/<?php echo htmlspecialchars($p['slug']); ?>">
    <?php echo htmlspecialchars($p['name']); ?>
</a>
<?php endwhile; ?>

URL 生成

build_url 函数

build_url('article', $id)          // → /article-123.html
build_url('category', $id)         // → /category-1.html
build_url('product', $id)          // → /product-123.html
build_url('product_category', $id) // → /product-category-1.html

build_url() 会自动适配后台伪静态设置(ID / 哈希 / MD5)和前缀。例如后台将文章前缀改为 post-,则输出 /post-123.html。

懒加载机制

如果使用哈希或 MD5 模式,首次访问时系统会自动生成哈希/MD5 值并写入数据库,之后复用。

不要手写 URL

❌ 不要写 <a href="/article-<?php echo $id; ?>.html">
✅ 使用 <a href="<?php echo build_url('article', $id); ?>">

SEO 集成

SEO 数据通过 get_seo() 获取,后台「SEO设置」中可为每个页面类型配置独立模板。

可用标签

标签说明适用页面
{site_name}网站名称全部页面
{category_name}分类名称分类页 / 文章页
{article_title}文章标题文章页
{article_description}文章摘要文章页
{product_title}产品标题产品详情页
{product_description}产品简介产品详情页
{keyword}搜索关键词搜索页

输出规范

<title><?php echo htmlspecialchars($seo['title']); ?></title>
<meta name="keywords" content="<?php echo htmlspecialchars($seo['keywords']); ?>">
<meta name="description" content="<?php echo htmlspecialchars($seo['description']); ?>">

数量参数

$seo['recommend_count']   // 推荐文章数
$seo['hot_count']         // 热门文章数
$seo['latest_count']      // 最新文章数
$seo['related_count']     // 相关文章数
$seo['recommend_product'] // 推荐产品数
$seo['hot_product']       // 热门产品数
$seo['latest_product']    // 最新产品数

缓存机制

缓存类型

类型说明
index首页
category分类列表页(含全部文章页)
article文章详情页
product_list产品分类页
product_detail产品详情页
search_rs热搜标签结果页(只缓存已发布词的第1页)

模板如何配合缓存

  • 缓存以 md5(REQUEST_URI) + 类型 + md5(HTTP_HOST) 为 key,生成 HTML 文件到 /cache/
  • 模板正常输出 HTML 即可,缓存机制在 index.php 层处理
  • 如果页面包含动态数据(如阅读量),开启缓存后不会实时更新

缓存目录权限

/cache/ 目录需 755 权限,属主为 Web 运行用户。模板本身不需要处理缓存目录。

安全规范

禁止使用的函数

逗号CMS 会对模板代码做安全校验,以下函数禁止在模板中调用:

  • 命令执行:system、exec、passthru、shell_exec、popen、proc_open、eval、assert
  • 文件读写:file_get_contents、file_put_contents、fopen、unlink、rename、mkdir、rmdir
  • 网络请求:fsockopen、curl_exec、stream_socket_client
  • 动态执行:call_user_func、create_function、include、require(变量)

输出转义

所有动态输出必须转义:echo htmlspecialchars($str);

富文本内容

输出后台富文本内容(如文章正文),必须用 safe_html():

echo safe_html($article['content']);

SQL 查询

<?php
$id = intval($_GET['id']);
$keyword = $db->real_escape_string($_GET['kw']);

$db->query("SELECT * FROM {$prefix}article WHERE id = $id");
?>

CSRF 防护

<form method="post">
    <?php echo csrf_field(); ?>
    <input type="text" name="xxx">
    <button type="submit">提交</button>
</form>

upgrade.php 升级脚本

upgrade.php 是主题安装时自动执行的脚本,用于初始化数据、写入路由等,安装完成后会自动删除。

基础结构

<?php
/**
 * 你的主题 - 升级脚本
 */

$db = get_db();
$prefix = $GLOBALS['db_config']['prefix'];

$new_route_code = '<?php
function parse_rewrite_url($uri, $setting, $db, $prefix) {
    // ...
}
function route_dispatch($action, $db, $prefix, $setting, $template_path) {
    // ...
}
';

$check = $db->query("SELECT id FROM {$prefix}front_route WHERE config_name = 'default' LIMIT 1");
if ($check && $check->num_rows > 0) {
    $escaped = $db->real_escape_string($new_route_code);
    $db->query("UPDATE {$prefix}front_route SET config_code = '$escaped' WHERE config_name = 'default'");
} else {
    $escaped = $db->real_escape_string($new_route_code);
    $db->query("INSERT INTO {$prefix}front_route (config_name, config_code) VALUES ('default', '$escaped')");
}

if (!is_dir(CACHE_PATH)) @mkdir(CACHE_PATH, 0755, true);

$cache_content = "<?php\n"
    . "if (isset(\$_SERVER['SCRIPT_FILENAME']) && realpath(\$_SERVER['SCRIPT_FILENAME']) === __FILE__) {\n"
    . "    http_response_code(404);\n"
    . "    exit;\n"
    . "}\n"
    . "?>\n"
    . $new_route_code;

@file_put_contents(CACHE_PATH . 'front_route_cache.php', $cache_content);
@chmod(CACHE_PATH . 'front_route_cache.php', 0644);
不需要写路由就不要加 upgrade.php。系统的默认路由 set/default_front_route.php 已包含 ubb / rs / article / category / product / list 全套路由,绝大多数模板无需自定义路由。

推荐的安全升级写法

<?php
$db = get_db();
$prefix = $GLOBALS['db_config']['prefix'];

$check = $db->query("SELECT config_code FROM {$prefix}front_route WHERE config_name = 'default' LIMIT 1");
$current_code = '';
if ($check && $check->num_rows > 0) {
    $current_code = $check->fetch_assoc()['config_code'];
}

if (strpos($current_code, 'search_rs_index') !== false) {
    return;
}

$new_route_code = '<?php ... ';

打包发布

打包结构

主题压缩包内部直接放文件,不要有多余的父目录:

yourtheme.zip
├── header.php
├── footer.php
├── index.php
├── article.php
├── category.php
├── search_rs.php
├── style.css
├── images/
│   └── 123.png
└── upgrade.php
❌ 错误:yourtheme.zip → yourtheme/ → header.php
✅ 正确:yourtheme.zip → header.php

上传主题到逗号CMS官方模板提交(暂未开放)

登录 逗号CMS官网 → 「发布主题」→ 上传 ZIP 包 + 缩略图 + 说明文档 + 截图。

必填信息

字段说明
模板名称展示给用户的中文名称
模板目录名解压后的文件夹名(英文小写)
版本号如 1.0、1.1、2.0
所属分类在总控台「分类管理」中维护
缩略图建议 600×400px,JPG/PNG
主题压缩包ZIP 格式,≤ 50MB
说明文档可选,HTML 文件
主题截图可选,可多张,带名称
模板简介简短描述

版本更新

更新版本时,只需在总控台「主题管理」点击对应主题的「编辑」,上传新的 ZIP 包即可(文件名不变,覆盖原文件)。

推荐使用补丁机制:在总控台为主题上传「补丁包」,用户端可增量更新,无需重新下载完整包。补丁包内可含 upgrade.php 或 upgrade.sql 执行数据库变更。

路由管理

逗号CMS 的路由分为前端路由和后台路由两类,均通过后台可视化界面管理,无需直接修改系统文件。

一、前端路由管理

入口:后台 → 管理 → 前端路由管理(或 ?page=front_route)

前端路由负责将 URL 映射到 action,再由 action 分发到对应模板。必须定义两个函数:

parse_rewrite_url($uri, $setting, $db, $prefix) {
    // 解析 URL,返回 action 字符串
}

route_dispatch($action, $db, $prefix, $setting, $template_path) {
    // 根据 action 加载对应模板
}

1. parse_rewrite_url 参数说明

参数类型说明
$uristring当前访问的 URI 路径(不含域名和查询串)
$settingarray网站设置
$dbmysqli数据库连接
$prefixstring数据表前缀

2. 返回值约定

  • 返回 action 名称(字符串),如 'index'、'article'、'category'
  • 返回 '404' 表示未匹配,由系统加载 set/404.php
  • 可在函数内直接给 $_GET['id']、$_GET['keyword'] 等赋值

3. route_dispatch 分发模板

if ($action === 'article') {
    $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
    $article = get_article($id);
    if (!$article) {
        http_response_code(404);
        require_once ROOT_PATH . 'set/404.php';
        exit;
    }
    update_views($id);

    $category_name = '';
    foreach (get_categories() as $cat) {
        if ($cat['id'] == $article['category_id']) {
            $category_name = $cat['name'];
            break;
        }
    }

    $seo = get_seo('article', [
        'article_title' => $article['title'],
        'article_description' => $article['description'],
        'category_name' => $category_name,
    ]);

    include $template_path . 'header.php';
    include $template_path . 'article.php';
    include $template_path . 'footer.php';
    return true;
}
每个 action 分支末尾必须 return true; 告诉系统已处理。route_dispatch() 返回 false 时,系统会回退到 index.php 内置的分发逻辑。

4. 内置 action 与参数

action关键 $_GET说明
index-首页
listpage全部文章列表,分页参数 page
categoryid、page分类页,id 是分类 ID 或 hash/md5
articleid文章详情页,id 是文章 ID 或 hash/md5
searchkeyword、page搜索页
search_rs_index-热搜标签聚合页
product_listid、page产品列表 / 产品分类页
product_detailid产品详情页
ubb_index-留言本首页
ubb_pageslug留言单页
404-未匹配

5. 新增自定义 action

如需新增自定义页面(如 /about.html),在 parse_rewrite_url() 中加匹配:

if ($uri === '/about.html') {
    return 'about';
}

在 route_dispatch() 中加分支:

if ($action === 'about') {
    $seo = get_seo('index');
    include $template_path . 'header.php';
    include $template_path . 'about.php';
    include $template_path . 'footer.php';
    return true;
}

然后在模板目录下新增 about.php 文件即可。

6. 缓存机制

  • 保存路由配置后,系统自动写入 /cache/front_route_cache.php
  • 所有请求直接读取缓存,性能优异
  • 修改后无需重启 PHP,下次请求即生效

7. 安全限制

路由代码经过 is_safe_template_code() 校验,禁止:system、exec、eval、file_get_contents、curl_exec 等危险函数。变量包含(include)只允许静态路径。

二、后台路由管理

入口:后台 → 管理 → 后台路由管理(或 ?page=admin_route)

后台路由负责将 ?page=xxx 映射到后台文件。必须定义 $allowed_pages 数组:

<?php
$allowed_pages = [
    'index' => 'admin/index.php',
    'site_setting' => 'admin/site_setting.php',
    'article_manage' => 'admin/article_manage.php',
    'article_edit' => 'admin/article_edit.php',
    // ...
];

return $allowed_pages;

1. 键值规则

  • 键:?page= 参数值,只允许 a-z A-Z 0-9 _
  • 值:后台文件路径,必须匹配 admin/[a-zA-Z0-9_\-]+\.php

2. 与内置路由的关系

admin.php 中内置了一份 $builtin_pages 数组,$allowed_pages 会与之合并:

$allowed_pages = array_merge($builtin_pages, $allowed_pages);

因此自定义路由只需补充新增页面即可,无需重复定义内置页面。

3. 新增后台页面

步骤:

  1. 在 /admin/ 目录新建 my_page.php(文件开头必须 require_once __DIR__ . '/auth.php';)
  2. 在后台路由管理中添加:'my_page' => 'admin/my_page.php'
  3. 在后台菜单管理中添加入口链接 <a href="?page=my_page">我的页面</a>

4. 权限控制

后台文件必须包含 auth.php,它会检查 $_SESSION['admin_id']。未登录时返回 403。

5. 安全限制

后台路由配置经过 safe_parse_route_config() 校验,只允许数组字面量,禁止任何函数调用、变量、表达式。

菜单管理

入口:后台 → 管理 → 后台菜单管理(或 ?page=admin_menu)

后台左侧导航菜单的显示结构由 HTML 代码定义,存储在 douhao_admin_menu 表的 menu_code 字段。

1. 菜单结构

菜单使用 <div class="nav-group"> 分组,每组内用 <a> 定义菜单项:

<nav class="sidebar-nav">
    <div class="nav-group">
        <div class="nav-title">系统管理</div>
        <a href="?page=site_setting" class="<?php echo $highlight_page=='site_setting'?'active':''; ?>">
            <i class="fas fa-cog"></i><span>网站设置</span>
        </a>
        <a href="?page=cache_manage" class="<?php echo $highlight_page=='cache_manage'?'active':''; ?>">
            <i class="fas fa-bolt"></i><span>缓存管理</span>
        </a>
    </div>

    <div class="nav-group">
        <div class="nav-title">内容管理</div>
        <a href="?page=article_manage" class="<?php echo $highlight_page=='article_manage'?'active':''; ?>">
            <i class="fas fa-newspaper"></i><span>文章管理</span>
        </a>
    </div>
</nav>

2. 可用变量

变量说明
$highlight_page当前页面标识(取 $_GET['from'] 或 $_GET['page'])

用 $highlight_page 判断当前菜单是否高亮,多个页面共用一个菜单项时可以这样写:

<a href="?page=article_manage" class="<?php echo in_array($highlight_page, ['article_manage', 'article_edit', 'category_manage']) ? 'active' : ''; ?>">
    <i class="fas fa-newspaper"></i><span>文章管理</span>
</a>

3. CSS 类说明

类名说明
sidebar-nav导航容器
nav-group菜单分组
nav-title分组标题
active当前菜单高亮

4. 图标使用

逗号CMS 后台集成 FontAwesome 6.5.0,图标类名格式:fas fa-图标名、far fa-图标名、fab fa-图标名。

常用示例

1. 首页大图轮播(PC 5 图 + 移动端轮播)

<?php
$top = get_articles('is_recommend = 1 AND image != \'\'', 5);
?>

<div class="douhao-hmnews">
    <div class="douhao-hmnews-list">
        <?php foreach ($top as $i => $art): ?>
        <div class="douhao-item douhao-item<?php echo $i + 1; ?>">
            <a href="<?php echo build_url('article', $art['id']); ?>">
                <div class="douhao-pic" style="background-image:url('<?php echo htmlspecialchars($art['image']); ?>')"></div>
                <h2><?php echo htmlspecialchars($art['title']); ?></h2>
            </a>
        </div>
        <?php endforeach; ?>
    </div>

    <div class="douhao-hmnews-slider">
        <div class="douhao-slider-track">
            <?php foreach ($top as $art): ?>
            <div class="douhao-slide">
                <a href="<?php echo build_url('article', $art['id']); ?>">
                    <div class="douhao-pic" style="background-image:url('<?php echo htmlspecialchars($art['image']); ?>')"></div>
                    <h2><?php echo htmlspecialchars($art['title']); ?></h2>
                </a>
            </div>
            <?php endforeach; ?>
        </div>
        <div class="douhao-slider-dots">
            <?php foreach ($top as $i => $art): ?>
            <span class="douhao-dot<?php echo $i === 0 ? ' douhao-active' : ''; ?>"></span>
            <?php endforeach; ?>
        </div>
    </div>
</div>

2. 分类区块(每个分类一个 section)

<?php foreach (get_categories() as $cat):
    $articles = get_articles("category_id = " . intval($cat['id']) . " AND image != ''", 6);
    if (empty($articles)) continue;
?>
<section class="douhao-newsbox">
    <h2><a href="<?php echo build_url('category', $cat['id']); ?>">
        <?php echo htmlspecialchars($cat['name']); ?>
    </a></h2>
    <ul>
        <?php foreach ($articles as $art): ?>
        <li>
            <a href="<?php echo build_url('article', $art['id']); ?>">
                <figure>
                    <img src="<?php echo htmlspecialchars($art['image']); ?>" alt="">
                </figure>
                <h3><?php echo htmlspecialchars($art['title']); ?></h3>
            </a>
        </li>
        <?php endforeach; ?>
    </ul>
</section>
<?php endforeach; ?>

3. 文章页相关文章

<?php
$related = get_articles("category_id = " . intval($article['category_id']) . " AND id != $id", 6);
if (!empty($related)):
?>
<section class="douhao-related">
    <h2>相关推荐</h2>
    <ul>
        <?php foreach ($related as $rel): ?>
        <li>
            <a href="<?php echo build_url('article', $rel['id']); ?>">
                <?php if (!empty($rel['image'])): ?>
                <img src="<?php echo htmlspecialchars($rel['image']); ?>" alt="">
                <?php endif; ?>
                <h3><?php echo htmlspecialchars($rel['title']); ?></h3>
            </a>
        </li>
        <?php endforeach; ?>
    </ul>
</section>
<?php endif; ?>

4. 侧栏最新/推荐

<aside class="douhao-sidebar">
    <?php $latest = get_articles('', 6); ?>
    <section class="douhao-widget">
        <h3>最新文章</h3>
        <ul>
            <?php foreach ($latest as $item): ?>
            <li>
                <a href="<?php echo build_url('article', $item['id']); ?>">
                    <?php echo htmlspecialchars($item['title']); ?>
                </a>
            </li>
            <?php endforeach; ?>
        </ul>
    </section>
</aside>

5. 提取文章首图(无封面时用正文首图)

<?php
function get_thumb($article) {
    if (!empty($article['image'])) return $article['image'];
    if (!empty($article['content'])) {
        if (preg_match('/<img[^>]+src="([^"]+)"/i', $article['content'], $m)) {
            return $m[1];
        }
    }
    return '';
}
?>

6. 广告位调用

<?php echo get_ad_position('banner'); ?>
<?php echo get_ad_position('article_readmore'); ?>

7. 分页输出

<?php echo pagination($total, $page_size, $page, "/list.html?"); ?>

8. 菜单高亮

<?php
$current_cat = 0;
if (isset($category_id) && $category_id > 0) {
    $current_cat = intval($category_id);
} elseif ($action == 'article' && isset($_GET['id'])) {
    $tmp = get_article(intval($_GET['id']));
    if ($tmp) $current_cat = intval($tmp['category_id']);
}
?>

<li class="<?php echo $current_cat == $cat['id'] ? 'active' : ''; ?>">
    <a href="<?php echo build_url('category', $cat['id']); ?>">
        <?php echo htmlspecialchars($cat['name']); ?>
    </a>
</li>

9. 移动端轮播 JS

(function() {
    var slider = document.getElementById('slider');
    var track = document.getElementById('track');
    if (!slider || !track) return;

    var total = track.children.length;
    var current = 0;

    function goTo(i) {
        if (i < 0) i = total - 1;
        if (i >= total) i = 0;
        current = i;
        track.style.transform = 'translateX(' + (-i * 100) + '%)';
    }

    setInterval(function() { goTo(current + 1); }, 4000);

    var startX = 0;
    slider.addEventListener('touchstart', function(e) {
        startX = e.changedTouches[0].screenX;
    }, {passive: true});

    slider.addEventListener('touchend', function(e) {
        var diff = startX - e.changedTouches[0].screenX;
        if (Math.abs(diff) > 40) {
            goTo(diff > 0 ? current + 1 : current - 1);
        }
    }, {passive: true});
})();

10. 搜索框(跳转 /rs)

<form onsubmit="return doSearch(event)">
    <input type="text" id="kw" placeholder="输入关键词搜索...">
    <button type="submit"><i class="fas fa-search"></i></button>
</form>

<script>
function doSearch(e) {
    e.preventDefault();
    var kw = document.getElementById('kw').value.trim();
    if (!kw) return false;
    window.location.href = '/rs/' + encodeURIComponent(kw);
    return false;
}
</script>

常见问题

Q1:模板安装后前端 404 或白屏?

检查以下项:

  • 模板目录名是否合法(仅字母数字下划线短横线)
  • 必需文件是否存在:header.php、footer.php、index.php、article.php、category.php
  • 伪静态规则是否配置(参考「安装手册 → 伪静态设置」)
  • 清空 /cache/ 目录后重试

Q2:导航菜单当前分类不亮?

文章页需主动查询当前文章的 category_id,因为 header.php 在 article.php 之前被 include。

$current_cat = 0;
if ($action == 'article' && isset($_GET['id'])) {
    $row = $db->query("SELECT category_id FROM {$prefix}article WHERE id = " . intval($_GET['id']))->fetch_assoc();
    if ($row) $current_cat = intval($row['category_id']);
}

Q3:文章正文首行缩进?

.douhao-entry p { text-indent: 2em; }
.douhao-entry p:has(img) { text-indent: 0; }

Q4:图片不显示或 404?

检查图片路径是相对路径还是绝对路径。模板中应使用绝对路径:/templates/yourtheme/images/xxx.png。

Q5:模板中能直接用 $_GET / $_POST 吗?

可以,但所有输入必须过滤。数字用 intval(),字符串用 htmlspecialchars() 输出、$db->real_escape_string() 入库。禁止直接拼接用户输入到 SQL。

Q6:模板能调第三方 API 吗?

不能。逗号CMS 对模板代码做安全校验,禁止 curl_exec、file_get_contents 等函数。所有网络请求应由系统层处理。

Q7:如何输出富文本内容?

必须用 safe_html() 包裹:echo safe_html($article['content']);

Q8:如何加自定义广告位?

先在后台「广告位管理 → 添加广告位」创建,标识如 sidebar_ad。模板中用 get_ad_position('sidebar_ad') 调用。

Q9:模板更新时用户数据会丢失吗?

不会。模板只覆盖 /templates/{directory}/ 下的文件,不影响数据库和 /set/img/ 目录。

Q10:能否做深色模式?

可以。使用 CSS 变量 + @media (prefers-color-scheme: dark) 或用户切换按钮,模板层完全自由。

Q11:模板需要支持 IE 吗?

不强制。逗号CMS 系统后台推荐现代浏览器,前台模板可自行决定兼容范围。默认模板支持 IE 10+。

Q12:seo['latest_count'] 等参数从哪来?

来自后台「网站设置 → 参数设置」,可配置每个页面类型的显示数量。模板只需 $seo['latest_count'] 读取即可。

Q13:模板可以加自定义路由吗?

可以。在 upgrade.php 中覆盖 front_route 表配置,新增 action 和 dispatch 逻辑。但要谨慎,避免覆盖用户已有配置。

Q14:如何调试模板?

  • 临时打开 PHP 错误显示:在模板顶部加 ini_set('display_errors', 1);(仅调试用,发布前删除)
  • 用 var_dump($var); die(); 输出变量
  • 查看 /cache/ 目录下的缓存文件确认输出内容
  • 清空 /cache/ 避免缓存干扰

Q15:模板上传后总控台不显示?

检查总控台「发布主题」时状态是否设为「发布」(不是「待审」)。待审状态的主题不会推送给用户。