实现基于 Umami 后端 API 提供的页面访问量统计,通过 PHP 脚本获取当天的页面访问总量,并返回数据给前端,通过 JS 脚本将数据渲染到侧边栏的今日热门卡片中。

搭建 Umami

具体自行操作,不可以使用官方提供的方式(官方不支持 API 访问,即不提供 Token 生成)。

获取 Token

下载 Postman, 或者你熟悉的一个 HTTP 请求工具。

通过 Post 请求携带你的 Umami 账户登陆,获取 Token。

请求地址:https://[你的 Umami 部署地址]/api/auth/login

请求参数:(Json 格式)

1
2
3
4
{
"username":"[你的 Umami 账户名]",
"password":"[你的 Umami 账户密码]"
}

获取 token

获取成功会在 Response 中返回一个 Token,请妥善保管。

获取 token 成功

PHP 脚本

通过 PHP 脚本获取当天的页面访问总量,并返回数据给前端。

自行创建一个 umami_cache.json 文件(权限 755,与 PHP 脚本文件同级),用于缓存数据。

hot.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<?php
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: *");

// 配置 Umami API 的凭据
$apiConfig = [
'baseUrl' => '[你的 Umami 部署地址,如 https://umami.everfu.cn]',
'token' => '[你的 Umami Token]',
'websiteId' => '[你的 Umami 网站 ID]'
];
$cacheConfig = [
'file' => 'umami_cache.json',
'time' => 600 // 缓存时间为10分钟(600秒)
];
$path = '/p/'; // 获取文章
$site = '[你的网站地址,如 https://blog.everfu.cn]';

// 获取当前时间戳(毫秒级)
$currentTimestamp = time() * 1000;

// Umami API 的起始时间戳(毫秒级)
$startTimestamps = [
'today' => strtotime("today") * 1000,
'yesterday' => strtotime("yesterday") * 1000,
'lastMonth' => strtotime("-1 month") * 1000,
'lastYear' => strtotime("-1 year") * 1000
];

// 定义 Umami API 请求函数
function fetchUmamiData($config, $startAt, $endAt, $type = 'url', $limit = 500) {
$url = "{$config['baseUrl']}/api/websites/{$config['websiteId']}/metrics?" . http_build_query([
'startAt' => $startAt,
'endAt' => $endAt,
'type' => $type,
'limit' => $limit
]);
$options = [
'http' => [
'method' => 'GET',
'header' => [
"Authorization: Bearer {$config['token']}",
"Content-Type: application/json"
]
]
];
$context = stream_context_create($options);
$response = @file_get_contents($url, false, $context);

if ($response === FALSE) {
$error = error_get_last();
echo json_encode(["error" => "Error fetching data: " . $error['message'], "url" => $url]);
return null;
}

global $path;
return array_values(array_filter(json_decode($response, true), fn($item) => strpos($item['x'], $path) === 0));
}

// 检查缓存文件是否存在且未过期
function isCacheValid($cacheConfig) {
return file_exists($cacheConfig['file']) && (time() - filemtime($cacheConfig['file']) < $cacheConfig['time']);
}

// 从缓存中读取数据
function readCache($cacheConfig) {
return file_get_contents($cacheConfig['file']);
}

// 将数据写入缓存
function writeCache($cacheConfig, $data) {
file_put_contents($cacheConfig['file'], json_encode($data));
}

// 获取并处理数据
function getProcessedData($config, $startTimestamps, $currentTimestamp) {
$data = fetchUmamiData($config, $startTimestamps['today'], $currentTimestamp);
if (!$data) return [];

$responseData = array_map(fn($item) => ["url" => $item['x'], "pv" => $item['y']], $data);
usort($responseData, fn($a, $b) => $b['pv'] <=> $a['pv']);
global $site;

$processedData = [];
foreach (array_slice($responseData, 0, 5) as $item) {
$url = $item['url'];
$title = fetchPageTitle($site . $url);
$processedData[] = ["url" => $url, "title" => $title];
}

return $processedData;
}

function fetchPageTitle($url) {
$html = @file_get_contents($url);
if ($html === FALSE) {
return "Unknown Title";
}

preg_match("/<h1 class=\"post-title\">(.*?)<\/h1>/is", $html, $matches);
return $matches[1] ?? "Unknown Title";
}

// 主逻辑
if (isCacheValid($cacheConfig)) {
echo readCache($cacheConfig);
} else {
$responseData = getProcessedData($apiConfig, $startTimestamps, $currentTimestamp);
writeCache($cacheConfig, $responseData);
echo json_encode($responseData);
}
?>

插入侧边栏卡片(Solitude主题适用)

  1. 新建或修改 aside.yaml 文件,一般在 source/_data 目录下,增加以下内容。

    1
    2
    3
    4
    5
    - name: hot
    title: 今日热门
    class: card-hotpost
    icon: fas fa-fire
    content_id: card-hotpost
  2. 新建 hot 目录,在 hot 目录下新建 hot.jshot.css 文件,内容如下。

    hot.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    document.addEventListener("DOMContentLoaded", () => {
    initializeHot();
    });

    document.addEventListener("pjax:complete", () => {
    initializeHot();
    });

    function initializeHot() {
    const url = "[你的 PHP 脚本地址,如 https://api.everfu.cn/hot/]";
    const e = document.getElementById("card-hotpost");
    if (e) {
    fetch(url)
    .then(response => response.json())
    .then(data => {
    let rank = 0;
    data.forEach(item => {
    rank++;
    const hotItem = document.createElement("a");
    hotItem.classList.add("hot-post-link");
    hotItem.setAttribute("href", item.url);
    hotItem.innerHTML = `<span class="post-rank rank-${rank == 1 ? "1" : "2"}">${rank}</span><div class="post-title-container"><span class="post-title">${item.title}</span></div>`;
    e.appendChild(hotItem);
    });
    });
    }
    }
    hot.css
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    #card-hotpost {
    display: flex;
    flex-direction: column;
    gap: 4px
    }

    #card-hotpost .post-rank {
    background: var(--efu-secondbg);
    border: var(--style-border-always);
    color: var(--efu-fontcolor);
    border-radius: 8px;
    margin-right: 4px;
    width: 25px;
    height: 25px;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    margin-top: 2px
    }

    #card-hotpost .post-rank.rank-1 {
    background: var(--efu-lighttext);
    color: var(--efu-card-bg);
    border-color: var(--efu-lighttext)
    }

    .hot-post-link {
    display: flex;
    font-size: 15px;
    overflow: hidden;
    padding: .3rem;
    gap: 4px;
    border-radius: 12px
    }

    .hot-post-link .post-title-container {
    display: flex;
    align-items: center;
    flex: 1
    }

    .hot-post-link .post-title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-height: 1.38;
    flex: 1;
    overflow: hidden;
    padding-top: 2px
    }

    .hot-post-link:hover {
    background: var(--efu-lighttext);
    border-radius: 8px;
    color: var(--efu-card-bg)
    }

    #card-hotpost .hot-post-link:hover .post-rank {
    background: var(--efu-card-bg);
    color: var(--efu-fontcolor);
    border-color: var(--efu-card-bg)
    }

    #card-hotpost .hot-post-link:hover .post-rank.rank-1 {
    background: var(--efu-maskbg);
    color: var(--efu-white);
    border-color: var(--efu-maskbg)
    }
  3. 在主题配置文件中,找到 extends 字段,增加以下内容。

    _config.solitude.yml
    1
    2
    3
    4
    5
    extends:
    head:
    - <link rel="stylesheet" href="/hot/hot.css">
    body:
    - <script src="/hot/hot.js"></script>
  4. 在主题配置文件中,找到 aside 字段,在需要显示的页面增加 hot 卡片。

    _config.solitude.yml
    1
    2
    3
    aside:
    home:
    noSticky: "about,hot"