逗号CMS 模板开发手册
面向模板开发者的完整技术文档 · 适用于 V6.8 及以上版本
概述
逗号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 示例 |
|---|---|---|
| index | index.php | / |
| list | category.php | /list.html、/list-2.html |
| category | category.php | /category-1.html |
| article | article.php | /article-123.html |
| search | category.php | /rs/关键词 |
| search_rs_index | search_rs.php | /rs |
| product_list | product_list.php | /product.html |
| product_detail | product_detail.php | /product-123.html |
| ubb_index | set/ubb/index.php | /ubb |
| ubb_page | set/ubb/page.php | /ubb/{slug} |
| 404 | set/404.php | 任意未知 URL |
全局变量
在 header.php、footer.php 和各类页面模板中,可以直接使用以下变量:
| 变量 | 类型 | 说明 |
|---|---|---|
$action | string | 当前 action(index / list / category / article / search / ...) |
$db | mysqli | 数据库连接对象 |
$prefix | string | 数据表前缀(默认 douhao_) |
$setting | array | 网站设置数组 |
$seo | array | 当前页面 SEO 数组(title / keywords / description) |
$template_path | string | 当前模板目录绝对路径(带尾部斜杠) |
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('自定义标识') 调用。自定义广告位默认使用「随机模式」。搜索系统集成
搜索框跳转
搜索框只需跳转到 /rs/{关键词}:
<form onsubmit="return doSearch(event)">
<input type="text" id="kw" placeholder="输入关键词搜索...">
<button type="submit">搜索</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>
分词搜索
<?php
$keyword = trim($_GET['keyword']);
$keywords = split_search_keywords($keyword);
$result = search_articles_by_keywords($keywords, 1, 20);
?>
热搜标签系统
获取热搜词
<?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 | 搜索次数(热度) |
status | 0=待审,1=已发布 |
is_top | 0=普通,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);
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 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
$uri | string | 当前访问的 URI 路径(不含域名和查询串) |
$setting | array | 网站设置 |
$db | mysqli | 数据库连接 |
$prefix | string | 数据表前缀 |
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;
}
return true; 告诉系统已处理。route_dispatch() 返回 false 时,系统会回退到 index.php 内置的分发逻辑。4. 内置 action 与参数
| action | 关键 $_GET | 说明 |
|---|---|---|
| index | - | 首页 |
| list | page | 全部文章列表,分页参数 page |
| category | id、page | 分类页,id 是分类 ID 或 hash/md5 |
| article | id | 文章详情页,id 是文章 ID 或 hash/md5 |
| search | keyword、page | 搜索页 |
| search_rs_index | - | 热搜标签聚合页 |
| product_list | id、page | 产品列表 / 产品分类页 |
| product_detail | id | 产品详情页 |
| ubb_index | - | 留言本首页 |
| ubb_page | slug | 留言单页 |
| 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. 新增后台页面
步骤:
- 在
/admin/目录新建my_page.php(文件开头必须require_once __DIR__ . '/auth.php';) - 在后台路由管理中添加:
'my_page' => 'admin/my_page.php' - 在后台菜单管理中添加入口链接
<a href="?page=my_page">我的页面</a>
4. 权限控制
后台文件必须包含 auth.php,它会检查 $_SESSION['admin_id']。未登录时返回 403。
5. 安全限制
safe_parse_route_config() 校验,只允许数组字面量,禁止任何函数调用、变量、表达式。常用示例
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:模板上传后总控台不显示?
检查总控台「发布主题」时状态是否设为「发布」(不是「待审」)。待审状态的主题不会推送给用户。
逗号CMS