The operating manual for traveller.org — kept current with every publish that changes a process. Sits with the CMS portal behind the admin door.
A runbook, in the order you do it. Each step says where you are, what to do, and how you know it worked. Steps marked OWNER need you at a keyboard on someone else's website; CLAUDE steps are done in a session — ask, and they happen. A Done chip means it is finished, with the date on the step.
The order is a dependency chain, not a suggestion. Every step can be started the moment the one above it is finished, and none can be started before — so working top to bottom never leaves you stuck. If a step ever tells you it must wait for a later one, that is a bug in this page: say so and it gets moved.
images.traveller.org needs no record from you anywhere. R2 creates it
itself when you add the custom domain in step 14, and it can only do that once the
zone is on Cloudflare — which is why that step sits immediately after the nameserver
change and not before it. Adding a CNAME for it at Porkbun would not work and is not a
shortcut: R2 serves a bucket under a hostname only for domains attached through its own
Custom Domains panel, and nothing else would issue the certificate. So there is no
subdomain DNS to do — not before step 13, and not after.Nothing here touches the live site. Do it whenever; it can all sit finished for weeks.
brarob100/traveller.org → Begin setup.traveller-org — keep exactly this, it is the name
step 2's redirect table was built against; Production branch production — the one field that must not be left at its default;
Framework preset None; Build command npm run build;
Build output directory dist. Preset None leaves the last two
blank — type them, rather than picking the Astro preset and trusting what it fills
in.npx wrangler deploy.
wrangler.toml, which belongs to the upload Worker, and
deploy that instead of the site — a second deploy path fighting the one GitHub
already runs. Worse, the Workers form pins no branch, so it would build the working
branch: every commit straight to the live site, which is the one thing this project exists
to prevent. It also never asks for the output directory or the five variables below, both
of which Pages does.PUBLIC_R2_BASE and check it ends .r2.dev):
SITE_URL = https://www.traveller.orgSITE_BASE = a single forward slashPUBLIC_R2_BASE =
https://pub-d86b555b7dd7462abd871c743192bcd6.r2.dev — the bucket's own
address, for now. It becomes https://images.traveller.org at
step 14, which cannot happen until the nameservers move at step 13. Setting the
custom name before it resolves builds a site on which every photograph is broken.PUBLIC_LEGACY_IMAGES = offPUBLIC_UPLOAD_URL = https://traveller-upload.brarob.workers.devtraveller-org.pages.dev shows the
site, and a session confirms the build is correctly shaped — green is not the
finish line, because it is green either way. Write that address down — step 2 needs
it.traveller-org.pages.dev, production branch
production, all five variables verified from a session against the deployed
site — see the checklist below for what that means and how to ask for it again after
a redeploy.production
branch, which moves only when you press Publish — so this first build shows
whatever was last published, not the newest staging. A missing recent change here is the
gate working, not a broken build: open
the Publish page, check the waiting list, and publish
once to bring the two level.traveller-org.pages.dev was added to the sandbox's allowlist and
a session verified all five from this side on 2026-08-18, so the check is one
sentence to Claude ("verify the deployed Pages site") rather than five things to find in a
browser. What it looks at, and which variable each proves:
pub-….r2.dev and the objects fetch 200 image/jpeg —
PUBLIC_R2_BASE reached the build, and its truncated ending survived the
paste/robots.txt does not say Disallow: / and names a
sitemap — SITE_URL reached itrel="canonical" on the home page says www.traveller.org with no
sub-path — SITE_URL and SITE_BASE both landed, so the build
is production-shaped/sitemap.xml exists, every URL in it names www.traveller.org, and
none is under /admin/PUBLIC_LEGACY_IMAGES=off reached itPUBLIC_UPLOAD_URL reached itproduction and not the working branch: every commit
reaches staging (the test site) by itself, but the live site must only change when you
press Publish on the Publish page —
that button is what moves the production branch, and this Pages project
rebuilds from it. Connect the working branch instead and every half-reviewed commit goes
straight to traveller.org.robots.txt, the sitemap and the llms.txt files all say
www.traveller.org instead of the test address; PUBLIC_R2_BASE points every
photograph at the image host; PUBLIC_LEGACY_IMAGES=off withholds the
0 albums whose photographs are not migrated yet
(), so the site never shows a
broken frame.NEW_HOST=https://traveller-org.pages.dev python3 migration/build_redirects.pymigration/htaccess-redirects.txt was generated against a guessed
address, traveller-org.pages.dev. If Cloudflare gives the project any other
name, every one of its 327 redirects points at a host that does not exist — and it
is pasted into the live server at step 6.migration/htaccess-redirects.txt names your Pages address,
and both checks pass:
python3 migration/verify_redirects.py and
python3 migration/test_htaccess.py.
traveller-org.pages.dev — the very address the committed table had been
generated against from a guess — so regenerating produced a byte-identical file
and git diff came back empty. That empty diff is the whole result: it proves
the 393 rules about to be pasted into the live server point at a host that exists. Had
Cloudflare named the project anything else, the diff would have been the work.SITE_URL=https://www.traveller.org SITE_BASE=/ PUBLIC_LEGACY_IMAGES=off
PUBLIC_R2_BASE=https://images.traveller.org npx astro build
then python3 migration/verify_cutover.py.SITE_URL and SITE_BASE
off and the build still calls itself production — but its canonical tags name the
staging address, so the redirect check reports STAGING and refuses to make the strong
claim this step exists to obtain, and the audit's own headline passes for the wrong reason
(see the note below).python3 migration/verify_redirects.py.dist/, and at step 2 that is
normally a staging build. Production sets PUBLIC_LEGACY_IMAGES=off and
withholds the legacy albums, so a redirect aimed at one would pass at step 2 and 404 on the
live site. The check now prints which shape it read and says so when the weaker claim is
all it can make.PUBLIC_LEGACY_IMAGES and PUBLIC_R2_BASE set, the audit printed
self-references the new site serves: 0 and then nothing on the public site
addresses www.traveller.org — safe to cut over. Both lines were true and together
they meant nothing: with SITE_URL unset there were no www.traveller.org
references in the build at all, so the audit passed by having nothing to look at.
Adding the two missing variables gives 1130 self-references and the same pass —
which is the pass that means something. That is `tests/README.md`'s second trap ("a passing
test can be measuring nothing") landing on the runbook for the second time in one day.@astrojs/sitemap, /admin/ excluded, named by production
robots.txt), canonical tags and og:url/og:image on every page,
and schema.org JSON-LD (WebSite + Person on the home page, ImageGallery on galleries,
BlogPosting on journal entries). tests/seo.test.mjs holds it all green../tests/run.sh passes — the SEO suite asserts the
sitemap exists, leaks no admin page, and robots.txt names it.run.sh builds a staging-shaped site, so for the production
claim run the step-4 build command above and then node tests/seo.test.mjs
against it. Done 2026-08-18 that way: 21 assertions green, 563 sitemap URLs, no
/admin/ page listed — and the same three facts confirmed independently
against the deployed site at traveller-org.pages.dev, which is the stronger
check because it reads what Cloudflare actually served.
This is the only stage that edits the old server. It sends visitors to the new site before DNS moves, so no old link is dead in the meantime. The old site keeps working for anyone typing the address.
traveller-org.pages.dev carries
rel="canonical" naming www.traveller.org — so the new host
disclaims itself, and search engines keep the www URLs in the index rather than replacing
them with pages.dev ones that would all have to be replaced again at
cutover. Nothing transfers early because nothing needs to: the addresses that are indexed
today are the addresses that serve the site after step 13..htaccess.public_html → Settings
→ tick Show Hidden Files → select .htaccess →
Download.migration/test_htaccess.py, which is the next best thing: the file can be
reconstructed from the repo if the download is ever lost.traveller-org.pages.dev, not at www.traveller.org, and
that host has been live since step 1. This is the whole reason Stage 2 comes before the DNS
flip: visitors and search engines start reaching the new site now, and nothing about it
waits for step 13. Verified 2026-08-18: all three of step 8's targets answer 200 on the
Pages site. If you would rather see that for yourself before editing anything, open
https://traveller-org.pages.dev/currency/thailand-20/ in a browser —
that is exactly where the third test URL will land..htaccess → Edit. Open
migration/htaccess-redirects.txt
on GitHub, click Raw, copy everything, and paste it in at the bottom, below
everything already there. Save.#spamming site lines and ends
with the redirect block.301 → traveller-org.pages.dev on an invented URL, which
it can only do if the rules are being read.content/ layout, which the generator had missed. Every URL they cover
already falls through your uploaded catch-all to the new host, where the new machinery
catches it — verified live on /luxembourg/1998/. Re-pasting would only
shorten a two-hop redirect to one, and the whole file stops mattering at step 13, when
this server stops seeing traffic for the domain.
migration/test_htaccess.py. It
holds only two things, a user-agent block and a PHP handler, and neither one catches URLs,
so the redirects can safely run last. Pasting below them means the user-agent block keeps
first refusal on the agent it bans; everyone else falls through to the 301s.[L], so the banned agent escapes the
block entirely and gets a 301 like everyone else. Bottom of the file, every time.RewriteCond pattern as a regular
expression — so the parentheses in (Windows NT 10.0; WOW64) and
(KHTML, like Gecko) are capture groups, not literal brackets. The rule
therefore hunts for a version of the user-agent with its brackets stripped, which no
browser sends. If you want the block to work, replace its RewriteCond line
with:RewriteCond %{HTTP_USER_AGENT} "Chrome/51\.0\.2704\.103" [NC]503 from the live server — the first time this rule has ever fired. A
normal browser is unaffected, and the redirects below it are untouched.
AddHandler line alone while you are tidying. The old
WordPress site still runs on it until InMotion is cancelled, and the 301s living in this
file have to keep working until then. (Its comment says PHP53 and the directive says
php56 — the comment is what is wrong, and neither is worth touching.)traveller-org.pages.dev, which is live, so they work the moment step 6 is
saved — the old site being still on InMotion changes nothing.www.traveller.org/journal/?p=15 → Climbing the Pyramidswww.traveller.org/cuba/1996/varadero/ → the Cuba gallerywww.traveller.org/currency/thai-notes-20.html → the Thai 20 note200 on the Pages site. Then
14 more were sampled, two from each of the seven redirect categories (banknote,
banknote-without-a-record, country, gallery, split-or-retired gallery, journal post, root
page): every Location header matched the redirect map exactly, and every
target answered 200 on the deployed site rather than merely existing in a
local build.
.htaccess (it must
be the one in public_html, the same file step 5 downloaded).www.traveller.org/definitely-not-a-real-page.html. With the block live, the
catch-all rule 301s anything unmatched to the new site. If instead you get a 302 to
missing.shtml, that is the old site's own 404 handler answering, which means
no rewrite rule ran — the block is not active. Confirmed as the pre-paste signature
on 2026-08-18, when the live server was probed from a session and answered exactly
that.Last look. Still nothing irreversible. Start with the TTL, because it is the only step here with a wait attached — set it first and the other three happen while the clock runs.
traveller-org.pages.dev — one of every kind
of page the site builds:
ERR_CONNECTION_RESET — which is why the committed browser suites run
against a local server with photographs stubbed. Corrected 2026-08-18, after trying it.200 image/jpeg from the bucket, including the four whose filenames carry
spaces and brackets. So the addresses are right and the objects exist.luxembourg/1998, was moved onto the new site the same day: its 23
photographs now ship with the build under /legacy-media/, so it is published
like any other album and is not marked deprecated — nothing supersedes it, it
is simply the best copy that exists.www.traveller.org, and step 6's redirects made every such URL 301 to the new
site, where they did not exist. The moment the redirect block went live, the album's images
broke. Shipping them with the site is what makes the old server switchable-off, which is
the whole point of stage 5.verify_redirects.py and verify_cutover.py both
pass against a fresh production build.luxembourg/1998 moved onto the site:
verify_cutover.py — 574 pages serve photographs from the image host,
1,133 self-references to www.traveller.org, and 0 references to the moving
hostname the new site does not serve. Nothing on the public site addresses
www.traveller.org — safe to cut over.verify_redirects.py — checked against a PRODUCTION build,
327 redirects to 278 distinct targets, 278/278 present.verify_redirects.py says STAGING and exits 0 —
so the job greps for the word PRODUCTION and fails without it. And the audit's headline
passes trivially on a build containing no www.traveller.org references at all, which is what
happens when SITE_URL is unset — so the job also fails if the
self-reference count comes back near zero, where a real build has over a thousand. Both
failure modes were reproduced deliberately before the job was trusted.amy.ns.cloudflare.com and
bob.ns.cloudflare.com but the two words are yours alone. Copying a pair from
any documentation, including this page, sends the domain to nameservers that are not
authoritative for it and the site goes dark. Step 13a is where yours appears; use those
two and nothing else.production branch, and the
post-cutover redirect machinery (step 18) only exists on it after a publish — the
100-rule discovery of 2026-08-18 was made on the deployed site, and the fix reaches
it the same way. Then ask a session to re-verify the three legacy URLs on
traveller-org.pages.dev before 13c; they must 301 or forward, not 404.traveller.org → choose the Free plan.
Cloudflare then scans Porkbun's existing DNS and imports what it finds. This changes
nothing yet — the domain keeps resolving through Porkbun until 13c.traveller.org → Authoritative
Nameservers (some accounts label it NS) → replace both existing
entries with Cloudflare's two → Save. This is the moment the switch is
thrown.traveller-org →
Custom domains → add www.traveller.org, then add
traveller.org. Cloudflare creates the records and issues the certificates
itself.traveller-org.pages.dev answers on
172.66.44.108 and 172.66.47.148, and those are Cloudflare's shared
edge, not this site's address. The edge decides what to serve from the hostname, not the
IP — so an A record pointing at it serves your visitors nothing until Cloudflare
has been told that hostname belongs to this project, and the only place to tell it is the
Custom domains panel above, which needs the zone in the account.
traveller-org.pages.dev → 200, the sitewww.traveller.org → 403 Forbidden, from
server: cloudflaretraveller.org by flattening it,
which requires hosting the zone); no certificate — Cloudflare issues the
padlock for a custom domain once it is attached, so an A record alone fails TLS before it
ever reaches the 403; and those addresses are Cloudflare's to change, so a hand-copied A
record is a bomb on a timer.
https://www.traveller.org shows the new site with a valid padlock.
Certificates can take a few minutes.www.traveller.org and reads the response headers. Server: cloudflare
with a CF-RAY means the request is being answered by Cloudflare;
Server: Apache means it is still InMotion and the change has not landed yet.
That is exactly how the pre-cutover state was established on 2026-08-18. What a session
cannot do is read the nameserver records themselves: cloudflare-dns.com
and dns.google both answer 403 on CONNECT from the sandbox, so the
authoritative check on the NS pair is Cloudflare's own Active badge.
www and the apex back at InMotion's address
(216.194.165.246, the value they resolve to today) takes effect within the
record TTL — minutes: Cloudflare serves proxied records with a ~5-minute TTL of its
own, and the records its scan imported carry the 600 seconds set in step 9. Reverting the nameservers at Porkbun instead means waiting on
the .org registry's delegation TTL, which is not yours to set and is
commonly a day: resolvers that have cached the delegation keep asking Cloudflare regardless
of what Porkbun now says. Same intent, two very different clocks./admin/ (20) all require it.server: cloudflare with a CF-RAY, serving the new site, and the
live home page counted the same 60 countries as the data. One thing broke, and it was
not the website.traveller.org MX 0 traveller.org — aimed at the
apex, which is proxied — and the orange cloud carries HTTP and HTTPS only. A
proxied name resolves to Cloudflare's edge, which does not speak SMTP, so inbound mail had
nowhere to land. Nothing in the dashboard says so; the record looks fine.mail →
216.194.165.246 set DNS only (grey cloud), the apex MX re-pointed at
mail.traveller.org, and cpanel and ftp greyed for
the same reason. Confirmed from a session: mail.traveller.org answers
server: Apache with no CF-RAY — the grey cloud is
working — while www answers as Cloudflare.images.traveller.org is attached to the bucket and works, and the owner reports
PUBLIC_R2_BASE changed — but the live build still asks for every
photograph from pub-….r2.dev. A variable is read at build time
only, so the site keeps the old value until a build runs having seen the new one. The
same symptom sits on step 18's Turnstile keys, from the same cause — which is what
makes this one build problem rather than two variable problems. Nothing is broken meanwhile:
both addresses serve the same bucket.pub-….r2.dev address set in step 1. If mail needs
attention first, do step 15 and come back — the two do not depend on each other.traveller-images →
Settings → Custom Domains → + Add →
images.traveller.org. It is the first panel on the Settings page,
above the fold, and while it is empty it reads "There is no custom domain assigned to
this bucket."https://pub-d86b555b7dd7462abd871c743192bcd6.r2.dev, which is the value
PUBLIC_R2_BASE is set to right now, so disabling it breaks every photograph on
the site the moment it is clicked. Searching the page for "public" lands on that panel and
not on the one you want. Cloudflare's own warning under it — rate-limited, not
recommended for production, Access and Caching unavailable — is the whole reason
this step exists. It stays enabled until PUBLIC_R2_BASE has been switched over
and a redeploy has proved the new name works.https://images.traveller.org/thumbs/vietnam/2009/halong-bay/…any file
listed in the bucket — and Pages → traveller-org → Settings
→ Environment variables has PUBLIC_R2_BASE changed from the
pub-….r2.dev address to https://images.traveller.org,
with a redeploy so the build picks it up.
PUBLIC_R2_BASE after the custom domain resolves,
never in the same sitting as adding it — between the two, every photograph on the
site is being served from a name that may not have propagated, and the Pages build cannot
tell you that. Add the domain, open a photograph over it in a browser, and only then edit
the variable and redeploy.send.traveller.org — a different hostname, which
Email Routing neither reads nor cares about. Do not delete it: it is Resend's
bounce and complaint return path, and removing it breaks sending, which is the half you are
keeping.MX 0 mail.traveller.org — the one
written at step 13 to repair inbound mail while it was pointed at a proxied name. That was
right then and it is exactly what Email Routing replaces now. Delete the apex MX, then
press Activate; Cloudflare writes its own three
route1/2/3.mx.cloudflare.net. Leave the mail A record
alone — it is not an MX, and it is how you reach cPanel and webmail afterwards.
v=spf1 include:_spf.mx.cloudflare.net ~all for the apex. You already have an
apex SPF covering InMotion's sending; accept the offer as written and InMotion's mail stops
passing SPF. Merge the two into one apex TXT — every include you have plus
Cloudflare's, one ~all at the end — and edit the record by hand
afterwards if the wizard has overwritten it. The InMotion include comes back out at step
24, when that plan is cancelled.v=DMARC1 TXT (DMARC is only ever read at _dmarc.traveller.org)
and a v=DKIM1 TXT (DKIM is only ever read at
<selector>._domainkey). Neither does anything where it sits. That the
DMARC one is inert is lucky rather than fine: it asks for strict alignment, which
Resend's send. return path would fail if anything ever read it.@traveller.org addresses to Gmail. The moment
the nameservers change, mail follows DNS — so this step happens the same day, right
after step 13. Cloudflare → the traveller.org zone → Email
→ Email Routing → Get started:
brarob@gmail.com and
reynaldh@gmail.com. Cloudflare emails each one a verification link; both must
be clicked before any rule can point at them.brarob,
brobinson, dns, hiole, privacy,
support, theboys → brarob@gmail.com, and
reynald → reynaldh@gmail.com. Add dmarc →
brarob@gmail.com alongside them — it is not one of InMotion's eight, but step 17's
reports are sent to it and bounce without it.theboys@ goes to both inboxes today, and a Cloudflare rule forwards
to one destination only — so its rule is different: set the action to Send to a
Worker → traveller-email. That Email Worker is already written and
deploys itself (cloudflare/email-worker.js); it hands the same message to both
Gmail inboxes. It still needs both destinations verified first.traveller-email”; the Worker decides
where it goes, from a list in its own code:
traveller-email → the code view. The line reads
const INBOXES = […], near the top.cloudflare/email-worker.js in the repository
and push — it redeploys itself. Not in the dashboard editor: that works until
the next auto-deploy overwrites it. Asking in a session is the short way.forward() refuses an
unverified destination, so an unverified inbox means theboys@ quietly reaches
one person instead of two. Check it here before activating.theboys@ arrives in both.
brobinson@traveller.org (or any address on the domain) must not
depend on InMotion's mail server. Resend takes over as the outgoing relay — the domain
is already verified there, because the import emails send from
pipeline@traveller.org:
gmail-smtp. Do not reuse the
pipeline's key — separate keys mean rolling one never breaks the other.brobinson@traveller.org. SMTP server smtp.resend.com, port
587, username resend, password = the new API key, TLS.brobinson@traveller.org — it
arrives in brarob@gmail.com through the forwarding set up in step 15, which is why this
step comes after it.brarob@traveller.org, support@…) — the same SMTP
credential serves them all.brobinson@traveller.org arrives showing that address — not “via
gmail.com” — and lands in the inbox, not spam.
d=gmail.com and its bounce address is your gmail.com account — so
SPF and DKIM both authenticate gmail.com, while the From line says
traveller.org. DMARC's whole test is whether those two agree, and here nothing
authenticates the domain in the From line. Through Resend the message is DKIM-signed
d=traveller.org, which is the same thing said properly.pipeline@traveller.org. Free tier: 100 emails a day, shared with the import
notices — plenty.theboys@ and hiole@
worked, brobinson@ did not) — Gmail stores the SMTP server per
address, not once per account. The screen that lists them shows only the address, so
three entries can look identical and be pointed at three different servers. An address you
set up years ago against InMotion keeps InMotion's server and password until you change it,
and mail.traveller.org still resolves, so the failure is a credential error
rather than an obvious one.smtp.resend.com on port 587 with username
resend is the bug; retype it and re-send. If it already says Resend, the next
suspects are an address showing (unverified) in that list — Gmail will not send
as an address whose confirmation code was never clicked — and an API key created with
sending access scoped to one domain.| Type | Name | Content | TTL |
|---|---|---|---|
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:dmarc@traveller.org; fo=1 | Auto |
p=none is deliberate. It changes what happens to nobody's mail; it
only asks receivers to report what they saw. Read a few weeks of reports, confirm everything
legitimate passes, and only then consider p=quarantine. Starting at enforcement
is how a forgotten sender — a booking site, an old form, a mailing list —
disappears into spam with no warning.send.traveller.org, a
subdomain, and relaxed alignment accepts that while strict would fail it. The old
apex record carried adkim=s; aspf=s — do not copy them across.v=DMARC1 TXT at the same time. DMARC is only ever
read at _dmarc, so the apex copy has never done anything; leaving it there
invites a future tidy-up that copies its strict flags onto the real record.dmarc@traveller.org has to exist, or the reports bounce. Add it as a
custom address in step 15's list, forwarding to brarob@gmail.com. Keep the address on
traveller.org — pointing rua at a gmail.com address instead requires
Gmail to publish an authorisation record for this domain, which it does not, so most
senders would silently refuse to report at all._dmarc.traveller.org answers with the record (any DNS lookup
tool), the apex copy is gone, and within a few days a report has arrived at
dmarc@.
data-sitekey, the Turnstile script and the live endpoint, and the contact
Worker answers past its own secret check. It came right a day after it looked broken, and
the reason is worth keeping: the page the owner was testing was staging, whose build
carried neither key and therefore drew the display-only form — a grey
“Test build” line and a picture of a checkbox — while the live site had
been sending all along. Staging now builds with both keys too, so the two agree.https://www.traveller.org/contact/, view source, search for
data-sitekey. Empty means the build had no key, whatever the dashboard shows.
Then, in order:
PUBLIC_TURNSTILE_KEY? Case-sensitive, no trailing
space. Astro only substitutes a variable whose name starts with PUBLIC_, and an
unrecognised name is not an error — it is an empty string.PUBLIC_R2_BASE, which is the useful clue: two
unrelated variables both missing points at the build, not at either variable.traveller.org contact, hostnames traveller.org and
www.traveller.org, widget mode Managed. It gives you two keys.
brarob100.github.io to that list as well (the
widget → Settings → Hostname Management → Add). The staging site
builds with the same public key since 2026-08-20, so the form works there too — but
Turnstile renders only on hostnames the widget names, and on any other it draws nothing at
all. That empty space is indistinguishable from a broken key, and it is neither.PUBLIC_TURNSTILE_KEY, and alongside it PUBLIC_CONTACT_URL
= https://traveller-contact.brarob.workers.dev. Then redeploy — a variable
on its own changes nothing until a build reads it.
traveller-org is the entry tagged Pages, not the
traveller- Workers beside it.PUBLIC_R2_BASE from the pub-….r2.dev address to
https://images.traveller.org and one redeploy finishes both.traveller-contact → Settings → Variables → add
TURNSTILE_SECRET (encrypted), and RESEND_API_KEY — a
third Resend key, its own, so rolling it touches neither the pipeline nor Gmail.https://www.traveller.org/contact/ and it arrives in your inbox — and
hitting Reply addresses it back to the address you typed, not to the form.
www.traveller.org or
brarob100.github.io, not traveller-org.pages.dev. The
Turnstile widget is bound to the hostnames in its Hostname Management. On any other name it
refuses to render — which looks exactly like a broken key, and is not.brarob100.github.io and reasonably read it as a broken CAPTCHA on a form that
was, on the live domain, already sending./journal/?p=15 — the journal index resolves the WordPress post id
with a script, because Pages redirects cannot see a query string./cuba/1996/varadero/ — a pre-rendered forwarding page at the
old address, one of 172 the build writes for every exact old URL./currency/thai-notes-20.html — a forwarding page too; the wildcard
rules in _redirects cover only the ~4,600 photo wrapper pages, whose filenames
cannot be enumerated._redirects file
(2026-08-18): the deployed Pages host was observed honoring exactly the first 100
rules of that file and silently dropping the other 125 — rule 100 fired, rule 101
did not, bisected live against traveller-org.pages.dev, with no bad byte at
the boundary. Every plain gallery URL was in the dropped half, so they would have 404ed the
moment DNS moved, with every build green. Cloudflare's documentation permits far more; the
site is built to the limit that was observed, with a guard in
build_redirects.py that refuses to emit more than 100 rules and a suite
(tests/redirect-stubs.test.mjs, also in the Cutover checks job) holding
every exact old URL to having its forwarding page.https://www.traveller.org/robots.txt, then
https://www.traveller.org/llms.txt.Disallow: /, and every link
in llms.txt names www.traveller.org, not the old test address. The staging
site blocks indexing and writes test-host links; a production build drops both
automatically. If either survives, the build did not pick up SITE_URL.
llms.txt / llms-full.txt are what AI
answer engines read. AI crawlers are deliberately not blocked — the
Pipeline page has the whole
LLM-and-search inventory, including what is deliberately not built.www in Subdomain, choose
traveller.org as the Domain, and type admin in Path.
traveller.org/admin and not www.traveller.org/admin
— and www is where the site actually lives, so the door would look configured and guard
an address nobody uses. Add a second public hostname with the Subdomain empty if you
want the apex covered too; it costs nothing and closes the back way in.Owner only → Action
Allow → under Include choose the selector Emails → save. Back
on the application, the policy now appears in the list.
/admin/,
including Publish and the upload pages. Anyone you add
can put the site live. Keep the list to people you would give the repository to.traveller.org;
change it to something you will recognise in a list, like traveller.org admin.
Session Duration 24 hours is fine. Then Create.https://www.traveller.org/admin/docs/ asks for an email and a code before
showing this manual — and your own browser, already signed in, still opens it normally.
Both held on 2026-08-20: an anonymous request is 302'd to the one-time-PIN login, the JSON
feeds the pages read stay public, and the owner signed in with the code the same day.
traveller-upload.brarob.workers.dev — nor
www.traveller.org/api/*, where the same Worker is routed (since 2026-08-20)
so the admin pages can call it on their own domain. Putting Access in front of either breaks every interactive
admin page, which is the fault this route was added to end (2026-08-20). Its door is the
passphrase —
which is why that gate rate-limits failed attempts (about ten wrong tries a minute, then
a one-minute wait; the Pipeline page has the
details). Remember that robots.txt and noindex keep pages out
of searches, not away from visitors — they are politeness, not locks.https://www.traveller.org as a property if it is not
already → submit https://www.traveller.org/sitemap.xml.traveller.org zone
→ DNS → Records. (Records left Proxied show TTL as
Auto and need no change; the setting only applies to unproxied records.)upload.traveller.org (your call, 2026-09-14). Declared as a Worker
custom domain in wrangler.toml and created by the deploy that already
runs on every push — no dashboard, no hand-written DNS record, no credential in the
conversation. Nothing was clicked, which is the point: the dashboard cannot do this.
Adding it there puts you in Cloudflare’s Connect your domain flow —
add-a-new-zone onboarding, which will not register a subdomain of a zone it already
hosts — and it spins on “processing” and gives up silently, which is
exactly what happened on 2026-08-20.
/ and
/m both serve the uploader carrying that deploy’s own build stamp,
/galleries answers 403 with no passphrase (the Worker itself
talking), and the certificate verifies. The deploy token turned out to already carry the
Zone · DNS · Edit row this needs — the run was green on the first
push, so nothing had to be added.
www.traveller.org/api/ is still what the admin login should read, because it is
the only same-origin one; traveller-upload.brarob.workers.dev still carries the
iPhone Shortcut, so the Shortcut needs no change.docs/guides-site/; they are not on the shelf. The reason is worth
keeping: the guide went stale between being drafted and being pushed — the
Rates screen was redesigned, promo codes went from two kinds to three, and a Reviews
screen appeared, all in the hours it took to write. To bring it back: delete the
hold line from that app’s row in src/data/guides.json,
then run the Sync app guides workflow. Before you do, have that repository’s
session re-read the six pages against the finished site — the sentence most likely
to be wrong by then is the one saying the permanent web address is not switched on
yet.direction: replace, which the fetch honours by
overwriting a taken name); the phone reuses its existing duplicates sheet. Tests pin the
classification’s parity with the Python, both pages’ POSTs, and the replace
flag. About half a day of session time. Reuse points: icloudGuid and
the destination handling on /dispatch, the /photos listing,
master_name / plan_names in icloud_fetch.py, the
#dupsheet on /m. Nothing is built until you say so.production, with five Pages Functions) and the Spec Intake form (two
functions, on spec-intake.pages.dev). The four Workers — upload,
contact, email and the recipe pipeline — are already Workers and are unaffected, as
are Netlify (Expense Tracker) and Railway (the Amapas 353 ChatGPT app).
_redirects, _headers, custom
domains, environment variables and secrets all carry over, and Git builds become Workers
Builds — but the functions/ folder is not supported natively and
must be compiled or rewritten. On this site that folder is the 301 layer answering every
old address plus the friends door, which is the machinery least safe to disturb while the
Search Console reports are still draining.
www.traveller.org/guides*,
allow-list of two (you and Reynald), one-time PIN, created by you on 2026-09-14 and
verified from outside the same day: the shelf and every page under it answer with the login,
the public site is unaffected. The app addresses went onto the cards in the same change,
which the door is what makes safe. The history, kept because the ordering mattered
(deferred by you on 2026-09-05: the shelf went live open, unlinked and unindexed but readable by anyone with
the address, and every guide is written as if public). The door is a Cloudflare Access
application, and it locks the shelf with no change to the site. In the Zero Trust dashboard,
Access → Applications → Add → Self-hosted: domain www.traveller.org, path
guides (covers /guides/ and everything under it), one policy
Allow with an email allow-list of just your address, one-time PIN as the login
method — a second application, not a new path on the admin one, so its list
can later take an app’s users without opening the admin to them (owner’s
choice: just you for now, widen later). Then check that a private window asking for
www.traveller.org/guides/ is answered with the PIN login, not the shelf.
That rule — nothing private on the shelf until the door exists — held until
2026-09-14, and is why the addresses waited.) Separately, and not
waiting on the door: the sync’s read token exists — you created it on
2026-09-05, under your own name for it, CENTRAL_DOCS_GIT_TOKEN (credentials
table on the pipeline page) — so the
nightly sync can read the app repositories.FRIEND_KEY on the Pages project and deployed on 2026-08-28. Rotating the
password (which signs every friend out) is one edit to that same setting followed by a
deploy; the manual’s Friends
section says what to send people.luxembourg/1998, whose country has no high-res
batch yet./admin is not built. Editing is by session or by
editing JSON on GitHub. The Journal editor (2026-08-18) now covers the letters
— prose, inline photographs, the pull-quote — which was the largest piece of
what a CMS was for.migration/journal-missing-images.csv
in case a scan turns up.