文章摘要
GPT
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结
投诉

实现基于 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"