feat: Umami/ntfy 可观测性栈与 WordPress AdSense 部署
This commit is contained in:
32
deploy/router/README.md
Normal file
32
deploy/router/README.md
Normal 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
|
||||
```
|
||||
17
deploy/router/nomadro-router.service
Normal file
17
deploy/router/nomadro-router.service
Normal 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
|
||||
8
deploy/router/package.json
Normal file
8
deploy/router/package.json
Normal 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
160
deploy/router/server.mjs
Normal 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user