Add nomadro.live English WordPress stack and audience router

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-08-24 04:35:48 -05:00
parent 0e997576c1
commit 7a9365480e
36 changed files with 1100 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# nomadro.live — WordPress + audience split
Reference implementation cloned from `nomadro.com` WordPress/router.
**Does not modify** `/var/www/nomadro-wp` or the TikTok promo databases.
| Audience | Destination |
| --- | --- |
| Chinese users (`nomadrolive_route=zh`) | Existing Next.js `:3015` |
| English users / default | WordPress `:8089` |
| Googlebot / AdsBot / crawlers | WordPress |
| `/api/*` | Existing FastAPI `:8015` (Caddy, never through the router) |
## Layout on server
```
/var/www/nomadro-live-wp/ docker compose (127.0.0.1:8089)
/var/www/nomadro-live-router/ node router (127.0.0.1:4404)
```
## Deploy
```bash
python deploy/nomadro-live/remote_provision.py
```
Admin: `hackrobot` / `Xiao4669805@` at `https://nomadro.live/wp-admin/`

View File

@@ -0,0 +1,14 @@
# TikTok Promo Next.js + FastAPI, plus English WordPress via audience router.
# /api stays on the existing backend. HTML is split: zh → :3015, en/bots → :8089.
nomadro.live, www.nomadro.live {
encode gzip
handle /api/* {
reverse_proxy 127.0.0.1:8015
}
handle {
reverse_proxy 127.0.0.1:4404
}
}

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Provision nomadro.live English WordPress + audience router (non-destructive).
What it does:
1) Uploads a fresh WordPress compose stack to /var/www/nomadro-live-wp (on 127.0.0.1:8089).
2) Uploads a dedicated audience router to /var/www/nomadro-live-router (on 127.0.0.1:4404).
3) Updates /etc/caddy/sites/nomadro.live.caddy so:
- /api/* continues to go to the existing FastAPI backend (:8015)
- other pages:
zh -> existing Next.js frontend (:3015)
en/default -> new WordPress (:8089)
4) Boots the router via systemd and runs bootstrap-wp.sh to seed 20 English articles.
"""
from __future__ import annotations
import os
import secrets
import string
import sys
import time
from pathlib import Path
import paramiko
HOST = "107.173.30.245"
USER = "root"
PASSWORD = os.environ.get("NOMADRO_SSH_PASS", "")
REMOTE_WP_DIR = "/var/www/nomadro-live-wp"
REMOTE_ROUTER_DIR = "/var/www/nomadro-live-router"
REMOTE_SYSTEMD_SERVICE = "/etc/systemd/system/nomadro-live-router.service"
REMOTE_CADDY_SITE = "/etc/caddy/sites/nomadro.live.caddy"
LOCAL_DIR = Path(__file__).resolve().parent
WP_LOCAL_DIR = LOCAL_DIR / "wordpress"
ROUTER_LOCAL_DIR = LOCAL_DIR / "router"
SYSTEMD_LOCAL_FILE = ROUTER_LOCAL_DIR / "nomadro-live-router.service"
CADDY_LOCAL_FILE = LOCAL_DIR / "caddy" / "nomadro.live.caddy"
def gen_pass(n: int = 24) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(n))
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> None:
print(f"\n$ {cmd}")
_stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout, get_pty=True)
out = stdout.read().decode(errors="replace")
err = stderr.read().decode(errors="replace")
def safe_print(s: str, file=None) -> None:
text = s.encode("ascii", "backslashreplace").decode("ascii")
print(text, file=file) if file is not None else print(text)
if out.strip():
safe_print(out[-4000:])
if err.strip():
safe_print(err[-2000:], file=sys.stderr)
code = stdout.channel.recv_exit_status()
if code != 0:
raise RuntimeError(f"Command failed ({code}): {cmd}")
def sftp_mkdirs(sftp: paramiko.SFTPClient, remote: str) -> None:
parts = remote.strip("/").split("/")
cur = ""
for part in parts:
cur += "/" + part
try:
sftp.stat(cur)
except FileNotFoundError:
sftp.mkdir(cur)
def upload_tree(sftp: paramiko.SFTPClient, local: Path, remote: str) -> None:
sftp_mkdirs(sftp, remote)
text_ext = {".sh", ".mjs", ".js", ".json", ".yml", ".yaml", ".html", ".php", ".txt", ".md", ".caddy", ".example", ".service"}
for path in local.rglob("*"):
if path.name.startswith(".env") or path.suffix in {".pyc"}:
continue
rel = path.relative_to(local).as_posix()
target = f"{remote}/{rel}"
if path.is_dir():
sftp_mkdirs(sftp, target)
else:
sftp_mkdirs(sftp, str(Path(target).parent).replace("\\", "/"))
print(f"put {rel}")
if path.suffix.lower() in text_ext or path.name in {"Caddyfile", "Dockerfile"}:
data = path.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
sftp.putfo(__import__("io").BytesIO(data), target)
else:
sftp.put(str(path), target)
def wait_wp_http(ssh: paramiko.SSHClient, port: int, tries: int = 60) -> None:
for _ in range(tries):
_stdin, stdout, _stderr = ssh.exec_command(
f"curl -fsS -o /dev/null http://127.0.0.1:{port} || true",
timeout=20,
get_pty=True,
)
ok = stdout.channel.recv_exit_status() == 0
if ok:
print("WP HTTP ready on", f"127.0.0.1:{port}")
return
time.sleep(2)
raise RuntimeError(f"WordPress did not become ready on :{port}")
def main() -> int:
if not PASSWORD:
print("Set NOMADRO_SSH_PASS env var", file=sys.stderr)
return 1
if not LOCAL_DIR.exists():
raise RuntimeError("Local deploy/nomadro-live folder missing")
mysql_pass = gen_pass()
mysql_root = gen_pass()
# User requested defaults
wp_admin_user = "hackrobot"
wp_admin_pass = "Xiao4669805@"
wp_admin_email = "hello@nomadro.live"
env_text = "\n".join(
[
f"MYSQL_PASSWORD={mysql_pass}",
f"MYSQL_ROOT_PASSWORD={mysql_root}",
f"WP_ADMIN_USER={wp_admin_user}",
f"WP_ADMIN_PASSWORD={wp_admin_pass}",
f"WP_ADMIN_EMAIL={wp_admin_email}",
"",
]
)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, username=USER, password=PASSWORD, timeout=30, allow_agent=False, look_for_keys=False)
sftp = ssh.open_sftp()
# Do not rotate DB passwords if the stack already exists.
env_exists = False
try:
sftp.stat(f"{REMOTE_WP_DIR}/.env")
env_exists = True
except FileNotFoundError:
env_exists = False
print("==> Upload nomadro.live WordPress stack")
upload_tree(sftp, WP_LOCAL_DIR, REMOTE_WP_DIR)
if not env_exists:
with sftp.file(f"{REMOTE_WP_DIR}/.env", "w") as f:
f.write(env_text)
else:
print("Keeping existing remote .env (DB already initialized)")
sftp.chmod(f"{REMOTE_WP_DIR}/bootstrap-wp.sh", 0o755)
# 2) Upload router code
print("\n==> Upload nomadro.live router")
upload_tree(sftp, ROUTER_LOCAL_DIR, REMOTE_ROUTER_DIR)
# 3) Configure Caddy site (overwrite just this one file)
print("\n==> Update Caddy site for nomadro.live")
sftp.put(str(CADDY_LOCAL_FILE), REMOTE_CADDY_SITE)
sftp.chmod(REMOTE_CADDY_SITE, 0o644)
# 4) Configure systemd service
print("\n==> Install systemd service for router")
sftp.put(str(SYSTEMD_LOCAL_FILE), REMOTE_SYSTEMD_SERVICE)
sftp.chmod(REMOTE_SYSTEMD_SERVICE, 0o644)
sftp.close()
# 5) Start docker stack
print("\n==> Start WordPress stack (docker compose up -d)")
run(ssh, f"cd {REMOTE_WP_DIR} && docker compose up -d", timeout=600)
# Wait WP
wait_wp_http(ssh, 8089, tries=60)
# 6) Seed content + trust pages
print("\n==> Bootstrap WordPress content (20 English posts)")
run(ssh, f"cd {REMOTE_WP_DIR} && bash bootstrap-wp.sh", timeout=900)
# 7) Install router deps + start service (keep Caddy unchanged until router is ready)
print("\n==> Start nomadro-live-router")
run(ssh, f"cd {REMOTE_ROUTER_DIR} && npm install --omit=dev", timeout=600)
run(ssh, "systemctl daemon-reload", timeout=60)
# Restart is safe: service does not modify other apps/data.
run(
ssh,
"systemctl enable --now nomadro-live-router.service || systemctl restart nomadro-live-router.service",
timeout=60,
)
# 8) Validate Caddy and reload
print("\n==> Reload Caddy (activate nomadro.live split)")
run(ssh, "caddy validate --config /etc/caddy/Caddyfile", timeout=60)
run(ssh, "systemctl reload caddy", timeout=60)
ssh.close()
print("\n=== nomadro.live provisioning done ===")
print(f"WP admin user: {wp_admin_user}")
print(f"WP admin pass: {wp_admin_pass}")
print(f"WP admin URL: https://nomadro.live/wp-admin/")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

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

View File

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

View File

@@ -0,0 +1,157 @@
/**
* Audience router for nomadro.live
* - Chinese users → TikTok promo Next.js :3015
* - English users / bots / default → WordPress :8089
*
* /api/* is handled by Caddy (FastAPI :8015) and never reaches this process.
*/
import http from "node:http";
import httpProxy from "http-proxy";
const PORT = Number(process.env.PORT || 4404);
const NEXT = process.env.NEXT_UPSTREAM || "http://127.0.0.1:3015";
const WP = process.env.WP_UPSTREAM || "http://127.0.0.1:8089";
const COOKIE = "nomadrolive_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() {
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 Live</title>
<style>html,body{height:100%;margin:0;background:#0b0f19}body{display:grid;place-items:center;font:14px system-ui,sans-serif;color:#9aa4b2}</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 {
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-live-router 127.0.0.1:${PORT} zh→${NEXT} en→${WP}`);
});

View File

@@ -0,0 +1,5 @@
MYSQL_PASSWORD=change-me
MYSQL_ROOT_PASSWORD=change-me
WP_ADMIN_USER=hackrobot
WP_ADMIN_PASSWORD=Xiao4669805@
WP_ADMIN_EMAIL=hello@nomadro.live

View File

@@ -0,0 +1 @@
google.com, pub-8461145959530971, DIRECT, f08c47fec0942fa0

View File

@@ -0,0 +1,149 @@
#!/bin/bash
# Bootstrap English WordPress for nomadro.live (idempotent-ish).
set -eu
set -o pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"
if [[ ! -f .env ]]; then
echo "Missing .env"
exit 1
fi
# shellcheck disable=SC1091
source .env
WP() {
docker compose --profile tools run --rm wpcli "$@"
}
echo "==> Waiting for WordPress HTTP..."
for i in $(seq 1 60); do
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8089" || true)
if [[ "$code" =~ ^(200|301|302|403)$ ]]; then
break
fi
sleep 2
done
if ! WP core is-installed >/dev/null 2>&1; then
echo "==> Installing WordPress (English)"
WP core install \
--url="https://nomadro.live" \
--title="Nomadro Live" \
--admin_user="${WP_ADMIN_USER}" \
--admin_password="${WP_ADMIN_PASSWORD}" \
--admin_email="${WP_ADMIN_EMAIL}" \
--skip-email
fi
echo "==> Core settings"
WP language core install en_US --activate || true
WP option update blogdescription "Guides for live-stream monitoring, creator operations, and event automation"
WP option update WPLANG ""
WP option update blog_public 1
WP option update timezone_string "UTC"
WP option update date_format "F j, Y"
WP option update time_format "g:i a"
WP option update start_of_week 1
WP option update show_on_front posts
WP rewrite structure '/%postname%/' --hard
WP rewrite flush --hard
echo "==> Theme + essentials"
WP theme install twentytwentyfive --activate || WP theme activate twentytwentyfive || true
WP plugin install wordpress-seo --activate || true
WP option patch update wpseo_titles noindex-author-wpseo true || true
HELLO_ID=$(WP post list --post_type=post --name=hello-world --field=ID 2>/dev/null || true)
if [[ -n "${HELLO_ID}" ]]; then
WP post delete "${HELLO_ID}" --force || true
fi
SAMPLE_ID=$(WP post list --post_type=page --name=sample-page --field=ID 2>/dev/null || true)
if [[ -n "${SAMPLE_ID}" ]]; then
WP post delete "${SAMPLE_ID}" --force || true
fi
# Default Privacy Policy page (often a draft) collides with our slug.
WP post list --post_type=page --post_status=any --format=ids | tr ' ' '\n' | while read -r pid; do
[ -z "$pid" ] && continue
name=$(WP post get "$pid" --field=post_name 2>/dev/null || true)
status=$(WP post get "$pid" --field=post_status 2>/dev/null || true)
if [[ "$name" == "privacy-policy" && "$status" != "publish" ]]; then
WP post delete "$pid" --force || true
fi
done
create_page() {
local slug="$1"
local title="$2"
local file="$3"
local existing
existing=$(WP post list --post_type=page --name="$slug" --field=ID 2>/dev/null || true)
if [[ -n "$existing" ]]; then
echo "Page exists: $slug ($existing)"
return
fi
WP post create "$file" --post_type=page --post_title="$title" --post_name="$slug" --post_status=publish
}
create_post() {
local slug="$1"
local title="$2"
local file="$3"
local existing
existing=$(WP post list --post_type=post --name="$slug" --field=ID 2>/dev/null || true)
if [[ -n "$existing" ]]; then
echo "Post exists: $slug ($existing)"
return
fi
WP post create "$file" --post_type=post --post_title="$title" --post_name="$slug" --post_status=publish
}
echo "==> Policy / trust pages"
create_page about "About Nomadro Live" /content/about.html
create_page contact "Contact" /content/contact.html
create_page privacy-policy "Privacy Policy" /content/privacy-policy.html
create_page terms "Terms of Service" /content/terms.html
create_page disclaimer "Disclaimer" /content/disclaimer.html
echo "==> English articles"
create_post what-is-a-live-monitoring-toolkit "What Is a Live Stream Monitoring Toolkit?" /content/posts/01-live-monitoring-toolkit.html
create_post how-to-track-live-gifts "How to Track Live Gifts Without Missing Events" /content/posts/02-gift-tracking.html
create_post multi-channel-live-monitoring "Multi-Channel Live Monitoring for Small Teams" /content/posts/03-multi-channel-ops.html
create_post webhook-automation-for-lives "Webhook Automation for Live Stream Events" /content/posts/04-webhook-automation.html
create_post desktop-vs-android-live-monitoring "Desktop vs Android for Live Monitoring" /content/posts/05-desktop-vs-android.html
create_post creator-ops-dashboard "Building a Creator Ops Dashboard That Gets Used" /content/posts/06-creator-ops-dashboard.html
create_post chat-moderation-workflow "Chat Moderation Workflows for Live Streams" /content/posts/07-chat-moderation-workflow.html
create_post reading-viewer-trend-charts "How to Read Viewer Trend Charts During a Live" /content/posts/08-viewer-trend-charts.html
create_post live-session-archives "Local Session Archives for Live Operations" /content/posts/09-session-archives.html
create_post overnight-live-coverage "Staffing Overnight Live Coverage Without Burnout" /content/posts/10-overnight-coverage.html
create_post notification-design-for-lives "Notification Design for High-Value Live Events" /content/posts/11-notification-design.html
create_post rest-api-live-event-tooling "REST API Basics for Live Event Tooling" /content/posts/12-rest-api-for-live-events.html
create_post reducing-false-alerts "Reducing False Alerts in Live Monitoring" /content/posts/13-false-alerts.html
create_post live-session-runbook "Preparing a One-Page Live Session Runbook" /content/posts/14-live-runbook.html
create_post creator-tooling-checklist "A Cross-Platform Creator Tooling Checklist" /content/posts/15-creator-tooling-checklist.html
create_post engagement-beyond-viewer-count "Measuring Engagement Beyond Viewer Count" /content/posts/16-engagement-beyond-viewers.html
create_post handling-live-disconnects "Handling Stream Disconnects and Reconnects" /content/posts/17-disconnects-and-reconnects.html
create_post privacy-for-live-logs "Privacy and Data Retention for Live Logs" /content/posts/18-privacy-live-logs.html
create_post hardware-for-always-on-monitoring "Choosing Hardware for Always-On Live Monitoring" /content/posts/19-hardware-for-24-7.html
create_post onboarding-live-operators "Onboarding a New Operator to Live Monitoring" /content/posts/20-onboarding-operators.html
MENU_ID=$(WP menu list --fields=term_id,name --format=csv 2>/dev/null | awk -F, '$2=="Footer"{print $1; exit}' || true)
if [[ -z "${MENU_ID}" ]]; then
MENU_ID=$(WP menu create "Footer" --porcelain)
fi
for slug in about contact privacy-policy terms disclaimer; do
PAGE_ID=$(WP post list --post_type=page --name="$slug" --field=ID)
if [[ -n "$PAGE_ID" ]]; then
WP menu item add-post "$MENU_ID" "$PAGE_ID" || true
fi
done
WP menu location assign "$MENU_ID" footer || true
WP menu location assign "$MENU_ID" primary || true
PRIVACY_ID=$(WP post list --post_type=page --name=privacy-policy --field=ID)
if [[ -n "${PRIVACY_ID}" ]]; then
WP option update wp_page_for_privacy_policy "${PRIVACY_ID}" || true
fi
echo "==> Done. Admin: https://nomadro.live/wp-admin/"

View File

@@ -0,0 +1,11 @@
<p>Nomadro Live is an English-language publication about live-stream operations: monitoring, alerts, session archives, and automation for creators and small teams.</p>
<p>The Chinese-language product site for the TikTok Live Monitor apps remains available to Chinese-speaking visitors. This English blog is the public editorial surface for guides, definitions, and operating checklists.</p>
<h2>Editorial standards</h2>
<ul>
<li>Guides prioritize usefulness and clarity over hype.</li>
<li>We separate educational overviews from personalized professional advice.</li>
<li>Articles are reviewed for structure, originality, and reader actionability.</li>
</ul>
<h2>Who publishes Nomadro Live</h2>
<p>Content is produced by <strong>Nomadro Live Editorial</strong>. For corrections, partnerships, or questions, email <a href="mailto:hello@nomadro.live">hello@nomadro.live</a>.</p>
<p>Nomadro Live is operated independently. We may display advertisements (including Google AdSense) to support the site.</p>

View File

@@ -0,0 +1,12 @@
<p>We read every message. For the fastest reply, include your name, the page URL (if relevant), and a short summary.</p>
<h2>Email</h2>
<p><a href="mailto:hello@nomadro.live">hello@nomadro.live</a></p>
<h2>What to contact us about</h2>
<ul>
<li>Content corrections or update requests</li>
<li>Partnership or sponsorship inquiries</li>
<li>Privacy or data requests</li>
<li>Technical issues on nomadro.live</li>
</ul>
<p>Please do not send passwords, payment credentials, or other sensitive account details by email.</p>
<p>We aim to respond within a few business days.</p>

View File

@@ -0,0 +1,10 @@
<p><strong>Last updated:</strong> August 24, 2026</p>
<p>Nomadro Live publishes educational guides about live-stream monitoring, creator operations, and related tooling.</p>
<h2>General information only</h2>
<p>Nothing on this website is personalized legal, tax, employment, financial, or professional advice. Platform rules, APIs, and creator policies change and vary by region.</p>
<h2>Your responsibility</h2>
<p>Before making decisions that affect your income, accounts, or compliance obligations, verify information with official sources and qualified professionals. This site is not affiliated with TikTok or ByteDance.</p>
<h2>Affiliate and advertising disclosure</h2>
<p>Nomadro Live may earn revenue from advertising partners, including Google AdSense, and may mention tools or services that could have commercial relationships in the future. Editorial recommendations aim to remain useful regardless of monetization.</p>
<h2>Contact</h2>
<p>Questions: <a href="mailto:hello@nomadro.live">hello@nomadro.live</a>.</p>

View File

@@ -0,0 +1,16 @@
<p>A live monitoring toolkit is software that captures stream events in real time so an operator does not have to stare at a single player window. It is not a replacement for the native live studio. It is an operations layer on top of it.</p>
<h2>What it actually captures</h2>
<p>Useful toolkits record chat, gifts, follows, shares, room joins, and viewer counts as discrete events with timestamps. The point is a structured feed you can filter, alert on, and archive. If a tool only screenshots a dashboard, you cannot reconstruct what happened after the fact.</p>
<h2>Who needs one</h2>
<p>Solo creators with long sessions, agencies running several rooms at once, and night-shift operators all hit the same limit: human attention. A toolkit exists to make attention scarce and events complete.</p>
<h2>Local versus cloud</h2>
<p>Local capture keeps session files on your machine. Cloud capture is easier to share across a team but introduces account, latency, and data-residency questions. Pick based on whether your primary risk is losing a laptop or leaking a chat log.</p>
<h2>A minimum viable setup</h2>
<ul>
<li>One reliable event stream per room</li>
<li>Alerts for gifts and keyword spikes</li>
<li>A session archive you can export</li>
<li>A documented restart procedure when the live disconnects</li>
</ul>
<h2>What it is not</h2>
<p>A monitoring toolkit does not grow your audience by itself. It does not bypass platform rules. Treat it as instrumentation for people who already run lives and need fewer missed events.</p>

View File

@@ -0,0 +1,11 @@
<p>Gift events are dense, bursty, and easy to miss if you only watch an on-screen ticker. Tracking them as data lets you thank the right people, notice unusual spikes, and review a session without scrubbing a recording.</p>
<h2>Capture the fields that matter</h2>
<p>At minimum log sender handle, gift name, quantity, diamond or coin value if available, and timestamp. Without value, a “Rose x99” and a high-tier gift look the same in a naive count. Without a handle, you cannot follow up.</p>
<h2>Burst handling</h2>
<p>Combos arrive faster than a human can type. Debounce on-screen toasts so operators see a summary, but keep the raw events in the archive. Summaries are for the shift. Raw events are for the post-show recap.</p>
<h2>High-value thresholds</h2>
<p>Define two alert tiers before the show: “acknowledge in chat” and “pause other work and thank on camera.” Numbers differ by creator. The mistake is using one threshold for every room and then ignoring the alerts.</p>
<h2>After the live</h2>
<p>Export the gift table, sort by value, and mark who already received a public thank-you. Repeat gifters over several nights are a relationship list, not a one-off statistic.</p>
<h2>Integrity</h2>
<p>Do not invent gift values. If the upstream event omits a field, store it as unknown. Fake precision in recap decks trains teams to distrust the whole pipeline.</p>

View File

@@ -0,0 +1,11 @@
<p>Agencies that run several rooms at once fail first on attention allocation, not on software. Multi-channel monitoring only helps if you decide which rooms are primary and which are watch-only.</p>
<h2>Primary versus secondary rooms</h2>
<p>A primary room gets full chat response and gift acknowledgements. A secondary room gets alerts and a lighter check-in cadence. If every room is primary, none of them are.</p>
<h2>Operator load</h2>
<p>One operator can usually sustain one primary plus two secondary rooms if alerts are quiet. Four noisy primaries produce missed thanks and delayed moderation. Measure this with a two-hour trial, not a slide deck.</p>
<h2>Layout</h2>
<p>Put viewer trend and last high-value gift on a single glance row. Chat detail belongs in a selected-room panel, not eight simultaneous scrolling columns. Density that looks impressive in a demo is unusable at 2 a.m.</p>
<h2>Handoffs</h2>
<p>Write the room list, alert thresholds, and who is on camera into a shift note. Incoming operators should not reverse-engineer the setup from open windows.</p>
<h2>Failure isolation</h2>
<p>If one room’s connection dies, the other rooms must keep recording. Shared process crashes are the most expensive class of bug in this category. Prefer per-room isolation when you can.</p>

View File

@@ -0,0 +1,11 @@
<p>Webhooks turn live events into actions in other systems: a Discord ping, a spreadsheet row, a light on a desk, a CRM note. They are useful when the action is boring and time-sensitive.</p>
<h2>Start with one rule</h2>
<p>Pick a single event type—high-value gifts is the usual first choice—and send it to one destination you already check. Ten half-working rules are worse than one reliable rule.</p>
<h2>Idempotency</h2>
<p>Live feeds retry. Your receiver must tolerate duplicate event ids. If the upstream has no id, hash timestamp plus sender plus gift name and drop duplicates inside a short window.</p>
<h2>Secrets and exposure</h2>
<p>Webhook URLs are credentials. Do not paste them into public docs or client-side code. Rotate them when an operator leaves. Prefer signed payloads if the tool supports them.</p>
<h2>Backpressure</h2>
<p>A viral minute can fire hundreds of events. Queue on the receiver. Dropping events silently is better than crashing the live monitor process because a chat app rate-limited you.</p>
<h2>Test before showtime</h2>
<p>Send a synthetic event, confirm the destination, then disable the synthetic source. Debugging webhooks during a paid collab is a self-inflicted outage.</p>

View File

@@ -0,0 +1,11 @@
<p>Desktop and Android monitors solve the same event problem with different constraints. Choosing wrong usually means battery death, a blocked port, or a machine that cannot sit in a bag.</p>
<h2>When desktop wins</h2>
<p>Long sessions, multiple rooms, large charts, and webhook fan-out all prefer a plugged-in Windows machine. Keyboard shortcuts and a second monitor reduce misclicks during gifts.</p>
<h2>When Android wins</h2>
<p>A phone in a pocket can follow a single room while you are away from the desk. Foreground services and notification channels matter more than chart density. If the OS kills the process, you have no archive.</p>
<h2>Battery and radios</h2>
<p>Hotspot plus 24/7 capture plus screen-on is a thermal problem. Use a charger, disable unused radios, and test a four-hour run before you trust the device on a real show.</p>
<h2>Notification design</h2>
<p>Phone alerts should be reserved for thresholds you will actually act on. Desktop can show more toast noise because it is not in your pocket during dinner.</p>
<h2>Shared operator habits</h2>
<p>Use the same room list, the same gift thresholds, and the same archive naming on both platforms. Tool switching should not require a new mental model every night.</p>

View File

@@ -0,0 +1,16 @@
<p>A creator ops dashboard is a decision surface, not a wallpaper of charts. If you cannot say what you will do when a number moves, remove the number.</p>
<h2>Four tiles that earn their space</h2>
<ul>
<li>Current viewers versus the session median</li>
<li>Gifts in the last five minutes</li>
<li>Chat rate versus baseline</li>
<li>Connection health (last event age)</li>
</ul>
<h2>Baselines beat raw counts</h2>
<p>2,000 viewers means nothing without the usual range for that creator and timeslot. Compute a rolling median from previous sessions and show deviation, not vanity totals.</p>
<h2>Stale data is a bug</h2>
<p>If events stop, the dashboard must look broken on purpose: a red last-seen timer beats a frozen pretty chart that implies the room is still live.</p>
<h2>Role-specific views</h2>
<p>The person on camera needs gifts and comments. The producer needs connection and queue. One layout for both roles produces ignored panels.</p>
<h2>End of session</h2>
<p>Lock the dashboard into a recap mode: peak viewers, top gifters, disconnect count. That recap is the only slide most clients will read.</p>

View File

@@ -0,0 +1,11 @@
<p>Chat moderation during a live is a workflow, not a personality trait. Teams that skip the workflow either freeze on insults or over-block and kill the room.</p>
<h2>Write the rules before the show</h2>
<p>List banned topics, allowed teasing, and what happens on repeat offenses. If two moderators would disagree, the rule is not written yet.</p>
<h2>Keyword alerts versus bans</h2>
<p>Alerts are for review. Auto-hide is for unambiguous spam. Mixing them causes either alert fatigue or silent over-moderation. Keep the lists separate and short.</p>
<h2>Escalation</h2>
<p>First offense: hide or timeout. Second: longer timeout. Third: block and a one-line note in the session log. Skipping steps makes appeals impossible.</p>
<h2>On-camera coordination</h2>
<p>The host should not read every flagged line. Moderators send a three-word cue when the host must address something. Long Slack essays during a live are a failure mode.</p>
<h2>Aftercare</h2>
<p>Export flagged messages with timestamps. Review once, adjust the keyword list, and stop relitigating the same clip in group chat for three days.</p>

View File

@@ -0,0 +1,11 @@
<p>Viewer charts are useful when they change what you do in the next five minutes. They are decoration when they only confirm that a popular creator is popular.</p>
<h2>Read slope, not peaks</h2>
<p>A slow climb after a topic change is a stronger signal than a one-second spike from a raid that already ended. Train operators to talk about slope in the recap.</p>
<h2>Align events to the curve</h2>
<p>Overlay gifts, follows, and topic changes on the same timeline. A dip after a product pitch is information. A dip after a disconnect is a technical incident. Mixing the two in a client report is malpractice.</p>
<h2>Sampling</h2>
<p>If the source updates every few seconds, do not pretend you have millisecond precision. Store the native interval and label charts accordingly.</p>
<h2>Compare like with like</h2>
<p>Weekday lunch lives are not Saturday night lives. Compare against the same weekday and timeslot. Cross-slot comparisons create fake emergencies.</p>
<h2>Action examples</h2>
<p>Rising chat rate plus flat viewers: ask a question. Falling viewers plus gift burst: thank and hold the topic. Charts that never produce an action should leave the default screen.</p>

View File

@@ -0,0 +1,11 @@
<p>Session archives are how teams learn. Without them, every live is a rumor. With them, you can answer who gifted, when chat spiked, and whether the disconnect was 20 seconds or 20 minutes.</p>
<h2>What to store</h2>
<p>Event log (JSON or CSV), a short metadata file (room, start, end, operator), and optionally a recap note. Video is optional and heavy. Start with events.</p>
<h2>Naming</h2>
<p>Use UTC timestamps in filenames: <code>2026-08-24T1900Z-room-handle</code>. Human titles belong inside the metadata file. Clever folder art does not survive a second operator.</p>
<h2>Retention</h2>
<p>Decide a retention window before you accumulate a disk of chat logs. Personal data in comments is still personal data. Shorter is usually better.</p>
<h2>Export formats</h2>
<p>CSV for spreadsheets. JSON for tooling. If your only export is a screenshot, you do not have an archive.</p>
<h2>Access control</h2>
<p>Chat logs can include phone numbers, insults, and minors’ usernames. Do not dump archives into a shared drive with “everyone” access because it is convenient.</p>

View File

@@ -0,0 +1,11 @@
<p>Overnight lives fail for predictable reasons: sleepy operators, silent alerts, and nobody who can restart a frozen client. Coverage is a staffing problem first.</p>
<h2>Shifts, not heroics</h2>
<p>Four-hour blocks with a 15-minute overlap beat a single person “watching until morning.” Overlap exists so the incoming person sees a live handoff, not a dead laptop.</p>
<h2>Alert budget</h2>
<p>Night alerts should be rarer and louder. If the phone buzzes for every follow, it will be in another room by 3 a.m. Keep night rules to disconnects and high-value gifts.</p>
<h2>Physical setup</h2>
<p>Charger, headset, written restart steps, and a backup hotspot. The overnight kit is boring on purpose. Novelty gadgets are for daytime experiments.</p>
<h2>Health</h2>
<p>Do not schedule the same person for consecutive overnights as a default. Error rates climb. So do accidental bans.</p>
<h2>Morning recap</h2>
<p>The day team needs: disconnect count, top gifts, unresolved moderation, and whether the host is still live. They do not need a novel.</p>

View File

@@ -0,0 +1,11 @@
<p>Notifications are a scarce resource. Each unnecessary ping trains operators to ignore the next one, including the gift you actually needed.</p>
<h2>Severity ladder</h2>
<p>Info (log only), attention (desktop toast), interrupt (sound or phone). Most events belong in info. If everything is interrupt, you have no ladder.</p>
<h2>Wording</h2>
<p>Lead with the action: “Thank @user — Galaxy x1.” Do not lead with the product name of your monitoring app. The operator already knows which app buzzed.</p>
<h2>Rate limits</h2>
<p>Cap toasts per minute per room. Collapse repeats into “12 roses in 20s.” Humans cannot process a firehose and will blame the tool instead of the settings.</p>
<h2>Quiet hours</h2>
<p>Personal phones need a night profile even if the desktop stays loud. Split channels so family devices are not on the same stream as ops.</p>
<h2>Review weekly</h2>
<p>If an alert type was never acted on in seven days, demote it. Notification design is maintenance, not a one-time toggle screen.</p>

View File

@@ -0,0 +1,11 @@
<p>A local HTTP API plus an event stream lets other programs subscribe to a live without scraping a window. That is the difference between a toy overlay and a maintainable ops stack.</p>
<h2>Read models</h2>
<p>Expose current room state (viewers, uptime, last event time) as a cheap GET. Expose history as a filtered query, not an unbounded dump.</p>
<h2>Event streams</h2>
<p>Server-sent events are enough for most overlays. WebSockets are warranted when you need bidirectional control. Start with SSE unless you have a reason.</p>
<h2>Auth</h2>
<p>Bind to localhost by default. If you expose beyond the machine, require a token and TLS at the reverse proxy. “It is only on the LAN” is not an access policy.</p>
<h2>Versioning</h2>
<p>Prefix routes with <code>/v1/</code>. Live ops tools get forked by tired people at 1 a.m. Breaking JSON fields without a version is how overlays die mid-show.</p>
<h2>Documentation</h2>
<p>One page with example curl calls is more valuable than a generated OpenAPI that nobody opens. Keep the examples accurate when fields change.</p>

View File

@@ -0,0 +1,11 @@
<p>False alerts are not a minor annoyance. They are how on-call culture dies. A live monitor that cries wolf will be muted, and then a real disconnect goes unnoticed.</p>
<h2>Common sources</h2>
<p>Reconnect flaps, duplicated gift combos, timezone-shifted “new day” counters, and keyword lists that match ordinary slang. Log why an alert fired, not only that it fired.</p>
<h2>Hysteresis</h2>
<p>Require a condition to persist for N seconds before paging. Viewer drops of one second are noise. Disconnects that last 15 seconds are incidents.</p>
<h2>Dedup windows</h2>
<p>The same gifter sending the same gift three times in two seconds is usually a combo, not three incidents. Collapse it.</p>
<h2>Operator feedback</h2>
<p>Add a “not useful” control on toasts. Review the pile weekly. Settings that cannot be tuned will be abandoned.</p>
<h2>Never hide failures</h2>
<p>Suppressing flaps is good. Suppressing “no events for 90 seconds while the room should be live” is not. Distinguish noisy metrics from dead pipelines.</p>

View File

@@ -0,0 +1,16 @@
<p>A runbook is a short document you can follow when the host is talking and the producer is already busy. If it only lives in someone’s head, you do not have a runbook.</p>
<h2>Pre-show (15 minutes)</h2>
<ul>
<li>Confirm room id and backup account</li>
<li>Start the monitor and wait for first events</li>
<li>Test one webhook and one alert sound</li>
<li>Note the operator name and shift end time</li>
</ul>
<h2>During</h2>
<p>Who thanks gifts, who mods chat, who restarts the client. Write names, not roles. Roles drift. Names do not.</p>
<h2>Disconnect</h2>
<p>Numbered steps: check host network, restart capture, verify events resume, post a status line to the team channel. No essays.</p>
<h2>End of show</h2>
<p>Stop capture, export archive, paste recap bullets, confirm the next shift or that the room is actually off.</p>
<h2>Keep it one page</h2>
<p>If the runbook needs a table of contents, it will not be read. Link out for deep docs. The page next to the keyboard stays thin.</p>

View File

@@ -0,0 +1,11 @@
<p>Creator tooling sprawls until nobody can say what is load-bearing. A checklist keeps the stack small enough to restart under pressure.</p>
<h2>Must-have</h2>
<p>Stable capture of events, a way to notify a human, and an export. Everything else is optional until those three work on a bad Wi-Fi day.</p>
<h2>Nice-to-have</h2>
<p>Charts, multi-room, webhooks, mobile companion, widgets. Add one at a time and keep it for two weeks before adding another.</p>
<h2>Account hygiene</h2>
<p>Shared logins in a chat history are an incident waiting to happen. Use a password manager and revoke access when people leave the roster.</p>
<h2>Platform policy</h2>
<p>Automation that spam-clicks, mass-messages, or impersonates the official app is not “ops.” It is a ban path. Stay on documented or local-listen approaches your counsel can live with.</p>
<h2>Review cadence</h2>
<p>Once a month, delete a tool nobody opened. Tool counts only go up unless you schedule subtraction.</p>

View File

@@ -0,0 +1,11 @@
<p>Viewer count is the loudest metric and often the least actionable. Engagement is whether people talk, gift, follow, and stay after a topic change.</p>
<h2>Chat rate</h2>
<p>Messages per minute versus your baseline tells you if the room is awake. A high viewer count with dead chat is a lurking crowd, not a conversation.</p>
<h2>Gift participation</h2>
<p>Count unique gifters, not only diamond totals. One whale and a silent room is a different show from fifty small gifts.</p>
<h2>Follow conversion</h2>
<p>Follows clustered after a specific ask are evidence. Follows smeared across the whole session are harder to learn from. Align the ask with a timestamp in the archive.</p>
<h2>Stay-through</h2>
<p>If you can sample unique viewers over time, look at how many remain 10 minutes after a spike. Raids that bounce teach you not to change the content to chase them.</p>
<h2>Pick two</h2>
<p>Report two engagement metrics besides viewers. More than that and clients hear noise. Operators cannot optimize seven numbers at once.</p>

View File

@@ -0,0 +1,11 @@
<p>Disconnects are normal. Unhandled disconnects are how archives get holes and how hosts talk to an empty capture for twenty minutes.</p>
<h2>Detect absence, not just errors</h2>
<p>A client can sit in a “connected” UI state while events have stopped. Alert on last-event age, not only on thrown exceptions.</p>
<h2>Reconnect policy</h2>
<p>Exponential backoff with a cap. Hammering a session endpoint in a tight loop can make a platform issue worse and can look abusive.</p>
<h2>User-visible status</h2>
<p>Show “no events for 22s” in the operator UI. Hidden retries feel like a freeze. Visible retries feel like a system doing its job.</p>
<h2>Host communication</h2>
<p>If capture is down, the host may still be live on the official app. Do not tell them to end the stream unless you have confirmed the official player is also dark.</p>
<h2>Post-incident</h2>
<p>Log start, end, suspected cause (Wi-Fi, app crash, laptop sleep). Patterns across weeks beat a single angry screenshot.</p>

View File

@@ -0,0 +1,11 @@
<p>Live logs contain other people’s messages. Treating them like a private diary for the team is how you get a leak and a policy problem in the same week.</p>
<h2>Minimize</h2>
<p>Store what you need for ops and recap. You rarely need full message history for ninety days. You often need gift tables and a short moderation excerpt.</p>
<h2>Access</h2>
<p>Operators on the current roster only. Shared ZIP files on WeChat are not an access-control system.</p>
<h2>Redaction</h2>
<p>When sharing clips or CSVs with sponsors, strip phone numbers, emails, and obvious personal data. Default to handles, not real names.</p>
<h2>Children</h2>
<p>If a room attracts minors, tighten retention and sharing. “It was in a public chat” is not a complete answer to a parent or a platform.</p>
<h2>Deletion</h2>
<p>Have a way to delete a session file. Backups that ignore deletion requests are still copies of the data.</p>

View File

@@ -0,0 +1,11 @@
<p>24/7 monitoring is a hardware reliability problem disguised as a software install. The weak points are power, heat, and sleep policies.</p>
<h2>Power</h2>
<p>Use a UPS if the building blinks. Disable Windows sleep on lid-close if the laptop must keep running closed—or use a small desktop so lids are irrelevant.</p>
<h2>Network</h2>
<p>Prefer ethernet. Keep a documented hotspot fallback. USB ethernet adapters are cheap compared with a missed collab.</p>
<h2>Heat</h2>
<p>A laptop on a bed running capture plus a browser plus a VPN will thermal-throttle and then throttle your attention. Elevate it. Dust the vents.</p>
<h2>Displays</h2>
<p>A second screen for chat versus charts reduces misclicks. It also increases power draw. Measure before you add a third.</p>
<h2>Spares</h2>
<p>One spare machine with the client already installed is worth more than a perfect primary with no backup. Image the spare quarterly.</p>

View File

@@ -0,0 +1,11 @@
<p>New operators fail when training is a screen-share of vibes. Onboarding should be a sequence they can repeat without the senior person on the call.</p>
<h2>Day one</h2>
<p>Accounts, runbook location, where archives go, and a 20-minute shadow of a real room with no typing privileges. Watching is cheaper than a mistaken ban.</p>
<h2>Day two</h2>
<p>They handle alerts on a secondary room while a senior owns the primary. Debrief for fifteen minutes. Write down the two mistakes that actually happened.</p>
<h2>Permissions</h2>
<p>Grant the least access that still lets them work. Admin on day one is how credentials leak into screenshots.</p>
<h2>Language and tone</h2>
<p>If the host’s room has a specific voice, give three example replies—not a brand manifesto. Operators copy examples. They do not memorize essays.</p>
<h2>Offboarding</h2>
<p>Revoke tokens the same day someone leaves the roster. Rotate webhook URLs if they had them. Onboarding without offboarding is half a security program.</p>

View File

@@ -0,0 +1,35 @@
<p><strong>Last updated:</strong> August 24, 2026</p>
<p>Nomadro Live (“we”, “us”) operates https://nomadro.live. This Privacy Policy explains what information we collect, how we use it, and your choices. It is written to support transparency for readers and advertising partners such as Google AdSense.</p>
<h2>Information we collect</h2>
<ul>
<li><strong>Information you provide</strong> — for example when you email hello@nomadro.live (name, email address, message content).</li>
<li><strong>Technical data</strong> — IP address, browser type, device type, referring pages, approximate location derived from IP, and similar log data.</li>
<li><strong>Cookies and similar technologies</strong> — used for site functionality, preferences, analytics, and advertising.</li>
</ul>
<h2>How we use information</h2>
<ul>
<li>To operate, secure, and improve the website</li>
<li>To respond to inquiries</li>
<li>To understand aggregate traffic and content performance</li>
<li>To show ads through partners such as Google AdSense when enabled</li>
</ul>
<h2>Advertising and Google AdSense</h2>
<p>We use Google AdSense and related Google advertising services. Third-party vendors, including Google, use cookies to serve ads based on a user’s prior visits to this or other websites.</p>
<p>Google’s use of advertising cookies enables it and its partners to serve ads based on your visit to this site and/or other sites on the Internet. You may opt out of personalized advertising by visiting <a href="https://www.google.com/settings/ads" rel="noopener noreferrer">Google Ads Settings</a>. You can also visit <a href="https://www.aboutads.info" rel="noopener noreferrer">www.aboutads.info</a> for more information about interest-based advertising opt-outs.</p>
<h2>Analytics</h2>
<p>We may use analytics tools to understand how visitors use the site. These tools may process IP addresses and usage events.</p>
<h2>Data sharing</h2>
<p>We do not sell personal information. We may share data with service providers that help us host, secure, analyze, or monetize the site, and when required by law.</p>
<h2>Data retention</h2>
<p>We retain information only as long as needed for the purposes described above, unless a longer period is required by law.</p>
<h2>Your choices</h2>
<ul>
<li>You can disable cookies in your browser settings.</li>
<li>You can request access, correction, or deletion of personal data you sent us by emailing <a href="mailto:hello@nomadro.live">hello@nomadro.live</a>.</li>
</ul>
<h2>Children</h2>
<p>This site is not directed at children under 13. We do not knowingly collect personal information from children.</p>
<h2>Changes</h2>
<p>We may update this policy. The “Last updated” date at the top will change when we do.</p>
<h2>Contact</h2>
<p>Privacy questions: <a href="mailto:hello@nomadro.live">hello@nomadro.live</a>.</p>

View File

@@ -0,0 +1,20 @@
<p><strong>Last updated:</strong> August 24, 2026</p>
<p>Welcome to Nomadro Live. By accessing https://nomadro.live, you agree to these Terms of Service.</p>
<h2>Use of the site</h2>
<p>Content on Nomadro Live is for general informational and educational purposes. You agree not to misuse the site, attempt unauthorized access, scrape content at abusive scale, or interfere with normal operations.</p>
<h2>No professional advice</h2>
<p>Articles about live streaming, creator operations, tooling, data, or automation are not personalized legal, tax, employment, or professional advice. Always verify requirements for your own situation and consult qualified professionals when needed.</p>
<h2>Intellectual property</h2>
<p>Unless otherwise noted, site text, branding, and original media belong to Nomadro Live or its licensors. You may share links and short quotations with attribution. Do not republish full articles without permission.</p>
<h2>Third-party links and services</h2>
<p>We may link to third-party websites, tools, or partners. We are not responsible for their content, policies, or practices. Purchases or accounts you create with third parties are between you and those providers.</p>
<h2>Advertising</h2>
<p>The site may display advertisements, including through Google AdSense. Ad labeling and partner practices follow applicable partner policies.</p>
<h2>Disclaimer of warranties</h2>
<p>The site is provided “as is” and “as available.” We do not warrant uninterrupted availability, error-free content, or fitness for a particular purpose.</p>
<h2>Limitation of liability</h2>
<p>To the maximum extent permitted by law, Nomadro Live is not liable for indirect, incidental, consequential, or special damages arising from your use of the site.</p>
<h2>Changes</h2>
<p>We may update these terms. Continued use after changes means you accept the updated terms.</p>
<h2>Contact</h2>
<p>Questions: <a href="mailto:hello@nomadro.live">hello@nomadro.live</a>.</p>

View File

@@ -0,0 +1,72 @@
# nomadro.live English WordPress (AdSense)
# Binds to 127.0.0.1:8089 — do not collide with nomadro.com WP on 8088.
name: nomadro-live-wp
services:
db:
image: mariadb:11.4
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
wordpress:
image: wordpress:6.8-php8.2-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8089:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME', 'https://nomadro.live');
define('WP_SITEURL', 'https://nomadro.live');
if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$$_SERVER['HTTPS'] = 'on';
}
define('FORCE_SSL_ADMIN', true);
volumes:
- wp_data:/var/www/html
- ./mu-plugins:/var/www/html/wp-content/mu-plugins:ro
wpcli:
image: wordpress:cli-php8.2
depends_on:
- wordpress
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME', 'https://nomadro.live');
define('WP_SITEURL', 'https://nomadro.live');
if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$$_SERVER['HTTPS'] = 'on';
}
volumes:
- wp_data:/var/www/html
- ./content:/content:ro
user: "33:33"
entrypoint: ["wp"]
command: ["--info"]
profiles: ["tools"]
volumes:
db_data:
wp_data:

View File

@@ -0,0 +1,112 @@
<?php
/**
* Plugin Name: Nomadro Live AdSense Optimization
* Description: AdSense head code, trust schema, ads.txt, and editorial CSS for nomadro.live.
* Author: Nomadro Live
*/
if (!defined('ABSPATH')) {
exit;
}
const NOMADRO_LIVE_ADSENSE_CLIENT = 'ca-pub-8461145959530971';
const NOMADRO_LIVE_UMAMI_WEBSITE_ID = '8f5b1e28-641d-467d-9cf3-ed8cf69bb00d';
add_action('wp_head', static function (): void {
$client = NOMADRO_LIVE_ADSENSE_CLIENT;
echo '<meta name="google-adsense-account" content="' . esc_attr($client) . '">' . "\n";
echo '<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client='
. esc_attr($client) . '" crossorigin="anonymous"></script>' . "\n";
echo '<meta name="author" content="Nomadro Live Editorial">' . "\n";
echo '<meta name="theme-color" content="#0b0f19">' . "\n";
if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', NOMADRO_LIVE_UMAMI_WEBSITE_ID)) {
echo '<script defer src="https://umami.nomadro.com/script.js" data-website-id="'
. esc_attr(NOMADRO_LIVE_UMAMI_WEBSITE_ID) . '"></script>' . "\n";
}
}, 1);
add_action('wp_head', static function (): void {
if (is_admin()) {
return;
}
$data = [
'@context' => 'https://schema.org',
'@graph' => [
[
'@type' => 'Organization',
'@id' => home_url('/#organization'),
'name' => 'Nomadro Live',
'url' => home_url('/'),
'email' => 'hello@nomadro.live',
'description' => 'Practical guides for live-stream monitoring, creator operations, and event automation.',
],
[
'@type' => 'WebSite',
'@id' => home_url('/#website'),
'url' => home_url('/'),
'name' => 'Nomadro Live',
'publisher' => ['@id' => home_url('/#organization')],
'inLanguage' => 'en-US',
],
],
];
echo '<script type="application/ld+json">' . wp_json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . '</script>' . "\n";
}, 5);
add_action('init', static function (): void {
$uri = isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'], '?') : '';
if ($uri === '/ads.txt') {
status_header(200);
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex');
echo "google.com, pub-8461145959530971, DIRECT, f08c47fec0942fa0\n";
exit;
}
});
add_action('wp_enqueue_scripts', static function (): void {
$css = <<<'CSS'
:root { --nomadro-accent:#7c3aed; }
body { -webkit-font-smoothing: antialiased; }
.entry-content, .wp-block-post-content { font-size: 1.0625rem; line-height: 1.75; }
.entry-content h2, .wp-block-post-content h2 { margin-top: 2rem; line-height: 1.3; }
.nomadro-trust-footer {
display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem;
justify-content: center; padding: 1.5rem 1rem 2rem; font-size: 0.95rem;
}
.nomadro-trust-footer a { color: #344054; text-decoration: underline; }
CSS;
wp_register_style('nomadro-live-adsense-opt', false, [], '1.0.0');
wp_enqueue_style('nomadro-live-adsense-opt');
wp_add_inline_style('nomadro-live-adsense-opt', $css);
}, 20);
add_action('wp_footer', static function (): void {
$links = [
home_url('/about/') => 'About',
home_url('/contact/') => 'Contact',
home_url('/privacy-policy/') => 'Privacy Policy',
home_url('/terms/') => 'Terms of Service',
home_url('/disclaimer/') => 'Disclaimer',
];
echo '<nav class="nomadro-trust-footer" aria-label="Trust and policy links">';
foreach ($links as $url => $label) {
echo '<a href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
}
echo '</nav>';
}, 5);
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', '__return_empty_string');
add_filter('xmlrpc_enabled', '__return_false');
add_filter('comments_open', '__return_false', 20, 2);
add_filter('pings_open', '__return_false', 20, 2);
add_filter('comments_array', static function () {
return [];
}, 10, 2);
add_filter('the_author', static function ($author) {
return 'Nomadro Live Editorial';
});
add_filter('get_the_author_display_name', static function () {
return 'Nomadro Live Editorial';
});