Typecho 置顶文章的一种更干净的实现方式
之前想给博客加置顶功能,搜到了 moeshin 的这篇教程。代码确实能用,但直接往 index.php 里塞一大段逻辑,对现有主题布局破坏太大了。我现在用的主题结构本身就比较复杂,硬插进去之后模板变得很难维护,后面想改主题都麻烦。
于是干脆把那段逻辑抽出来,封装成几个函数,主题里只需要一行调用,剩下的该干嘛干嘛。
核心思路
原教程的思路大概是:从主题设置里读取置顶 CID,查出来手动输出,然后在普通文章循环里跳过这些 CID。但把查询、排序、跳过逻辑全摊在模板里。
改法是把"获取置顶列表"这件事完全包起来,模板只负责"拿过来用"。
封装后的函数
都丢进主题的 functions.php 就行。
1. 获取置顶文章列表
/**
* 获取置顶文章列表(仅首页第一页)
*
*/
function getStickyPosts($archive, $optionKey = 'sticky') {
static $cache = [];
$pageId = $archive->is('index') ? 'index_' . ($archive->currentPage ?? 1) : 'other';
// 缓存
if (array_key_exists($pageId, $cache)) {
return $cache[$pageId];
}
$stickyCids = parseStickyCids($optionKey);
if (empty($stickyCids) || !$archive->is('index') || ($archive->currentPage ?? 1) != 1) {
$cache[$pageId] = [];
return [];
}
$db = Typecho_Db::get();
$placeholders = implode(',', array_fill(0, count($stickyCids), '?'));
$rows = $db->fetchAll(
$db->select()->from('table.contents')
->where('type = ?', 'post')
->where('status = ?', 'publish')
->where('cid IN (' . $placeholders . ')', ...$stickyCids)
->where('created <= ?', time())
);
// 按后台设置顺序排序
$order = array_flip($stickyCids);
usort($rows, function($a, $b) use ($order) {
return ($order[$a['cid']] ?? 999) - ($order[$b['cid']] ?? 999);
});
$posts = [];
foreach ($rows as $row) {
$widget = $archive->widget('Widget_Archive@sticky' . $row['cid'], 'type=post', 'cid=' . $row['cid']);
if ($widget->have()) {
$widget->next();
$posts[] = $widget;
}
}
$cache[$pageId] = $posts;
return $posts;
}加了 static 缓存,同一页内多次调用不会重复查数据库。只在首页第一页生效,翻页后自动忽略,不影响分页逻辑。
2. 解析置顶 CID
/**
* 从主题选项中解析置顶 CID 列表
*/
function parseStickyCids($optionKey = 'sticky') {
static $cache = null;
if ($cache !== null) return $cache;
$options = Helper::options();
if (empty($options->{$optionKey})) {
$cache = [];
return [];
}
$cids = array_filter(array_map('trim', explode(',', $options->{$optionKey})), 'is_numeric');
$cache = array_values($cids);
return $cache;
}主题设置里填 1,5,8 这种逗号分隔的 CID 字符串,这个函数负责转成干净的数组。同样做了缓存,一次请求里只解析一次。
3. 判断是否为置顶
/**
* 判断一个 CID 是否在置顶列表中
*/
function isStickyCid($cid) {
$cids = parseStickyCids();
return in_array($cid, $cids);
}普通文章循环里用来跳过置顶文章,避免重复显示。
在主题里怎么用
index.php 里原来怎么写还怎么写,只需要在文章循环前面加一行:
<?php
// 获取置顶文章(仅首页第一页有效)
$stickyPosts = getStickyPosts($this);
?>
<!-- 置顶文章手动输出(仅首页) -->
<?php foreach ($stickyPosts as $sticky): ?>
<!-- 这里按你主题的卡片样式输出 -->
<article class="post-sticky">
<h2><a href="<?php $sticky->permalink(); ?>"><?php $sticky->title(); ?></a></h2>
<p><?php $sticky->excerpt(100); ?></p>
</article>
<?php endforeach; ?>
<!-- 普通文章循环 -->
<?php while ($this->next()): ?>
<?php
// 只在首页跳过已置顶的 CID,避免分类页/标签页丢失文章
if ($this->is('index') && isStickyCid($this->cid)) {
continue;
}
?>
<!-- 原来的文章卡片代码 -->
<article class="post">
...
</article>
<?php endwhile; ?>关键点:
- 置顶文章和普通文章完全分离,你可以给置顶单独写一套样式,比如加个边框、背景色或者"置顶"标签。
isStickyCid只在首页生效,分类页、标签页不会误杀文章。- 没有置顶设置时,两个函数都返回空,不会影响原有逻辑。
主题设置里加配置项
在主题的 themeConfig 里加一项,让用户填 CID:
$sticky = new Typecho_Widget_Helper_Form_Element_Text(
'sticky',
NULL,
NULL,
_t('置顶文章 CID'),
_t('填写要置顶的文章 CID,多个用英文逗号分隔,如:1,5,8')
);
$form->addInput($sticky);总结
其实原教程的 SQL 查询逻辑没什么问题,主要是"怎么塞进主题"这件事没处理好。抽成函数之后,模板保持干净,后面换主题或者调整布局都不受影响。如果不想动太多模板代码,这个方案应该比较省心。
Typecho 置顶文章的一种更干净的实现方式
https://astrsource.com/archives/Typecho-Article-Sticky.html