【Hexo】Umami-Api搭建全攻略

最近好多人都在折腾这个API,正好刚换了主题,我也来折腾下(主要别人铺好路了,不怕踩坑)

一、部署umami服务

此处使用Docker部署,需提前准备Docker环境

通过 Vercel 部署可参考文章:

通过 Railway 部署可参考文章:

使用docker-compose一键部署

在需要放入umami信息的文件夹建立docker-compose.yaml文件,填写下面的内容:

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
version: '3'
services:
umami:
container_name: umami
image: umamisoftware/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
HASH_SALT: replace-me-with-a-random-string
depends_on:
- db
restart: always
db:
container_name: umami-db
image: postgres:12-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- ./sql/schema.postgresql.sql:/docker-entrypoint-initdb.d/schema.postgresql.sql:ro
- ./umami-db-data:/var/lib/postgresql/data
restart: always

注意,umami的ports中,3000:3000可以将前面的3000更换成其他闲置端口。

然后通过ssh访问服务器后,cd进这个文件夹,然后执行docker-compose up -d即可配置完成。

绑定域名

添加一条A记录

image-20241025211139357

创建反代

image-20241025210703323

申请证书

建议配置,嫌麻烦可跳过

image-20241025211454300启用Https

建议配置,嫌麻烦可跳过

image-20241025211618295

设置信息

进入之后umami默认的用户名为admin,默认密码为umami。进入之后我们先改一下语言。

image-20241025211832785

选择中文即可

image-20241025211906872

还可以顺便修改下登录密码

image-20241025213321708

添加网站

进入设置 ->点击添加网站,只填写域名即可

image-20241025212110142

我们将统计代码插入到head中比较好。

点击网站的“编辑”,选择“跟踪代码”我们就可以看到html代码了。插入到每一个页面即可。

image-20241025212708113

Umami-API搭建

由于服务器搭建访问速度比较快,我这里直接在服务器部署了

通过 Cloudflar 部署可参考文章:

基于林木木的前端调用 Umami API 数据,首先进入Hoppscotch获取token

请求路径:https://你的地址/api/auth/login,注意POST请求

image-20241025214141954

然后访问HeoPVBridge,下载并修改umami/info.php

更改地址、token和网站id。然后部署到网站的php项目中即可。

1
2
3
4
5
6
// 配置 Umami API 的凭据
$apiBaseUrl = '你的Umami服务地址';
$token = '你的tocken';
$websiteId = '你的网站id';
$cacheFile = 'umami_cache.json';
$cacheTime = 600; // 缓存时间为10分钟(600秒)

添加域名

image-20241025222257503

创建PHP运行环境

image-20241025222048672

打开站点网站目录,上传php文件

image-20241025223334840

将php文件重命名为index.php,否则会因为找不到启动文件出现403错误

image-20241025223304267

配置好后访问域名,出现下面这个界面代表成功

image-20241025223534684

关于页配置

需要强调一下我使用的主题是Anzhiyu主题

themes/anzhiyu/layout/includes/head.pug中添加

1
2
3
4
5
6
//- Umami
if theme.Umami
if theme.Umami.umami_url
script(async defer src=`${theme.Umami.umami_url_js}` data-website-id=`${theme.Umami.umami_id}` data-host-url=`${theme.Umami.umami_url}`)
else
script(async defer src=`${theme.Umami.umami_url_js}` data-website-id=`${theme.Umami.umami_id}`)

然后修改 themes/anzhiyu/source/css/_page/about.styl

1
2
3
4
// 大致在1255行,找到
if (hexo-config('LA.enable')) {
// 替换为
if (hexo-config('LA.enable') || hexo-config('Umami.enable')) {

接着修改 themes/anzhiyu/layout/includes/page/about.pug

在大概91行的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.author-content
if theme.LA.enable
- let cover = item.statistic.cover
.about-statistic.author-content-item(style=`background: url(${cover}) top / cover no-repeat;`)
.card-content
.author-content-item-tips 数据
span.author-content-item-title 访问统计
#statistic
.post-tips
| 统计信息来自
a(href='https://invite.51.la/1NzKqTeb?target=V6', target='_blank', rel='noopener nofollow') 51la网站统计
.banner-button-group
- let link = item.statistic.link
- let text = item.statistic.text
a.banner-button(onclick=`pjax.loadUrl("${link}")`)
i.anzhiyufont.anzhiyu-icon-arrow-circle-right
|
span.banner-button-text=text

替换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.author-content
if theme.LA.enable || theme.Umami.enable
.about-statistic.author-content-item(style=`background: url(${cover}) top / cover no-repeat;`)
.card-content
.author-content-item-tips 数据
span.author-content-item-title 访问统计
#statistic
if theme.LA.enable
.post-tips
| 统计信息来自
a(href='https://www.51.la/', target='_blank', rel='noopener nofollow') 51LA统计
else if theme.Umami.enable
.post-tips
| 统计信息来自
a(href='https://um.ruom.top', target='_blank', rel='noopener nofollow') Umami统计
.banner-button-group
- let link = item.statistic.link
- let text = item.statistic.text
a.banner-button(onclick=`pjax.loadUrl("${link}")`)
i.anzhiyufont.anzhiyu-icon-arrow-circle-right
|
span.banner-button-text=text

继续修改 直接搜 - const ck = theme.LA.ck 把下面的全部替换

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// 复制即是正常缩进(两个字符) 需要删除本行
//- Umami 统计 和 51LA 统计
if theme.Umami && theme.Umami.enable
script(defer).
(function() {
const umamiApiUrl = "#{url_for(theme.Umami.umami_api)}";
fetch(umamiApiUrl)
.then(res => res.json())
.then(data => {
let title = {
"today_uv": "今日人数",
"today_pv": "今日访问",
"yesterday_uv": "昨日人数",
"yesterday_pv": "昨日访问",
"last_month_pv": "本月访问",
"last_year_pv": "本年访问"
};
let s = document.getElementById("statistic");
for (let key in data) {
if (data.hasOwnProperty(key) && title[key]) {
s.innerHTML += `<div><span>${title[key]}</span><span id="${key}">${data[key]}</span></div>`;
}
}
initCountUp(data, title);
})
.catch(error => console.error('Error:', error));
})();
else
script(defer).
function initAboutPage() {
fetch("https://v6-widget.51.la/v6/#{ck}/quote.js")
.then(res => res.text())
.then(data => {
let title = ["最近活跃", "今日人数", "今日访问", "昨日人数", "昨日访问", "本月访问", "总访问量"];
let num = data.match(/(<\/span><span>).*?(\/span><\/p>)/g);

num = num.map(el => {
let val = el.replace(/(<\/span><span>)/g, "");
let str = val.replace(/(<\/span><\/p>)/g, "");
return str;
});

let statisticEl = document.getElementById("statistic");

// 自定义不显示哪个或者显示哪个,如下为不显示 最近活跃访客 和 总访问量
let statistic = [];
for (let i = 0; i < num.length; i++) {
if (!statisticEl) return;
if (i == 0) continue;
statisticEl.innerHTML +=
"<div><span>" + title[i] + "</span><span id=" + title[i] + ">" + num[i] + "</span></div>";
queueMicrotask(() => {
statistic.push(
new CountUp(title[i], 0, num[i], 0, 2, {
useEasing: true,
useGrouping: true,
separator: ",",
decimal: ".",
prefix: "",
suffix: "",
})
);
});
}

let statisticElement = document.querySelector(".about-statistic.author-content-item");
function statisticUP() {
if (!statisticElement) return;

const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
for (let i = 0; i < num.length; i++) {
if (i == 0) continue;
queueMicrotask(() => {
statistic[i - 1].start();
});
}
observer.disconnect(); // 停止观察元素,因为不再需要触发此回调
}
});
};

const options = {
root: null,
rootMargin: "0px",
threshold: 0
};
const observer = new IntersectionObserver(callback, options);
observer.observe(statisticElement);
}

statisticUP();
initCountUp({}, {});
});

initAnimation();
}
if (typeof gsap === "object") {
initAboutPage()
} else {
getScript("!{url_for(theme.asset.gsap_js)}").then(initAboutPage);
}

//- 初始化 countup.js
script(defer).
function initCountUp(data, title) {
const elements = [];

for (let key in data) {
if (data.hasOwnProperty(key) && title[key]) {
const element = document.getElementById(key);
if (element) {
elements.push({ id: key, value: data[key], element: element });
}
}
}

const selfInfoContentYearElement = document.getElementById("selfInfo-content-year");
if (selfInfoContentYearElement) {
elements.push({ id: "selfInfo-content-year", value: #{selfInfoContentYear}, element: selfInfoContentYearElement });
}

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const target = elements.find(el => el.element === entry.target);
if (target) {
const countUp = new CountUp(target.id, 0, target.value, 0, 2, {
useEasing: true,
useGrouping: target.id === "selfInfo-content-year" ? false : true,
separator: ",",
decimal: ".",
prefix: "",
suffix: "",
});
countUp.start();
observer.unobserve(entry.target);
}
}
});
}, { threshold: 0 });

elements.forEach(el => observer.observe(el.element));
}

//- 独立鼠标跟随动画
script(defer).
function initAnimation() {
var pursuitInterval = null;
pursuitInterval = setInterval(function () {
const show = document.querySelector("span[data-show]");
const next = show.nextElementSibling || document.querySelector(".first-tips");
const up = document.querySelector("span[data-up]");

if (up) {
up.removeAttribute("data-up");
}

show.removeAttribute("data-show");
show.setAttribute("data-up", "");

next.setAttribute("data-show", "");
}, 2000);

document.addEventListener("pjax:send", function () {
pursuitInterval && clearInterval(pursuitInterval);
});

var helloAboutEl = document.querySelector(".hello-about");
helloAboutEl.addEventListener("mousemove", evt => {
const mouseX = evt.offsetX;
const mouseY = evt.offsetY;
gsap.set(".cursor", {
x: mouseX,
y: mouseY,
});

gsap.to(".shape", {
x: mouseX,
y: mouseY,
stagger: -0.1,
});
});
}
if (typeof gsap === "object") {
initAnimation()
} else {
getScript("!{url_for(theme.asset.gsap_js)}").then(initAnimation);
}

最后在主题的配置内添加

1
2
3
4
5
6
7
# Umami
Umami:
enable: true # 开关
umami_url_js: https://um.ruom.top/script.js # 填写 umami js地址 可以使用第三方CDN加速但需要配置下面的 umami_url
umami_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # 填写 umami 统计 ID
umami_api: https://xxxxx.xxx.xx/ # 填写 umami API 地址
umami_url: # https://um.ruom.top 填写 umami 服务器地址 使用 CDN 加速 Umami 静态资源后需配置此项

效果如下:

image-20241025232812797