WordPress 友链文章适配子比主题[已实装]

前言

之前的友链文章一直使用的 Fciecle 这个项目,现在换成了 WordPress 程序,想着直接调用 WordPress 接口和数据库更方便管理。

注意事项

  • 链接需要填写 RSS 地址那一栏,否则将不会自动匹配抓取 RSS 页面。
  • 页面需要管理员手动刷新,没有设置定时任务。
  • 清除缓存按钮只有管理员登录状态下可见。
  • 抓取 RSS 内容后,会将抓取到的内容以 JSON 文件存放在 /wp-resource/fcircle 路径下。
  • 抓取结束后,会将抓取成功和失败链接以 LOG 文件存放在 /wp-resource/fcircle 路径下。
  • 代码中可以设置每个 RSS 订阅获取的数量,和前台分页时每页文章数量。

部署办法

1. 创建主题模板文件

创建模板文件 fcircle.php(名称自定义) 放在 wp-content/themes/zibll/pages/ 路径下

<?php
/*
Template Name: RSS 朋友圈
*/

date_default_timezone_set('Asia/Shanghai');
require_once(ABSPATH . WPINC . '/class-simplepie.php');

// 配置参数
$posts_per_feed = 6;      // 每个订阅源获取的文章数量
$storage_path = ABSPATH . 'wp-resource/fcircle/';
$article_file = $storage_path . 'article.json';
$log_file = $storage_path . 'logs.log';
$posts_per_page = 10;     // 每页显示多少篇文章(分页用)

// 确保存储目录存在
if (!file_exists($storage_path)) {
    mkdir($storage_path, 0755, true);
}

// 处理缓存清除请求(仅管理员)
if (current_user_can('manage_options') && isset($_GET['clear_rss_cache'])) {
    if (file_exists($article_file)) {
        unlink($article_file);
    }
    log_message("====== 缓存手动清除请求 ======", $log_file);
    $data = fetch_all_rss_items(get_rss_sites(), $posts_per_feed);
    save_articles_data($data, $article_file);
    wp_redirect(remove_query_arg('clear_rss_cache'));
    exit;
}

get_header();

// 获取RSS站点数据用于统计
$rss_sites = get_rss_sites();
$total_links = count($rss_sites); // 总链接数量

// 加载数据
$data = load_articles_data($article_file);
$last_updated = '从未更新';
$success_links = 0; // 成功获取的站点数量

if ($data !== false) {
    // 获取最后更新时间
    $file_time = filemtime($article_file);
    if ($file_time) {
        $time_diff = time() - $file_time;
        if ($time_diff < 3600) {
            $last_updated = floor($time_diff / 60) . ' 分钟前';
        } elseif ($time_diff < 86400) {
            $last_updated = floor($time_diff / 3600) . ' 小时前';
        } else {
            $last_updated = floor($time_diff / 86400) . ' 天前';
        }
    }
    
    // 计算成功获取的站点数量(去重)
    $sources = array_unique(array_column((array)$data, 'source_name'));
    $success_links = count($sources);
} else {
    $data = fetch_all_rss_items($rss_sites, $posts_per_feed);
    save_articles_data($data, $article_file);
    // 计算成功获取的站点数量(去重)
    $sources = array_unique(array_column((array)$data, 'source_name'));
    $success_links = count($sources);
}

// 分页处理
$total_posts = count($data);
$paged = isset($_GET['rss_page']) ? max(1, intval($_GET['rss_page'])) : 1;
$total_pages = max(1, ceil($total_posts / $posts_per_page));
$offset = ($paged - 1) * $posts_per_page;
$current_posts = array_slice($data, $offset, $posts_per_page);

// 输出标题和控制区
echo '<div class="zib-title rss-header">';
echo '<div class="header-title">友链文章</div>';
echo '<div class="stats-container">';
echo '<div class="stat-item"><span class="stat-label">总共友链数量 </span> <span class="stat-label-num"> ' . $total_links . ' </span><span class="stat-label"> 条</span></div>';
echo '<div class="stat-item"><span class="stat-label">成功获取数量 </span> <span class="stat-label-num"> ' . $success_links . ' </span><span class="stat-label"> 条</span></div>';
echo '<div class="stat-item"><span class="stat-label">获取文章数量 </span> <span class="stat-label-num"> ' . $total_posts . ' </span><span class="stat-label"> 篇</span></div>';
echo '</div>';
echo '<div class="header-actions">';
echo '<p class="update-info">最后更新: ' . $last_updated . '</p>';
if (current_user_can('manage_options')) {
    echo '<a href="' . add_query_arg('clear_rss_cache', '1') . '" class="clear-cache-btn"><i class="fa fa-trash"></i> 清除缓存</a>';
}
echo '</div>';
echo '</div>';

// ======================== RSS 数据函数 ========================
function get_rss_sites() {
    global $wpdb;
    return $wpdb->get_results(
        $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}links WHERE link_visible = 'Y' AND TRIM(link_rss) != %s",
            ''
        )
    );
}

function fetch_all_rss_items($rss_sites, $posts_per_feed = 5, $timeout = 20) {
    global $log_file;
    
    log_message("====== 开始新的RSS抓取任务: " . date('Y-m-d H:i:s') . " ======", $log_file);
    log_message("发现 " . count($rss_sites) . " 个有效RSS站点,开始处理...", $log_file);
    
    $mh = curl_multi_init();
    $chs = [];
    $results = [];

    foreach ($rss_sites as $i => $site) {
        $site_name = $site->link_name;
        if (!isset($site->link_rss) || trim($site->link_rss) === '') {
            continue;
        }
        
        $rss_url = $site->link_rss;
        log_message("正在处理站点: {$site_name},RSS地址: {$rss_url}", $log_file);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $rss_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_USERAGENT => 'Mozilla/5.0',
        ]);
        curl_multi_add_handle($mh, $ch);
        $chs[$i] = [
            'ch' => $ch,
            'site' => $site
        ];
    }

    $running = null;
    do {
        curl_multi_exec($mh, $running);
        curl_multi_select($mh);
    } while ($running > 0);

    foreach ($chs as $i => $item) {
        $ch = $item['ch'];
        $site = $item['site'];
        $site_name = $site->link_name;
        $body = curl_multi_getcontent($ch);
        curl_multi_remove_handle($mh, $ch);
        curl_close($ch);

        if (strlen(trim($body)) < 100) {
            $msg = "RSS内容过短(可能无效),跳过: {$site_name}";
            log_message("警告: {$msg}", $log_file);
            continue;
        }

        $feed = new SimplePie();
        $feed->set_stupidly_fast(true);
        $feed->set_raw_data(ltrim(preg_replace('/^\xEF\xBB\xBF/', '', $body)));
        $feed->set_useragent('Mozilla/5.0');
        $feed->enable_cache(false);
        $feed->init();

        if (!$feed->error()) {
            $items = $feed->get_items(0, $posts_per_feed);

            $has_valid_title = false;
            foreach ($items as $item) {
                if (trim($item->get_title()) !== '') {
                    $has_valid_title = true;
                    break;
                }
            }

            if (!$has_valid_title) {
                $msg = "所有条目标题为空,跳过: {$site_name}";
                echo "<!-- {$msg} -->";
                log_message("警告: {$msg}", $log_file);
                continue;
            }

            $item_count = 1;
            foreach ($items as $item) {
                $results[] = (object)[
                    'title' => strip_tags(html_entity_decode((string)$item->get_title())),
                    'link' => $item->get_link(),
                    'date' => $item->get_date('U'),
                    'source_name' => $site->link_name,
                    'source_link' => $site->link_url,
                    'source_avatar' => $site->link_image,
                    'item_number' => $item_count++
                ];
            }
            log_message("成功抓取 {$site_name} 的 " . count($items) . " 篇文章", $log_file);
        } else {
            $msg = "RSS解析错误: {$site_name}, 错误: {$feed->error()}";
            log_message("错误: {$msg}", $log_file);
        }
    }

    curl_multi_close($mh);
    usort($results, fn($a, $b) => $b->date <=> $a->date);
    
    log_message("抓取完成,共获取 " . count($results) . " 篇有效文章", $log_file);
    log_message("====== 抓取任务结束 ======" . PHP_EOL, $log_file);
    
    return $results;
}

function save_articles_data($data, $file_path) {
    global $log_file;
    $json_data = json_encode($data);
    if (file_put_contents($file_path, $json_data) !== false) {
        log_message("成功保存 " . count($data) . " 条文章数据到 " . $file_path, $log_file);
        return true;
    } else {
        log_message("保存文章数据失败: " . $file_path, $log_file);
        return false;
    }
}

function load_articles_data($file_path) {
    if (file_exists($file_path) && filesize($file_path) > 0) {
        $json_data = file_get_contents($file_path);
        $data = json_decode($json_data);
        if (json_last_error() === JSON_ERROR_NONE) {
            return $data;
        }
    }
    return false;
}

function log_message($message, $log_file) {
    $time = date('Y-m-d H:i:s');
    $log_entry = "[$time] $message" . PHP_EOL;
    file_put_contents($log_file, $log_entry, FILE_APPEND);
}
// ======================== END FUNC ========================
?>

<style>

/* 子比主题标题样式 - 确保与列表同宽 */
.zib-title.rss-header {
	position: relative;
	margin: 0 auto 20px;
	padding: 25px;
	background-color: var(--main-bg-color);
	border-radius: 8px;
	border-left: 4px solid var(--theme-color);
	border-right: 4px solid var(--theme-color);
	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
	max-width: 964px;
	width: calc(100% - 36px);
	box-sizing: border-box;
	margin-left: auto;
	margin-right: auto;
	display: flex;
	flex-direction: column;
	gap: 20px;
}

/* 标题样式 - 水平居中,垂直方向在上方1/4,按比例放大 */
.header-title {
	font-size: 2.2em;
	color: var(--text-color);
	font-weight: 700;
	text-align: center;
	margin: 0;
	line-height: 1.3;
	padding-bottom: 10px;
	border-bottom: 1px solid var(--border-color);
}

/* 统计数据容器 - 中间1/2内容,增强模块感 */
.stats-container {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 15px;
	background-color: rgba(0, 0, 0, 0.02);
	border-radius: 6px;
	gap: 15px;
	flex-wrap: wrap;
}

/* 中间统计项样式 */
.stat-item {
	flex: 1;
	text-align: center;
	font-size: 1.2em;
	color: var(--text-color);
	line-height: 1.6;
	min-width: 150px;
}

/* 统计项标签加粗 */
.stat-label {
	font-weight: 700;
	color: var(--theme-color);
}

.stat-label-num {
	font-weight: 700;
}

/* 底部操作区 */
.header-actions {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 10px 0 5px;
	margin-top: 5px;
	flex-wrap: wrap;
	gap: 10px;
}

.update-info {
	margin: 0;
	color: var(--main-color);
	font-size: 0.95em;
}

/* 清除缓存按钮 - 确保不透明 */
.clear-cache-btn {
	color: var(--main-color);
	border: none;
	padding: 8px 16px;
	border-radius: 4px;
	cursor: pointer;
	font-size: 0.95em;
	text-decoration: none;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	opacity: 1;
}

.rss-list {
	list-style: none;
	padding: 0;
	margin: 0 auto;
	max-width: 1000px;
}

/* 卡片式列表样式 - 确保与标题宽度一致 */
.rss-item {
	display: grid;
	grid-template-columns: 1fr auto;
	grid-template-rows: auto auto;
	grid-template-areas: "title number" "source date";
	padding: 20px;
	margin-bottom: 15px;
	border-radius: 8px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
	background-color: var(--main-bg-color);
	border: 1px solid var(--border-color);
	gap: 12px;
	align-items: center;
	max-width: 1000px;
	width: calc(100% - 36px);
	box-sizing: border-box;
	margin-left: auto;
	margin-right: auto;
}

.rss-title {
	grid-area: title;
	margin: 0;
	font-size: 1.1em;
	line-height: 1.4;
}

.rss-title a {
	color: var(--link-color);
	text-decoration: none;
	transition: color 0.3s;
	word-break: break-word;
}

.rss-title a:hover {
	color: var(--theme-color);
	text-decoration: none;
}

.rss-number {
	grid-area: number;
	font-size: 1.8em;
	font-weight: bold;
	color: var(--theme-color);
	margin: 0;
	opacity: 0.8;
	justify-self: end;
}

.rss-source {
	grid-area: source;
	display: flex;
	align-items: center;
	gap: 10px;
	color: var(--text-secondary);
	font-size: 0.9em;
}

.rss-avatar {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	object-fit: cover;
	border: 1px solid var(--border-color);
}

.rss-avatar-placeholder {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	background-color: var(--light-bg-color);
	display: flex;
	align-items: center;
	justify-content: center;
	color: var(--text-secondary);
	font-size: 14px;
	border: 1px solid var(--border-color);
}

.rss-date {
	grid-area: date;
	margin: 0;
	color: var(--text-secondary);
	font-size: 0.9em;
	justify-self: end;
}

/* 深色模式适配 */
.dark-theme .rss-item,
[data-theme="dark"] .rss-item,
:root.dark .rss-item {
	background-color: var(--main-bg-color);
	border-color: var(--dark-border-color);
}

.dark-theme .rss-title a,
[data-theme="dark"] .rss-title a,
:root.dark .rss-title a {
	color: var(--dark-link-color);
}

.dark-theme .rss-number,
[data-theme="dark"] .rss-number,
:root.dark .rss-number {
	color: var(--dark-theme-color);
}

.dark-theme .stats-container,
[data-theme="dark"] .stats-container,
:root.dark .stats-container {
	background-color: rgba(255, 255, 255, 0.02);
}

/* 移动端适配 - 优化布局 */
@media (max-width: 768px) {
	.zib-title.rss-header {
		margin-left: 18px;
		margin-right: 18px;
		width: calc(100% - 36px);
		padding: 18px;
		gap: 15px;
	}

	.header-title {
		font-size: 1.8em;
		padding-bottom: 8px;
	}

	.stats-container {
		flex-direction: column;
		gap: 12px;
		text-align: left;
		padding: 12px;
	}

	.stat-item {
		width: 100%;
		text-align: center;
		font-size: 1.1em;
		min-width: auto;
	}

	.header-actions {
		flex-direction: row;
		justify-content: space-between;
		align-items: center;
		gap: 10px;
		padding: 5px 0 0;
	}

	.update-info {
		font-size: 0.9em;
	}

	.clear-cache-btn {
		padding: 6px 12px;
		font-size: 0.9em;
		align-self: auto;
		margin-top: 0;
	}

	.rss-item {
		margin-left: 18px;
		margin-right: 18px;
		width: calc(100% - 36px);
		grid-template-columns: 1fr auto;
		grid-template-rows: auto auto auto;
		grid-template-areas: "title number" "date date" "source source";
		padding: 15px;
	}

	.rss-date {
		justify-self: start;
		padding-top: 5px;
	}
}

/* 电脑端样式 */
@media (min-width: 769px) {
	/* 保持电脑端放大效果 */
}

/* 分页样式 */
.rss-pagination {
	display: flex;
	justify-content: center;
	margin: 20px auto;
	gap: 8px;
	flex-wrap: wrap;
	max-width: 1000px;
	width: calc(100% - 36px);
}

.rss-pagination a,
.rss-pagination span {
	padding: 6px 12px;
	border-radius: 4px;
	border: 1px solid var(--border-color);
	background: var(--main-bg-color);
	color: var(--text-color);
	text-decoration: none;
	font-size: 0.9em;
}

.rss-pagination a:hover {
	background: var(--theme-color);
	color: #fff;
}

.rss-pagination .current {
	background: var(--theme-color);
	color: #fff;
	font-weight: bold;
}

</style>

<?php
if (empty($current_posts)) {
    echo '<p style="padding:20px; text-align:center; color:var(--text-secondary); margin:0 auto; max-width:1000px;">暂无RSS数据,请稍后刷新或稍候再试。</p>';
} else {
    echo '<ul class="rss-list" id="rss-feed-list">';
    $global_counter = $offset + 1;
    foreach ($current_posts as $item) {
        echo '<li class="rss-item">';
        echo '<h3 class="rss-title"><a href="' . esc_url($item->link) . '" target="_blank">' . esc_html($item->title) . '</a></h3>';
        echo '<p class="rss-number">' . esc_html($global_counter) . '</p>';
        echo '<div class="rss-source">';
        if (!empty($item->source_avatar)) {
            echo '<img src="' . esc_url($item->source_avatar) . '" alt="' . esc_attr($item->source_name) . '" class="rss-avatar">';
        } else {
            echo '<div class="rss-avatar-placeholder">' . esc_html(substr($item->source_name, 0, 1)) . '</div>';
        }
        echo '<a href="' . esc_url($item->source_link) . '" target="_blank">' . esc_html($item->source_name) . '</a>';
        echo '</div>';
        echo '<p class="rss-date">' . date('Y-m-d H:i', $item->date) . '</p>';
        echo '</li>';
        $global_counter++;
    }
    echo '</ul>';

    // ======= 分页 =======
    if ($total_pages > 1) {
        echo '<div class="pagenav ajax-pag">';
        
        // 上一页
        if ($paged > 1) {
            echo '<a class="prev page-numbers" href="' . esc_url(home_url('/fcircle/page/' . ($paged - 1))) . '">
                <i class="fa fa-angle-left em12"></i>
                <span class="hide-sm ml6">上一页</span>
            </a>';
        }
        
        // 首页、2、3
        for ($i = 1; $i <= min(3, $total_pages); $i++) {
            if ($i == $paged) {
                echo '<span aria-current="page" class="page-numbers current">' . $i . '</span>';
            } else {
                echo '<a class="page-numbers" href="' . esc_url(home_url('/fcircle/page/' . $i)) . '">' . $i . '</a>';
            }
        }
        
        // 省略号和末页
        if ($total_pages > 4) {
            echo '<span class="page-numbers dots">…</span>';
            
            // 末页
            if ($paged == $total_pages) {
                echo '<span aria-current="page" class="page-numbers current">' . $total_pages . '</span>';
            } else {
                echo '<a class="page-numbers" href="' . esc_url(home_url('/fcircle/page/' . $total_pages)) . '">' . $total_pages . '</a>';
            }
        }
        
        // 下一页
        if ($paged < $total_pages) {
            echo '<a class="next page-numbers" href="' . esc_url(home_url('/fcircle/page/' . ($paged + 1))) . '">
                <span class="hide-sm mr6">下一页</span>
                <i class="fa fa-angle-right em12"></i>
            </a>';
        }
        
        echo '</div>';
    }

}
?>

<?php get_footer(); ?>

2. 修改 Nginx 伪静态文件

修改该站点伪静态规则,加入以下链接重写规则

# 为 /fcircle/page/1 格式的 URL 添加重写规则
rewrite ^/fcircle/page/([0-9]+)/?$ /index.php?pagename=fcircle&rss_page=$1 last;
  • 注意:其中的 fciecle 为友链文章页面设置的 Permalink

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 共4条
访客头像 - 云晓晨 KaiQi.Wang
欢迎您留下宝贵的见解!
提交
访客头像 - 云晓晨 KaiQi.Wang

昵称

取消
昵称表情代码图片快捷回复