moecounter的原作者的网站访问速度太慢,多数情况下计数器无法显示,我追求的是比较稳定的方案,因此选择了自建。由于我对于各种知识的理解还不充足,而且文笔也一般,大家将就着看吧。网上已有大量的自建教程,现在ai很发达,有不懂的直接问ai,展示的效果如下图所示
创建node环境,配置如图

启动命令
pnpm startNodejs版本:25.8.0
端口自己填,填完之后去安全组打开。
填完这些内容就会生成一个项目文件夹里面是空的,点击项目目录,将项目的zip压缩包解压并上传至项目目录。项目地址:https://github.com/journey-ad/Moe-Counter
查看一下自己的日志(容器页面),看有没有成功启动(成功启动会显示Your app is listening on port 3001)。但是这个时候还打不开,需要自己配置一下反向代理,就用1panel的网站功能即可轻松配置,这里不再过多赘述(或许我以后闲的时候会回来补充)
点开自己的代理地址,就能成功访问页面了。(会有图片无法显示)
还没完,需要在网站页面修改自己的nginx配置(openresty也一样),server_name需要改成自己的代理地址。
server {
listen 80 ;
listen 443 ssl ;
server_name <your address>;
index index.php index.html index.htm default.php default.htm default.html;
access_log /www/sites/moecounter.sakurafishermua.top/log/access.log main;
error_log /www/sites/moecounter.sakurafishermua.top/log/error.log;
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
return 404;
}
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# root /www/sites/moecounter.sakurafishermua.top/index; # 建议注释
http2 on;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
ssl_certificate /www/sites/moecounter.sakurafishermua.top/ssl/fullchain.pem;
ssl_certificate_key /www/sites/moecounter.sakurafishermua.top/ssl/privkey.pem;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
error_page 497 https://$host$request_uri;
proxy_set_header X-Forwarded-Proto https;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
# include /www/sites/moecounter.sakurafishermua.top/proxy/*.conf; # 避免重复location
}
修改项目目录里的index.js文件(直接复制就行)
"use strict";
const path = require('path');
require('dotenv').config();
const express = require("express");
const compression = require("compression");
const { z } = require("zod");
const db = require("./db");
const { themeList, getCountImage } = require("./utils/themify");
const { cors, ZodValid } = require("./utils/middleware");
const { randomArray, logger } = require("./utils");
const app = express();
//app.use(express.static("assets"));
app.use(express.static(path.join(__dirname, "assets")));
app.use(compression());
app.use(cors());
app.set("view engine", "pug");
app.get('/', (req, res) => {
const site = process.env.APP_SITE || `${req.protocol}://${req.get('host')}`
const ga_id = process.env.GA_ID || null
res.render('index', {
site,
ga_id,
themeList,
})
});
// get the image
app.get(["/@:name", "/get/@:name"],
ZodValid({
params: z.object({
name: z.string().max(32),
}),
query: z.object({
theme: z.string().default("moebooru"),
padding: z.coerce.number().int().min(0).max(16).default(7),
offset: z.coerce.number().min(-500).max(500).default(0),
align: z.enum(["top", "center", "bottom"]).default("top"),
scale: z.coerce.number().min(0.1).max(2).default(1),
pixelated: z.enum(["0", "1"]).default("1"),
darkmode: z.enum(["0", "1", "auto"]).default("auto"),
// Unusual Options
num: z.coerce.number().int().min(0).max(1e15).default(0), // a carry-safe integer, less than `2^53-1`, and aesthetically pleasing in decimal.
prefix: z.coerce.number().int().min(-1).max(999999).default(-1)
})
}),
async (req, res) => {
const { name } = req.params;
let { theme = "moebooru", num = 0, ...rest } = req.query;
// This helps with GitHub's image cache
res.set({
"content-type": "image/svg+xml",
"cache-control": "max-age=0, no-cache, no-store, must-revalidate",
});
const data = await getCountByName(String(name), Number(num));
if (name === "demo") {
res.set("cache-control", "max-age=31536000");
}
if (theme === "random") {
theme = randomArray(Object.keys(themeList));
}
// Send the generated SVG as the result
const renderSvg = getCountImage({
count: data.num,
theme,
...rest
});
res.send(renderSvg);
logger.debug(
data,
{ theme, ...req.query },
`ip: ${req.headers['x-forwarded-for'] || req.connection.remoteAddress}`,
`ref: ${req.get("Referrer") || null}`,
`ua: ${req.get("User-Agent") || null}`
);
}
);
// JSON record
app.get("/record/@:name", async (req, res) => {
const { name } = req.params;
const data = await getCountByName(name);
res.json(data);
});
app.get("/heart-beat", (req, res) => {
res.set("cache-control", "max-age=0, no-cache, no-store, must-revalidate");
res.send("alive");
logger.debug("heart-beat");
});
const listener = app.listen(process.env.APP_PORT || 3000, () => {
logger.info("Your app is listening on port " + listener.address().port);
});
let __cache_counter = {};
let enablePushDelay = process.env.DB_INTERVAL > 0
let needPush = false;
if (enablePushDelay) {
setInterval(() => {
needPush = true;
}, 1000 * process.env.DB_INTERVAL);
}
async function pushDB() {
if (Object.keys(__cache_counter).length === 0) return;
if (enablePushDelay && !needPush) return;
try {
needPush = false;
logger.info("pushDB", __cache_counter);
const counters = Object.keys(__cache_counter).map((key) => {
return {
name: key,
num: __cache_counter[key],
};
});
await db.setNumMulti(counters);
__cache_counter = {};
} catch (error) {
logger.error("pushDB is error: ", error);
}
}
async function getCountByName(name, num) {
const defaultCount = { name, num: 0 };
if (name === "demo") return { name, num: "0123456789" };
if (num > 0) { return { name, num } };
try {
if (!(name in __cache_counter)) {
const counter = (await db.getNum(name)) || defaultCount;
__cache_counter[name] = counter.num + 1;
} else {
__cache_counter[name]++;
}
pushDB();
return { name, num: __cache_counter[name] };
} catch (error) {
logger.error("get count by name is error: ", error);
return defaultCount;
}
}
修改/views/index.pug文件。
html
head
title='Moe Counter!'
meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
link(rel='icon', type='image/png', href='/favicon.png')
link(rel='stylesheet', href='https://cdn.jsdelivr.net/npm/normalize.css')
link(rel='stylesheet', href='https://cdn.jsdelivr.net/npm/bamboo.css')
link(rel='stylesheet/less', href='/style.less')
script(less, src='https://cdn.jsdelivr.net/npm/less')
if ga_id
<!-- Global site tag (gtag.js) - Google Analytics -->
script(async, src=`https://www.googletagmanager.com/gtag/js?id=${ga_id}`)
script.
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '#{ga_id}');
function _evt_push(type, category, label) {
gtag('event', type, {
'event_category' : category,
'event_label' : label
});
}
script.
var __global_data = { site: "#{site}" };
body
h1#main_title
i Moe Counter!
h3 How to use
p Set a unique id for your counter, replace
code :name
| in the url, That's it!
h5 SVG address
code /@:name
h5 Img tag
code <img src="/@:name" alt=":name" />
h5 Markdown
code 
h5 e.g.
img(src='/@index' alt="Moe Counter!")
details#themes
summary#more_theme(onclick='_evt_push("click", "normal", "more_theme")')
h3 More theme✨
p Just use the query parameters <code>theme</code>, like this: <code>/@:name?theme=moebooru</code>
each theme in Object.keys(themeList)
div.item(data-theme=theme)
h5 #{theme}
img(data-src=`/@demo?theme=${theme}` alt=theme)
h3 Credits
ul
li: a(href='https://space.bilibili.com/703007996', target='_blank', title='A-SOUL_Official') A-SOUL
li: a(href='https://github.com/moebooru/moebooru', target='_blank', rel='nofollow') moebooru
li
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') gelbooru.com
| NSFW
li: a(href='https://icons8.com/icon/80355/star', target='_blank', rel='nofollow') Icons8
span: i And all booru site...
h3 Tool
.tool
table
thead
tr
th Param
th Description
th Value
tbody
tr
td: code name
td Unique counter name
td: input#name(type='text', placeholder=':name')
tr
td: code theme
td Select a counter image theme, default is
code moebooru
td
select#theme
option(value="random", selected) * random
each theme in Object.keys(themeList)
option(value=theme) #{theme}
tr
td: code padding
td Set the minimum length, between 1-16, default is
code 7
td: input#padding(type='number', value='7', min='1', max='32', step='1', oninput='this.value = this.value.replace(/[^0-9]/g, "")')
tr
td: code offset
td Set the offset pixel value, between -500-500, default is
code 0
td: input#offset(type='number', value='0', min='-500', max='500', step='1', oninput='this.value = this.value.replace(/[^0-9|\-]/g, "")')
tr
td: code scale
td Set the image scale, between 0.1-2, default is
code 1
td: input#scale(type='number', value='1', min='0.1', max='2', step='0.1', oninput='this.value = this.value.replace(/[^0-9|\.]/g, "")')
tr
td: code align
td Set the image align, Enum top/center/bottom, default is
code top
td: select#align(name="align")
option(value="top", selected) top
option(value="center") center
option(value="bottom") bottom
tr
td: code pixelated
td Enable pixelated mode, Enum 0/1, default is
code 1
td
input#pixelated(type='checkbox', role='switch', checked)
label(for='pixelated'): span
tr
td: code darkmode
td Enable dark mode, Enum 0/1/auto, default is
code auto
td: select#darkmode(name="darkmode")
option(value="auto", selected) auto
option(value="1") yes
option(value="0") no
tr
td(colspan=3)
h4.caption Unusual Options
tr
td: code num
td Set counter display number, 0 for disable, default is
code 0
td: input#num(type='number', value='0', min='0', max='1e15', step='1', oninput='this.value = this.value.replace(/[^0-9]/g, "")')
tr
td: code prefix
td Set the prefix number, empty for disable
td: input#prefix(type='number', value='', min='0', max='999999', step='1', oninput='this.value = this.value.replace(/[^0-9]/g, "")')
button#get(onclick='_evt_push("click", "normal", "get_counter")') Generate
div
code#code
img#result
p.github
a(href='https://github.com/journey-ad/Moe-Counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
div.back-to-top
script(async, src='https://cdn.jsdelivr.net/npm/party-js@2/bundle/party.min.js')
script(async, src='/script.js')部署完之后重启项目,页面就能正常显示了,快去试试吧!