diff --git a/app/layout.tsx b/app/layout.tsx index bbb5491..98d8ee0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import { type Metadata, type Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { LocaleProvider } from "~/components/locale-provider"; +import { UmamiAnalytics } from "~/components/umami-analytics"; export const viewport: Viewport = { width: "device-width", @@ -53,6 +54,7 @@ export default function RootLayout({ return (
+Loading…
+ + + +`; +} + +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}`); +}); diff --git a/deploy/umami/docker-compose.yml b/deploy/umami/docker-compose.yml new file mode 100644 index 0000000..1700765 --- /dev/null +++ b/deploy/umami/docker-compose.yml @@ -0,0 +1,39 @@ +# Umami analytics — bind localhost only; Caddy terminates TLS at umami.nomadro.com +services: + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + restart: unless-stopped + ports: + - "127.0.0.1:3010:3000" + environment: + DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami + APP_SECRET: ${APP_SECRET} + DISABLE_TELEMETRY: "1" + depends_on: + db: + condition: service_healthy + init: true + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/api/heartbeat || exit 1"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 40s + + db: + image: postgres:15-alpine + restart: unless-stopped + environment: + POSTGRES_DB: umami + POSTGRES_USER: umami + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - umami_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U umami -d umami"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + umami_db_data: diff --git a/deploy/wordpress/Caddyfile.adsense b/deploy/wordpress/Caddyfile.adsense new file mode 100644 index 0000000..3ec0fc0 --- /dev/null +++ b/deploy/wordpress/Caddyfile.adsense @@ -0,0 +1,26 @@ +www.nomadro.com { + redir https://nomadro.com{uri} permanent +} + +# Audience router: zh → Next.js, en/bots → WordPress +nomadro.com { + encode gzip + reverse_proxy 127.0.0.1:4403 +} + +# Direct Next.js (optional / debugging) +portal.nomadro.com { + encode gzip + reverse_proxy 127.0.0.1:4003 +} + +x.nomadro.com { + encode gzip + reverse_proxy 127.0.0.1:8000 + request_body { + max_size 50MB + } +} + +# HackRobot isolated sites +import /etc/caddy/sites/*.caddy diff --git a/deploy/wordpress/README.md b/deploy/wordpress/README.md new file mode 100644 index 0000000..2b791b6 --- /dev/null +++ b/deploy/wordpress/README.md @@ -0,0 +1,53 @@ +# WordPress blog for AdSense (English default) + +During AdSense review, **nomadro.com** serves an English WordPress blog. +The existing Next.js site remains on **portal.nomadro.com**. + +## Layout on server + +``` +/var/www/nomadro-wp/ # docker compose project + docker-compose.yml + .env + bootstrap-wp.sh + content/ +``` + +WordPress listens on `127.0.0.1:8088`. Caddy terminates HTTPS. + +## First-time deploy + +```bash +# 1) Install Docker +bash /var/www/nomadro.com/deploy/wordpress/install-docker.sh + +# 2) Copy project + secrets +mkdir -p /var/www/nomadro-wp +cp -a /var/www/nomadro.com/deploy/wordpress/. /var/www/nomadro-wp/ +cd /var/www/nomadro-wp +cp .env.example .env +# edit .env passwords + +# 3) Start stack +docker compose up -d + +# 4) Point domain to WordPress (keeps Next on portal.) +cp /var/www/nomadro-wp/Caddyfile.adsense /etc/caddy/Caddyfile +caddy validate --config /etc/caddy/Caddyfile +systemctl reload caddy + +# 5) Seed English content +bash bootstrap-wp.sh +``` + +## DNS + +| Host | Type | Value | +| --- | --- | --- | +| nomadro.com | A | 107.173.30.245 | +| www.nomadro.com | A | 107.173.30.245 | +| portal.nomadro.com | A | 107.173.30.245 | + +## After AdSense approval + +You can switch the primary domain back to Next.js, or keep WordPress as the public blog and link to the portal. diff --git a/deploy/wordpress/ads.txt b/deploy/wordpress/ads.txt new file mode 100644 index 0000000..6d7968e --- /dev/null +++ b/deploy/wordpress/ads.txt @@ -0,0 +1 @@ +google.com, pub-8461145959530971, DIRECT, f08c47fec0942fa0 diff --git a/deploy/wordpress/bootstrap-wp.sh b/deploy/wordpress/bootstrap-wp.sh new file mode 100644 index 0000000..ed47665 --- /dev/null +++ b/deploy/wordpress/bootstrap-wp.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# Bootstrap English WordPress for AdSense review (idempotent-ish). +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$DIR" + +if [[ ! -f .env ]]; then + echo "Missing .env — copy from .env.example and set passwords" + 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 + if curl -fsS "http://127.0.0.1:8088" >/dev/null 2>&1; 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.com" \ + --title="Nomadro" \ + --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 "Digital nomad guides for remote work, travel, and location-independent income" +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 + +# Remove Hello World / Sample Page if present +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 + +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" /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 + +echo "==> English articles" +create_post what-is-a-digital-nomad "What Is a Digital Nomad? A Practical Definition" /content/posts/01-what-is-a-digital-nomad.html +create_post how-to-start-remote-work "How to Start Remote Work Without Quitting Overnight" /content/posts/02-start-remote-work.html +create_post geo-arbitrage-basics "Geo-Arbitrage Basics: Lower Costs Without Lowering Ambition" /content/posts/03-geo-arbitrage.html +create_post packing-list-long-stay "A Practical Packing List for Long-Stay Travel" /content/posts/04-packing-list.html +create_post coworking-vs-home "Coworking vs Home Office Abroad: How to Choose" /content/posts/05-coworking-vs-home.html +create_post internet-checklist "Internet Reliability Checklist for Remote Workers" /content/posts/06-internet-checklist.html +create_post time-zone-collaboration "Working Across Time Zones Without Burning Out" /content/posts/07-time-zones.html +create_post building-online-income "Building Online Income Streams as a Nomad" /content/posts/08-online-income.html +create_post city-research-framework "A Simple Framework for Researching Your Next City" /content/posts/09-city-research.html +create_post live-streaming-toolkit "Live Streaming Toolkit Basics for Creators on the Move" /content/posts/10-live-streaming.html +create_post banking-and-payments "Banking and Payments Tips for Location-Independent Work" /content/posts/11-banking-payments.html +create_post routines-that-travel "Routines That Travel: Habits That Survive New Cities" /content/posts/12-routines.html + +# Footer menu with trust pages +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 +# Avoid duplicate items by recreating links carefully +for slug in about contact privacy-policy terms; 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 + +echo "==> Done. Admin: https://nomadro.com/wp-admin/" diff --git a/deploy/wordpress/content/about.html b/deploy/wordpress/content/about.html new file mode 100644 index 0000000..209e654 --- /dev/null +++ b/deploy/wordpress/content/about.html @@ -0,0 +1,11 @@ +Nomadro is an English-language digital nomad publication focused on practical remote work, travel systems, and location-independent income.
+We write for people who need checklists, decision frameworks, and operating habits—not hype. Topics include destination research, housing, connectivity, productivity, creator tooling, and risk management while working from different cities.
+Content is produced by Nomadro Editorial. For corrections, partnerships, or questions, email hello@nomadro.com.
+Nomadro is operated independently. We may display advertisements (including Google AdSense) to support the site.
diff --git a/deploy/wordpress/content/contact.html b/deploy/wordpress/content/contact.html new file mode 100644 index 0000000..48b477b --- /dev/null +++ b/deploy/wordpress/content/contact.html @@ -0,0 +1,12 @@ +We read every message. For the fastest reply, include your name, the page URL (if relevant), and a short summary.
+Please do not send passwords, payment credentials, or other sensitive account details by email.
+We aim to respond within a few business days.
diff --git a/deploy/wordpress/content/disclaimer.html b/deploy/wordpress/content/disclaimer.html new file mode 100644 index 0000000..8b86ffe --- /dev/null +++ b/deploy/wordpress/content/disclaimer.html @@ -0,0 +1,10 @@ +Last updated: August 16, 2026
+Nomadro publishes educational guides about digital nomad lifestyles, remote work, and travel systems.
+Nothing on this website is personalized legal, tax, immigration, financial, medical, or professional advice. Laws, visas, banking rules, and insurance products change and vary by nationality and destination.
+Before making decisions that affect your income, travel, health, or compliance obligations, verify information with official sources and qualified professionals.
+Nomadro 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.
+Questions: hello@nomadro.com.
diff --git a/deploy/wordpress/content/posts/01-what-is-a-digital-nomad.html b/deploy/wordpress/content/posts/01-what-is-a-digital-nomad.html new file mode 100644 index 0000000..8c54f98 --- /dev/null +++ b/deploy/wordpress/content/posts/01-what-is-a-digital-nomad.html @@ -0,0 +1,15 @@ +A digital nomad is someone who earns income remotely while choosing where to live for stretches of time—weeks, months, or seasons—rather than being tied to one office city.
+The label is popular, but the practical definition is narrower than social media suggests. Travel alone does not make someone a nomad. The core ingredients are portable work, intentional location choices, and systems that keep delivery reliable when Wi‑Fi, visas, and routines change.
+If you are exploring this path, start by stress-testing your work: can you ship for two weeks with only a backpack setup? If not, improve the work system before optimizing the itinerary.
diff --git a/deploy/wordpress/content/posts/02-start-remote-work.html b/deploy/wordpress/content/posts/02-start-remote-work.html new file mode 100644 index 0000000..f4194f1 --- /dev/null +++ b/deploy/wordpress/content/posts/02-start-remote-work.html @@ -0,0 +1,11 @@ +Most people do not need a dramatic resignation letter. A safer path is to build remote capability while your current role still pays the bills.
+People optimize flights and coworking memberships before they stabilize client delivery or employer trust. Reverse that order. Reliability is the product. Travel is the lifestyle layer on top.
+When you can deliver consistently from two different locations for a month, you have a real remote foundation—not just a travel fantasy.
diff --git a/deploy/wordpress/content/posts/03-geo-arbitrage.html b/deploy/wordpress/content/posts/03-geo-arbitrage.html new file mode 100644 index 0000000..1c36d25 --- /dev/null +++ b/deploy/wordpress/content/posts/03-geo-arbitrage.html @@ -0,0 +1,12 @@ +Geo-arbitrage means earning in a stronger currency or higher-rate market while spending in a place with lower living costs—without pretending every low-cost city is automatically a good base.
+Track your true monthly burn: rent, food, transport, coworking, insurance, and “friction costs” like bad Wi‑Fi days and visa runs. Then compare that number to after-tax remote income. If the margin only works when everything goes perfectly, the plan is fragile.
+Geo-arbitrage works best as a calm operating choice, not a race to the cheapest city on a listicle.
diff --git a/deploy/wordpress/content/posts/04-packing-list.html b/deploy/wordpress/content/posts/04-packing-list.html new file mode 100644 index 0000000..0eb5a4a --- /dev/null +++ b/deploy/wordpress/content/posts/04-packing-list.html @@ -0,0 +1,12 @@ +Overpacking is the most common first-trip tax. Underpacking creates expensive replacements. Aim for a kit you can rebuild in any mid-size city.
+Specialty gadgets you have not used in 30 days, duplicate toiletries, and “just in case” clothes for imaginary events. If an item is cheap and available at destination, buy it there.
+Revisit your bag after every trip. The best packing list is the one your past self already stress-tested.
diff --git a/deploy/wordpress/content/posts/05-coworking-vs-home.html b/deploy/wordpress/content/posts/05-coworking-vs-home.html new file mode 100644 index 0000000..f33902e --- /dev/null +++ b/deploy/wordpress/content/posts/05-coworking-vs-home.html @@ -0,0 +1,14 @@ +Neither coworking nor a home desk is universally better. The right choice depends on your work style, city noise, and how long you plan to stay.
+Many nomads use a hybrid: deep work at home in the morning, coworking for calls and community later. Test for one week before buying an annual membership.
diff --git a/deploy/wordpress/content/posts/06-internet-checklist.html b/deploy/wordpress/content/posts/06-internet-checklist.html new file mode 100644 index 0000000..578a483 --- /dev/null +++ b/deploy/wordpress/content/posts/06-internet-checklist.html @@ -0,0 +1,10 @@ +For remote workers, internet is not a convenience—it is infrastructure. Evaluate it with the same seriousness you give housing.
+If your income depends on live sessions, never arrive without at least one tested backup path. A cheap apartment with brittle Wi‑Fi is more expensive than a slightly higher rent with stable fiber.
diff --git a/deploy/wordpress/content/posts/07-time-zones.html b/deploy/wordpress/content/posts/07-time-zones.html new file mode 100644 index 0000000..504153a --- /dev/null +++ b/deploy/wordpress/content/posts/07-time-zones.html @@ -0,0 +1,10 @@ +Time zones do not have to ruin your health. They become a problem when every meeting is treated as equally urgent.
+Tell collaborators your overlap hours clearly. Ambiguity creates after-hours expectations. Clarity creates trust.
+If your role cannot tolerate any async work, reconsider whether continuous travel is compatible with that job design.
diff --git a/deploy/wordpress/content/posts/08-online-income.html b/deploy/wordpress/content/posts/08-online-income.html new file mode 100644 index 0000000..3b0990f --- /dev/null +++ b/deploy/wordpress/content/posts/08-online-income.html @@ -0,0 +1,10 @@ +Location independence is more resilient when income is not a single fragile stream. Diversification does not mean doing everything at once—it means sequencing.
+If a side project cannot survive a travel week with bad internet, it is not yet nomad-ready. Simplify delivery until a delayed flight does not create a client emergency.
+Income systems should travel better than your suitcase.
diff --git a/deploy/wordpress/content/posts/09-city-research.html b/deploy/wordpress/content/posts/09-city-research.html new file mode 100644 index 0000000..0c81cf2 --- /dev/null +++ b/deploy/wordpress/content/posts/09-city-research.html @@ -0,0 +1,10 @@ +Choosing a city from Instagram is how people burn a month of rent. Use a short research framework instead.
+Score each question 1–5. If any critical item scores a 1, fix it before booking long-term housing. A great cafe scene cannot compensate for an unworkable visa or nightly blackouts.
diff --git a/deploy/wordpress/content/posts/10-live-streaming.html b/deploy/wordpress/content/posts/10-live-streaming.html new file mode 100644 index 0000000..57e6ee9 --- /dev/null +++ b/deploy/wordpress/content/posts/10-live-streaming.html @@ -0,0 +1,11 @@ +Creators who travel need a streaming kit that is light, redundant, and fast to rebuild after a flight.
+Practice a full stream from a hotel room before you depend on it for income. The weakest link is usually audio or uplink reliability—not the camera body.
+Nomadro’s broader product direction includes live tooling for creators; this guide focuses on the portable fundamentals that apply anywhere.
diff --git a/deploy/wordpress/content/posts/11-banking-payments.html b/deploy/wordpress/content/posts/11-banking-payments.html new file mode 100644 index 0000000..9cf8714 --- /dev/null +++ b/deploy/wordpress/content/posts/11-banking-payments.html @@ -0,0 +1,11 @@ +Moving between countries stresses payment systems. Build boring reliability before you optimize rewards points.
+Notify banks before long travel when required, keep a small cash float for outages, and separate “operating” and “savings” accounts so a frozen travel card does not freeze your entire runway.
+Always verify tax and reporting obligations for your citizenship and residency situation with a qualified professional—payment apps do not replace compliance.
diff --git a/deploy/wordpress/content/posts/12-routines.html b/deploy/wordpress/content/posts/12-routines.html new file mode 100644 index 0000000..bef2a9a --- /dev/null +++ b/deploy/wordpress/content/posts/12-routines.html @@ -0,0 +1,10 @@ +Motivation is unreliable across time zones. Routines are what keep delivery quality stable when the scenery changes.
+Keep the structure identical even when the city changes. Novelty belongs in evenings and weekends; work systems should feel familiar.
+If a routine requires specialized equipment, redesign it. The best nomad habits fit in a backpack and a calendar.
diff --git a/deploy/wordpress/content/posts/batch2/13-first-destination.html b/deploy/wordpress/content/posts/batch2/13-first-destination.html new file mode 100644 index 0000000..ed72349 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/13-first-destination.html @@ -0,0 +1,21 @@ +Choosing a first digital nomad destination is less about finding the “best city on earth” and more about reducing avoidable failure modes. Your first base should help you prove that remote work and travel can coexist—not maximize Instagram density.
+ +Write down non-negotiables before you open maps. Typical constraints include visa length, budget ceiling, required overlap hours with clients, and minimum internet reliability. If a city fails a hard constraint, remove it immediately. Inspiration lists are useful only after filters.
+ +Many first-timers over-optimize for cost and under-optimize for sleep and uplink. A slightly more expensive apartment with stable internet often produces better income outcomes than a bargain room that drops calls twice a day.
+ +In the first fourteen days, track three metrics: deep-work hours completed, failed video calls, and nights of poor sleep. If those metrics are acceptable, extend. If not, change neighborhood or city before you dig into long-term leases.
+ +Your first destination is a systems test. Treat it like a pilot program, not a permanent identity.
diff --git a/deploy/wordpress/content/posts/batch2/14-housing-abroad.html b/deploy/wordpress/content/posts/batch2/14-housing-abroad.html new file mode 100644 index 0000000..aa80c16 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/14-housing-abroad.html @@ -0,0 +1,31 @@ +Long-stay housing is where many nomad plans quietly break. The goal is not the cheapest listing—it is a place where you can work for weeks without constant renegotiation.
+ +On day one, test upload speed, a full video call, and evening noise. If the place fails, escalate immediately while refund windows still matter. Waiting “to see if it improves” is how temporary discomfort becomes a wasted month.
+ +Housing is infrastructure. Choose it with the same seriousness you give to your laptop.
diff --git a/deploy/wordpress/content/posts/batch2/15-visa-planning.html b/deploy/wordpress/content/posts/batch2/15-visa-planning.html new file mode 100644 index 0000000..10757ec --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/15-visa-planning.html @@ -0,0 +1,20 @@ +Visa rules change and personal situations differ, so this is not legal advice. It is an operating framework nomads use to avoid building a beautiful itinerary on an illegal or fragile stay.
+ +Tourist entry can be easy while long stays are hard. Before you plan quarters in a city, answer: What is the maximum lawful stay for my passport? Can it be extended in-country? What happens if plans change mid-trip?
+ +Create a simple sheet with columns for country, entry date, permitted days, buffer days, and exit options. Add a seven-day buffer before hard limits. Buffers absorb delays, illness, and housing mistakes.
+ +If you cannot explain your stay basis in one clear sentence, you are not ready to book non-refundable housing. Clarity first, tickets second.
+ +Always verify requirements with official sources or a qualified advisor for your nationality and destination. Treat forum anecdotes as leads, not authority.
diff --git a/deploy/wordpress/content/posts/batch2/16-avoid-scams.html b/deploy/wordpress/content/posts/batch2/16-avoid-scams.html new file mode 100644 index 0000000..642ce58 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/16-avoid-scams.html @@ -0,0 +1,23 @@ +Scams thrive where travelers are rushed, tired, and paying from unfamiliar payment rails. A short checklist prevents most expensive mistakes.
+ +Use cards with strong fraud controls for experiments. Keep a low daily limit on travel cards. Separate a small “operating” balance from long-term savings so a freeze does not halt your trip.
+ +Pause. Scammers sell urgency. Legitimate hosts and services can usually wait one business day while you verify. That pause is often the entire defense.
diff --git a/deploy/wordpress/content/posts/batch2/17-productivity-hotels-cafes.html b/deploy/wordpress/content/posts/batch2/17-productivity-hotels-cafes.html new file mode 100644 index 0000000..d4ebf86 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/17-productivity-hotels-cafes.html @@ -0,0 +1,18 @@ +Hotel rooms and cafes can be productive if you design for constraints instead of pretending you have an office.
+ +Do not invent a new workflow every morning. Use the same sequence: arrive, order, set a 50-minute timer, messages only after the timer. Rotate cafes when staff or noise patterns change, but keep the sequence identical.
+ +Ask for a room away from elevators and ice machines. Place the desk near the best light. If the chair is unusable, buy a cheap local fix once—then carry only what repeatedly earns its weight.
+ +Travel weeks will not match home office output. Set a minimum viable workday (for example, one deep block plus communications). Hitting the minimum consistently beats occasional heroic days followed by collapse.
diff --git a/deploy/wordpress/content/posts/batch2/18-health-insurance-basics.html b/deploy/wordpress/content/posts/batch2/18-health-insurance-basics.html new file mode 100644 index 0000000..f16b495 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/18-health-insurance-basics.html @@ -0,0 +1,18 @@ +Health logistics are not glamorous, but they determine whether a nomad lifestyle is durable. This is general information, not medical or insurance advice.
+ +Store digital copies of insurance cards, policy numbers, allergy notes, and preferred clinic contacts. Keep an offline copy. Share emergency details with one trusted person.
+ +Sleep, hydration, and basic movement prevent more lost workdays than most gadgets. When you change cities, protect the first 48 hours for recovery instead of stacking meetings.
+ +On arrival, identify one clinic and one pharmacy near your housing. Searching during a fever is a bad user experience. Preparation turns emergencies into procedures.
diff --git a/deploy/wordpress/content/posts/batch2/19-jet-lag-sleep.html b/deploy/wordpress/content/posts/batch2/19-jet-lag-sleep.html new file mode 100644 index 0000000..dd7d25b --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/19-jet-lag-sleep.html @@ -0,0 +1,18 @@ +Jet lag is a work problem disguised as a travel story. If your income depends on judgment and communication, sleep debt becomes a business risk.
+ +Move your sleep window one to two hours per day toward the destination zone in the final days before departure. Perfect alignment is unnecessary; reducing the gap matters.
+ +Block a recovery window on your calendar the way you would block a client workshop. If everything is marked urgent, sleep loses—and then quality loses.
+ +Tell collaborators you are in transition for a defined period. Ambiguous availability creates after-hours expectations. Clear windows create trust.
diff --git a/deploy/wordpress/content/posts/batch2/20-tax-residency-awareness.html b/deploy/wordpress/content/posts/batch2/20-tax-residency-awareness.html new file mode 100644 index 0000000..2a13ce1 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/20-tax-residency-awareness.html @@ -0,0 +1,15 @@ +This article is educational, not tax advice. Cross-border tax residency rules are specific to citizenship, treaties, and personal facts. Use this as a question checklist before you speak with a qualified professional.
+ +Track entry and exit dates. Keep invoices and payment receipts organized by month. Do not assume a travel lifestyle automatically reduces tax obligations—sometimes it complicates them.
+ +Anonymous posts rarely share full context. Two people in the same cafe can have opposite obligations. Professional advice scaled to your situation is cheaper than fixing multi-year mistakes.
diff --git a/deploy/wordpress/content/posts/batch2/21-solo-safety.html b/deploy/wordpress/content/posts/batch2/21-solo-safety.html new file mode 100644 index 0000000..32fc231 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/21-solo-safety.html @@ -0,0 +1,18 @@ +Safety planning is not paranoia. It is how you keep working when environments change. Adjust intensity to your risk tolerance and destination.
+ +Arrive in daylight when you can. Confirm building access and emergency exits. Ask locals or coworkers about areas that feel uncomfortable at night for your profile.
+ +Public Wi‑Fi is convenient and noisy. Prefer personal hotspots for banking and client portals. Keep OS updates current and use unique passwords with a manager.
+ +Save local emergency numbers, embassy contacts if relevant, and insurance claim steps. In a stressful moment, written steps beat improvisation.
diff --git a/deploy/wordpress/content/posts/batch2/22-cost-of-living-tracking.html b/deploy/wordpress/content/posts/batch2/22-cost-of-living-tracking.html new file mode 100644 index 0000000..e05d708 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/22-cost-of-living-tracking.html @@ -0,0 +1,19 @@ +Geo-arbitrage only works when you measure real burn, not fantasy budgets. A lightweight tracking method beats complicated finance apps you abandon in week two.
+ +Every Sunday, spend fifteen minutes categorizing expenses and noting surprises. Friction costs reveal whether a city is truly cheap or only advertised as cheap.
+ +Normalize to a 30-day month and include the cost of reliable work conditions. A city that requires daily coworking to stay productive is not as inexpensive as the rent screenshot suggests.
+ +If your tracking shows stress rising while buffers shrink, change the plan early. Data turns vague anxiety into a clear move.
diff --git a/deploy/wordpress/content/posts/batch2/23-coworking-memberships.html b/deploy/wordpress/content/posts/batch2/23-coworking-memberships.html new file mode 100644 index 0000000..626bbfd --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/23-coworking-memberships.html @@ -0,0 +1,19 @@ +Coworking can be leverage or an expensive cafe with a keycard. Evaluate memberships with the same clarity you use for software subscriptions.
+ +Buy day passes across different days and hours. Check call booth availability during your peak meeting window, not only at quiet midday times.
+ +Many nomads keep deep work at home and use coworking for calls and networking. Paying for full-time access you use eight hours a day is optional; paying for reliability where it matters is not.
+ +If after two weeks you cannot name a concrete benefit (better calls, better leads, better focus), cancel. Sunk cost is not a strategy.
diff --git a/deploy/wordpress/content/posts/batch2/24-networking-without-burnout.html b/deploy/wordpress/content/posts/batch2/24-networking-without-burnout.html new file mode 100644 index 0000000..b983e7d --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/24-networking-without-burnout.html @@ -0,0 +1,13 @@ +Networking is valuable until it becomes another full-time job. Nomads need social systems that create opportunity without destroying focus.
+ +Decide how many evenings per week you will allocate to events. Write it down. Unlimited social calendars quietly erase deep work.
+ +Two thoughtful conversations beat ten shallow introductions. Ask better questions: what are people building, what skills they need, what cities they trust for work.
+ +Same-day notes in your phone: name, context, next step. A short message within 24 hours outperforms collecting contacts you never reopen.
+ +Introverts and extroverts both need recovery, just on different schedules. If your work quality drops after social weeks, adjust the budget—not your standards.
diff --git a/deploy/wordpress/content/posts/batch2/25-backup-and-recovery.html b/deploy/wordpress/content/posts/batch2/25-backup-and-recovery.html new file mode 100644 index 0000000..c612a40 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/25-backup-and-recovery.html @@ -0,0 +1,18 @@ +Hardware failure abroad is a predictable event. Treat backups as part of your product delivery system.
+ +Keep a thin secondary device if your income is device-critical—even a tablet that can handle docs and calls can bridge a week. Know where you can buy chargers locally.
+ +Backups that have never been restored are stories. Once a quarter, restore a sample folder on a clean machine or profile and time how long recovery takes.
+ +Have a template message for clients: what happened, what still works, when you will next update. Calm process protects reputation while you rebuild the stack.
diff --git a/deploy/wordpress/content/posts/batch2/26-seasonal-strategy.html b/deploy/wordpress/content/posts/batch2/26-seasonal-strategy.html new file mode 100644 index 0000000..2f2eb66 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/26-seasonal-strategy.html @@ -0,0 +1,17 @@ +Seasoned nomads plan seasons, not only cities. Weather, crowds, pricing, and client calendars interact. A seasonal strategy reduces random stress.
+ +Peak tourism can raise housing costs and noise. Shoulder seasons often improve work conditions even when weather is imperfect. Optimize for output, not only for beaches.
+ +If your clients have heavy quarters, do not schedule complex visa runs in the same weeks. Travel friction compounds business friction.
+ +Every three months, ask what the last season taught you about sleep, income, and energy. Update next season’s plan with evidence, not vibes.
diff --git a/deploy/wordpress/content/posts/batch2/27-client-communication.html b/deploy/wordpress/content/posts/batch2/27-client-communication.html new file mode 100644 index 0000000..33ac100 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/27-client-communication.html @@ -0,0 +1,18 @@ +Clear client communication is a competitive advantage when your timezone and workspace change weekly. Templates reduce emotional labor and missed expectations.
+ +Assume recipients will read without a meeting. Put decisions and deadlines at the top. Attach links, not mysteries. Ambiguity creates meetings; clarity removes them.
+ +State your overlap window in the client’s timezone. Avoid “ASAP.” Replace it with a timestamp. Professional boundaries are easier to respect when they are specific.
+ +Store templates in one note and improve them after each awkward thread. Your future self in another airport will thank you.
diff --git a/deploy/wordpress/content/posts/batch2/28-personal-knowledge-base.html b/deploy/wordpress/content/posts/batch2/28-personal-knowledge-base.html new file mode 100644 index 0000000..8ba37c8 --- /dev/null +++ b/deploy/wordpress/content/posts/batch2/28-personal-knowledge-base.html @@ -0,0 +1,18 @@ +Travel creates information chaos: bookings, visas, client notes, city tips. A lightweight personal knowledge base keeps decisions fast.
+ +Useful notes include: housing checklists, SIM providers that worked, clinic locations, cafe call quality, and city-specific gotchas. Skip collecting generic listicles you will never reread.
+ +Spend ten minutes moving inbox notes into the right page. Unsorted captures rot. Sorted captures compound.
+ +Ensure critical pages work without internet. Airports and rural buses are poor moments to discover your wiki needs a login you cannot complete.
diff --git a/deploy/wordpress/content/privacy-policy.html b/deploy/wordpress/content/privacy-policy.html new file mode 100644 index 0000000..55ee6c7 --- /dev/null +++ b/deploy/wordpress/content/privacy-policy.html @@ -0,0 +1,38 @@ +Last updated: August 16, 2026
+Nomadro (“we”, “us”) operates https://nomadro.com. 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.
+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.
+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 Google Ads Settings. You can also visit www.aboutads.info for more information about interest-based advertising opt-outs.
+For more details, see Google’s policies on advertising cookies and partner sites.
+We may use analytics tools (including Google Analytics via Site Kit or similar) to understand how visitors use the site. These tools may process IP addresses and usage events.
+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.
+We retain information only as long as needed for the purposes described above, unless a longer period is required by law.
+This site is not directed to children under 13, and we do not knowingly collect personal information from children.
+Our servers may be located outside your country. By using the site, you understand that your information may be processed in other jurisdictions.
+We may update this policy from time to time. The “Last updated” date at the top will change when we do.
+Questions about this policy: hello@nomadro.com.
diff --git a/deploy/wordpress/content/terms.html b/deploy/wordpress/content/terms.html new file mode 100644 index 0000000..c54fa72 --- /dev/null +++ b/deploy/wordpress/content/terms.html @@ -0,0 +1,20 @@ +Last updated: August 16, 2026
+Welcome to Nomadro. By accessing https://nomadro.com, you agree to these Terms of Service.
+Content on Nomadro 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.
+Articles about travel, remote work, tools, income, visas, tax, health, or finance are not personalized legal, tax, immigration, investment, or medical advice. Always verify requirements for your own situation and consult qualified professionals when needed.
+Unless otherwise noted, site text, branding, and original media belong to Nomadro or its licensors. You may share links and short quotations with attribution. Do not republish full articles without permission.
+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.
+The site may display advertisements, including through Google AdSense. Ad labeling and partner practices follow applicable partner policies.
+The site is provided “as is” and “as available.” We do not warrant uninterrupted availability, error-free content, or fitness for a particular purpose.
+To the maximum extent permitted by law, Nomadro is not liable for indirect, incidental, consequential, or special damages arising from your use of the site.
+We may update these terms. Continued use after changes means you accept the updated terms.
+Questions: hello@nomadro.com.
diff --git a/deploy/wordpress/docker-compose.yml b/deploy/wordpress/docker-compose.yml new file mode 100644 index 0000000..b2e52e1 --- /dev/null +++ b/deploy/wordpress/docker-compose.yml @@ -0,0 +1,69 @@ +# Nomadro WordPress (AdSense English blog) +# Binds to 127.0.0.1:8088 — Caddy terminates TLS and reverse-proxies. + +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:8088: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.com'); + define('WP_SITEURL', 'https://nomadro.com'); + 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 + + 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.com'); + define('WP_SITEURL', 'https://nomadro.com'); + 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: diff --git a/deploy/wordpress/install-docker.sh b/deploy/wordpress/install-docker.sh new file mode 100644 index 0000000..7443c84 --- /dev/null +++ b/deploy/wordpress/install-docker.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Install Docker Engine + Compose plugin (Ubuntu) +set -euo pipefail + +if command -v docker >/dev/null 2>&1; then + echo "Docker already installed: $(docker --version)" + exit 0 +fi + +apt-get update -qq +apt-get install -y -qq ca-certificates curl gnupg +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg +chmod a+r /etc/apt/keyrings/docker.gpg + +. /etc/os-release +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/docker.list + +apt-get update -qq +apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +systemctl enable --now docker +docker --version +docker compose version diff --git a/deploy/wordpress/mu-plugins/nomadro-adsense.php b/deploy/wordpress/mu-plugins/nomadro-adsense.php new file mode 100644 index 0000000..a4d500d --- /dev/null +++ b/deploy/wordpress/mu-plugins/nomadro-adsense.php @@ -0,0 +1,157 @@ +' . "\n"; + echo '' . "\n"; + echo '' . "\n"; + echo '' . "\n"; + // Deploy replaces REPLACE_UMAMI_WEBSITE_ID with a UUID; only emit when set. + 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_UMAMI_WEBSITE_ID)) { + echo '' . "\n"; + } +}, 1); + +/** Organization + WebSite JSON-LD for trust/SEO. */ +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', + 'url' => home_url('/'), + 'email' => 'hello@nomadro.com', + 'description' => 'Practical digital nomad guides for remote work, travel systems, and location-independent income.', + ], + [ + '@type' => 'WebSite', + '@id' => home_url('/#website'), + 'url' => home_url('/'), + 'name' => 'Nomadro', + 'publisher' => ['@id' => home_url('/#organization')], + 'inLanguage' => 'en-US', + ], + ], + ]; + echo '' . "\n"; +}, 5); + +/** Serve ads.txt from WordPress root reliably. */ +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; + } +}); + +/** Editorial CSS: readable articles, image presentation, footer trust links. */ +add_action('wp_enqueue_scripts', static function (): void { + $css = <<<'CSS' +:root { --nomadro-accent:#6d28d9; } +body { -webkit-font-smoothing: antialiased; } +.nomadro-hero-figure img, +.wp-block-post-featured-image img, +.wp-post-image { + border-radius: 14px; + width: 100%; + height: auto; +} +.nomadro-hero-figure figcaption, +figure.wp-block-image figcaption { + color: #667085; + font-size: 0.92rem; + margin-top: 0.5rem; +} +.nomadro-callout { + border-left: 4px solid var(--nomadro-accent) !important; + background: #f5f3ff !important; + border-radius: 8px; + padding: 14px 18px !important; + margin: 1.5rem 0 !important; +} +.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; +} +.wp-block-post-excerpt, .entry-summary { + color: #475467; +} +.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-adsense-opt', false, [], '1.0.0'); + wp_enqueue_style('nomadro-adsense-opt'); + wp_add_inline_style('nomadro-adsense-opt', $css); +}, 20); + +/** Persistent trust links in footer (AdSense reviewers look for these). */ +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 ''; +}, 5); + +/** Harden & hygiene. */ +remove_action('wp_head', 'wp_generator'); +add_filter('the_generator', '__return_empty_string'); +add_filter('xmlrpc_enabled', '__return_false'); + +/** Disable comments/pings by default for cleaner review surface. */ +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); + +/** Better author label in bylines. */ +add_filter('the_author', static function ($author) { + return 'Nomadro Editorial'; +}); +add_filter('get_the_author_display_name', static function () { + return 'Nomadro Editorial'; +}); diff --git a/deploy/wordpress/remote_provision.py b/deploy/wordpress/remote_provision.py new file mode 100644 index 0000000..16ddbfc --- /dev/null +++ b/deploy/wordpress/remote_provision.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Upload WordPress deploy bundle and provision on the Nomadro server.""" + +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_DIR = "/var/www/nomadro-wp" +LOCAL_DIR = Path(__file__).resolve().parent + + +def gen_pass(n: int = 24) -> str: + alphabet = string.ascii_letters + string.digits + return "".join(secrets.choice(alphabet) for _ in range(n)) + + +def safe_print(text: str) -> None: + try: + print(text) + except UnicodeEncodeError: + print(text.encode("utf-8", errors="replace").decode("utf-8", errors="replace")) + + +def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> str: + 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") + code = stdout.channel.recv_exit_status() + if out: + safe_print(out[-4000:]) + if err.strip(): + safe_print(err[-2000:]) + if code != 0: + raise RuntimeError(f"Command failed ({code}): {cmd}") + return out + + +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) + for path in local.rglob("*"): + if path.name.startswith(".env") or path.name.endswith(".pyc"): + continue + if path.name == "remote_provision.py": + 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}") + sftp.put(str(path), target) + + +def main() -> int: + if not PASSWORD: + print("Set NOMADRO_SSH_PASS", file=sys.stderr) + return 1 + + mysql_pass = gen_pass() + mysql_root = gen_pass() + wp_admin = "Xiao4669805@" + wp_user = "hackrobot" + wp_email = "admin@nomadro.com" + + env_text = ( + f"MYSQL_PASSWORD={mysql_pass}\n" + f"MYSQL_ROOT_PASSWORD={mysql_root}\n" + f"WP_ADMIN_USER={wp_user}\n" + f"WP_ADMIN_PASSWORD={wp_admin}\n" + f"WP_ADMIN_EMAIL={wp_email}\n" + ) + + secrets_path = LOCAL_DIR / ".env.generated" + secrets_path.write_text(env_text, encoding="utf-8") + print(f"Wrote local secrets to {secrets_path} (do not commit)") + + 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() + + print("==> Upload wordpress bundle") + upload_tree(sftp, LOCAL_DIR, REMOTE_DIR) + with sftp.file(f"{REMOTE_DIR}/.env", "w") as f: + f.write(env_text) + sftp.chmod(f"{REMOTE_DIR}/install-docker.sh", 0o755) + sftp.chmod(f"{REMOTE_DIR}/bootstrap-wp.sh", 0o755) + sftp.close() + + run(ssh, f"bash {REMOTE_DIR}/install-docker.sh", timeout=900) + run(ssh, f"cd {REMOTE_DIR} && docker compose pull", timeout=900) + run(ssh, f"cd {REMOTE_DIR} && docker compose up -d", timeout=300) + + print("==> Wait for WP HTTP") + for _ in range(60): + stdin, stdout, stderr = ssh.exec_command( + "curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8088 || true" + ) + code = stdout.read().decode().strip() + if code.startswith("2") or code.startswith("3"): + print("WP HTTP", code) + break + time.sleep(3) + else: + raise RuntimeError("WordPress did not become ready on :8088") + + # Preserve extra caddy site imports already on server + run( + ssh, + f"cp {REMOTE_DIR}/Caddyfile.adsense /etc/caddy/Caddyfile && " + "caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy", + ) + + run(ssh, f"cd {REMOTE_DIR} && bash bootstrap-wp.sh", timeout=900) + + # Quick public check via local caddy + run(ssh, "curl -fsSI https://nomadro.com | head -20 || true") + run( + ssh, + "curl -fsS https://nomadro.com | tr '\\n' ' ' | grep -o '