之前想给博客加置顶功能,搜到了 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 查询逻辑没什么问题,主要是"怎么塞进主题"这件事没处理好。抽成函数之后,模板保持干净,后面换主题或者调整布局都不受影响。如果不想动太多模板代码,这个方案应该比较省心。