在移植大叔的Cherry主题过程中,发现WordPress版本有一项功能是文章内容中插入其他文章的卡片,直接删掉该模块有些可惜,遂研究了下该如何实现,检索后发现已经有短代码插件可以使用,但不太符合需求,所以考虑利用functions.php文件来实现,预览效果如下文章卡片。

基于PHP-GD实现的单文件图片缩略图API

基于PHP实现的缩略图API在Github上有现成的,但过于老旧,遂用DeepSeek写了一个,基于PHP的GD扩展实现,支持域名白名单,本地缓存,临时文件隔离,过期缓存文件清理,需要在配置中修改域名白名单及缓存文件存放目录。 考虑到...

基于PHP-GD实现的单文件图片缩略图API

首先得捋清楚需求,在编辑文章过程中,可以通过插入形如[post id=""10""]这样的短代码,在文章进行解析时,将匹配到的短代码转化为文章卡片,根据图中的内容可以知道,文章卡片中需要调取ID为10的文章中的文章标题、文章链接、文章摘要、文章缩略图这些字段,直接贴出完成代码。

// 文章短代码
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('ShortcodeParser', 'postCard');

class ShortcodeParser
{
    public static function postCard($content, $widget)
    {
        $pattern = '/\[post\s+id=[""\']?(\d+)[""\']?\]/i';
        return preg_replace_callback($pattern, function($matches) {
            return self::buildPostCard($matches[1]);
        }, $content);
    }

    private static function buildPostCard($cid)
    {
        try {
            $db = Typecho_Db::get();
            $options = Typecho_Widget::widget('Widget_Options');
    
            $post = $db->fetchRow($db->select('title', 'text', 'slug', 'created', 'modified')
            ->from('table.contents')
            ->where('cid = ?', intval($cid))
            ->where('type = ?', 'post')
            ->where('status = ?', 'publish')
            ->limit(1));
    
            if (!$post) return '文章不存在';
    
            // 根据路由配置动态选择生成方式
            $routeExists = (bool)Typecho_Router::get('post');
            $permalink = $routeExists 
                ? Typecho_Common::url(
                    Typecho_Router::url(
                        'post', 
                        [
                            'slug' => $post['slug'],
                            'category' => self::getFirstCategory($cid)
                        ]
                    ),
                    $options->siteUrl
                )
                : $options->siteUrl . '?cid=' . $cid;

            
            $excerpt = self::getExcerpt($post['text'], 50);
            $thumb = $options->Thumb . '?url=' . self::getPostThumb($cid, $options) . '&w=200&h=140';
    
            return <<<HTML
            <div class=""post_go"">
                <div class=""post_left"">
                    <img decoding=""async"" src=""{$thumb}"" alt=""{$post['title']}"">
                    <div class=""post_info"">
                        <div class=""post_name_h2"">{$post['title']}</div>
                        <p>{$excerpt}</p>
                    </div>
                </div>
                <a class=""post_url"" href=""{$permalink}"">阅读全文
                    <i class=""bi bi-arrow-right ms-2""></i>
                </a>
            </div>
            HTML;
        } 
        catch (Exception $e) {
            return '文章加载失败:'.$e->getMessage();
        }
    }    

    private static function getFirstCategory($cid)
    {
        $db = Typecho_Db::get();
        return $db->fetchRow($db->select('slug')
            ->from('table.metas')
            ->join('table.relationships', 'table.relationships.mid = table.metas.mid')
            ->where('table.relationships.cid = ?', $cid)
            ->where('table.metas.type = ?', 'category')
            ->limit(1))['slug'] ?? '';
    }
    
    private static function getExcerpt($text, $length)
    {
        $cleanText = trim(strip_tags($text));
        return Typecho_Common::subStr($cleanText, 0, $length, '...');
    }

    private static function getPostThumb($cid, $options)
    {

        $uploadBase = defined('__TYPECHO_UPLOAD_URL__') 
        ? rtrim(__TYPECHO_UPLOAD_URL__, '/') 
        : $options->siteUrl;
        
        $default = $options->Image ?? '';
    
        $db = Typecho_Db::get();
        $attachments = $db->fetchAll($db->select('text')
            ->from('table.contents')
            ->where('parent = ?', $cid)
            ->where('type = ?', 'attachment'));
    
        foreach ($attachments as $attach) {
            $meta = unserialize($attach['text']);
            if (isset($meta['mime']) && strpos($meta['mime'], 'image/') === 0) {
                return Typecho_Common::url($meta['path'], $uploadBase);
            }
        } 
        return $default;
    }    
}

上面是functions.php中的代码,将该段代码放入主题的functions.php文件中,当文章内容中有类似[post id=""10""]的内容就会被解析成如下html结构。

<div class=""post_go"">
    <div class=""post_left"">
        <img decoding=""async"" src=""{$thumb}"" alt=""{$post['title']}"">
        <div class=""post_info"">
            <div class=""post_name_h2"">{$post['title']}</div>
            <p>{$excerpt}</p>
        </div>
    </div>
    <a class=""post_url"" href=""{$widget->permalink}"">阅读全文
        <i class=""bi bi-arrow-right ms-2""></i>
    </a>
</div>

注意:在文章缩略图的函数中,因为个人使用的原因,是通过getPostThumb()从获取文章第一个图片附件,若附件中无图片则调用主题设置中的Image字段设置作为默认缩略图,你可能需要在主题中加入如下配置项。

$Image = new Typecho_Widget_Helper_Form_Element_Text('Image', NULL, NULL, _t('默认缩略图'));
$form->addInput($Image);  

实际使用该段代码时,你可能会发现文章列表中使用<?php $this->excerpt(100, '...'); ?>输出的摘要部分会存在形如[post id=""10""]的短代码,如果介意的话可以将下面代码加入functions.php中,并在文章列表循环中使用<?php echo clearExcerpt($this->excerpt); ?>来输出摘要。

// 摘要过滤
function clearExcerpt($content, $max_length = 150) {
    $clean = preg_replace('/\[[^]]+\]/', '', $content);
    $clean = strip_tags($clean);
    
    $clean = htmlspecialchars_decode($clean);
    $clean = str_replace(['“', '”'], '""', $clean);
    
    $clean = preg_replace('/\s+/', ' ', $clean);
    return Typecho_Common::subStr($clean, 0, $max_length, '...');
}