feat: Umami/ntfy 可观测性栈与 WordPress AdSense 部署

This commit is contained in:
eric
2026-08-15 23:49:19 -05:00
parent ba0b1058b0
commit 08f484780d
61 changed files with 2723 additions and 3 deletions

32
deploy/router/README.md Normal file
View File

@@ -0,0 +1,32 @@
# Audience router
`nomadro.com` is split by audience:
| Audience | Destination |
| --- | --- |
| Chinese users (`nomadro_route=zh`, via language/timezone sniff) | Next.js `:4003` |
| English users / default | WordPress `:8088` |
| Googlebot / AdsBot / other crawlers | WordPress (AdSense English surface) |
First HTML visit without a cookie gets a same-origin sniff page that sets `nomadro_route` and reloads.
Detection matches `is-china-user` language + timezone signals (any Chinese language in `navigator.languages` counts).
Router listens on `127.0.0.1:4403`.
## Deploy
```bash
python deploy/router/deploy_router.py
```
Or on server:
```bash
cd /var/www/nomadro-router
npm install --omit=dev
cp nomadro-router.service /etc/systemd/system/
systemctl enable --now nomadro-router
cp /path/to/Caddyfile /etc/caddy/Caddyfile
systemctl reload caddy
```

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Nomadro audience router (zh→Next, en→WordPress)
After=network.target docker.service nomadro.service
Wants=nomadro.service
[Service]
Type=simple
WorkingDirectory=/var/www/nomadro-router
Environment=PORT=4403
Environment=NEXT_UPSTREAM=http://127.0.0.1:4003
Environment=WP_UPSTREAM=http://127.0.0.1:8088
ExecStart=/usr/bin/node /var/www/nomadro-router/server.mjs
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,8 @@
{
"name": "nomadro-router",
"private": true,
"type": "module",
"dependencies": {
"http-proxy": "^1.18.1"
}
}

160
deploy/router/server.mjs Normal file
View File

@@ -0,0 +1,160 @@
/**
* Audience router for nomadro.com
* - Chinese users → Next.js :4003
* - English users / bots / default → WordPress :8088
*
* Detection mirrors is-china-user primary signals (language + timezone),
* evaluated in a same-origin sniff page (no external CDN).
*/
import http from "node:http";
import httpProxy from "http-proxy";
const PORT = Number(process.env.PORT || 4403);
const NEXT = process.env.NEXT_UPSTREAM || "http://127.0.0.1:4003";
const WP = process.env.WP_UPSTREAM || "http://127.0.0.1:8088";
const COOKIE = "nomadro_route";
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
const proxy = httpProxy.createProxyServer({
xfwd: true,
changeOrigin: false,
});
proxy.on("error", (err, _req, res) => {
console.error("proxy error", err.message);
if (res && !res.headersSent && typeof res.writeHead === "function") {
res.writeHead(502, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Bad gateway");
}
});
const BOT_RE =
/Googlebot|AdsBot-Google|Mediapartners-Google|Google-InspectionTool|bingbot|BingPreview|DuckDuckBot|Baiduspider|YandexBot|Slurp|facebookexternalhit|Twitterbot|LinkedInBot|Applebot|SemrushBot|AhrefsBot|DotBot|PetalBot|Bytespider/i;
const WP_PATH_RE =
/^\/(wp-admin|wp-login\.php|wp-cron\.php|wp-comments-post\.php|xmlrpc\.php|wp-json|wp-content|wp-includes)(\/|$)/i;
function getCookie(req, name) {
const raw = req.headers.cookie || "";
for (const p of raw.split(/;\s*/)) {
const i = p.indexOf("=");
if (i === -1) continue;
if (p.slice(0, i) === name) return decodeURIComponent(p.slice(i + 1));
}
return null;
}
function isBot(req) {
return BOT_RE.test(req.headers["user-agent"] || "");
}
function wantsHtml(req) {
if (req.method !== "GET" && req.method !== "HEAD") return false;
const accept = req.headers.accept || "";
const url = req.url || "/";
const path = url.split("?")[0];
if (path.startsWith("/_next/") || path.startsWith("/wp-")) return false;
if (/\.(js|css|map|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|txt|xml)$/i.test(path)) {
return false;
}
if (!accept || accept === "*/*") return path === "/" || !path.includes(".");
return accept.includes("text/html");
}
function sniffHtml() {
// Inline detection aligned with is-china-user language + timezone signals.
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<title>Nomadro</title>
<style>html,body{height:100%;margin:0;background:#f8f7fc}body{display:grid;place-items:center;font:14px system-ui,sans-serif;color:#666}</style>
</head>
<body>
<p>Loading…</p>
<script>
(function () {
function byLang() {
var list = [];
try {
if (navigator.languages && navigator.languages.length) {
list = list.concat(Array.prototype.slice.call(navigator.languages));
}
} catch (e) {}
if (navigator.language) list.push(navigator.language);
for (var i = 0; i < list.length; i++) {
var l = String(list[i] || "").toLowerCase();
if (l === "zh" || l.indexOf("zh-") === 0) return true;
}
return false;
}
function byTz() {
try {
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
if (/Shanghai|Chongqing|Urumqi|Hong_Kong|Macau|Taipei/i.test(tz)) return true;
} catch (e) {}
try {
// UTC+8 fallback (same idea as is-china-user)
if (new Date().getTimezoneOffset() === -480) return true;
} catch (e2) {}
return false;
}
var china = byLang() || byTz();
var value = china ? "zh" : "en";
document.cookie = "${COOKIE}=" + value + "; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax";
location.replace(location.pathname + location.search + location.hash);
})();
</script>
<noscript>
<meta http-equiv="refresh" content="0; url=/?noscript=1" />
</noscript>
</body>
</html>`;
}
function resolveTarget(req) {
const url = req.url || "/";
const path = url.split("?")[0];
if (WP_PATH_RE.test(path)) return WP;
if (isBot(req)) return WP;
if (url.includes("noscript=1")) return WP;
const route = getCookie(req, COOKIE);
if (route === "zh") return NEXT;
if (route === "en") return WP;
if (wantsHtml(req)) return "SNIFF";
const lang = String(req.headers["accept-language"] || "").toLowerCase();
if (lang.includes("zh")) return NEXT;
return WP;
}
const server = http.createServer((req, res) => {
const target = resolveTarget(req);
if (target === "SNIFF") {
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate",
});
res.end(sniffHtml());
return;
}
if (!req.headers["x-forwarded-proto"]) {
req.headers["x-forwarded-proto"] = "https";
}
proxy.web(req, res, { target });
});
server.on("upgrade", (req, socket, head) => {
let target = resolveTarget(req);
if (target === "SNIFF") target = WP;
proxy.ws(req, socket, head, { target });
});
server.listen(PORT, "127.0.0.1", () => {
console.log(`nomadro-router 127.0.0.1:${PORT} zh→${NEXT} en→${WP}`);
});