Admin / Site manual Back to the site

The stack & how it works

The operating manual for traveller.org — kept current with every publish that changes a process. Sits with the CMS portal behind the admin door.

What this is

traveller.org is a static website generated from plain content files. There is no database and no server-side code: every page is pre-built into HTML whenever content changes, then served from a CDN. Publishing means "commit a change to the GitHub repository" — everything downstream is automatic.

The one rule of the architecture: content never contains design; design never contains content. Photographs, journal text, and banknote data live as plain records; the look of the site lives in layouts and one stylesheet. Either can change without touching the other.
The documentation rule: every feature or process change lands with its manual update in the same push — this site is operated from these pages, so an undocumented feature does not exist. A repository check (Docs guard) watches every push and flags feature changes that arrive without a docs change, and the standing instructions for the AI sessions that build the site (CLAUDE.md in the repository) make the rule binding on them too.
Two checks run on every push, and you do not have to ask for either. Docs guard warns when feature code changes without its documentation. Cutover checks (added 2026-08-18) rebuilds the site production-shaped and proves two things that go stale on their own: that nothing on the site still points at www.traveller.org in a way the new host cannot serve, and that every one of the 767 redirect targets still exists. An import that renames a page, or a hero that starts pointing at the old server, breaks one of those without breaking the build — so this one is red on failure, not a warning. Both live in Actions.
Every page of the old site now forwards somewhere (26 August 2026). The redirect table is generated from legacy/, the crawl of the old site, and it used to recognise only two album layouts — so anything shaped differently fell through in silence, and 440 real pages were returning 404 on the live site: paginated album indexes, per-note currency pages filed a level deeper than their siblings, the 1992 Africa letters. The crawl is the checklist now: a page in it with nowhere to go fails the generator, and a build missing one fails tests/redirect-stubs.test.mjs. Three kinds of page are exempt, on purpose — ones the old site had itself stopped serving, addresses the new site answers directly, and query strings the crawler flattened into filenames, which were never paths anyone could request.
Old URLs answer with a real 301 (26 August 2026). Forwarding a visitor and forwarding a search engine are not the same job. Cloudflare’s _redirects file gives a proper 301 — but it honours only about 100 rules and cannot read a query string, so two sets of URLs were being forwarded the weak way: the 618 exact old addresses (a page that says “moved” in the browser, which Google treats as a soft redirect worth far less), and every journal post, because the old site’s posts were query strings — /journal/?p=15 was “Climbing the Pyramids”, the most-read page on the whole site. After the cutover those served the journal index, told Google the index was the canonical page, and only JavaScript carried a reader to the post. A small program that runs in front of the site (functions/_middleware.js) now answers all of them with a true 301. The “moved” pages are deliberately kept underneath it: if that program is ever switched off, every old address still forwards, just less strongly.

Where the domain stands today

The stack table below says what each service is. This one says what is true right now, because during the cutover those are different things and the table alone reads as though everything is already in place. Every line is dated; a session updates this section in the same push as any change to the domain or a service, and tests/docs-refs.test.mjs keeps its runbook step numbers honest.

And within one album it now also catches resemblance (25 August 2026): a photograph 90% or more the same picture as one already in that album is held back rather than published — kept in storage under near-dupes/, named in the email, reversible with direction: keep-near.
WhatTodayBecomes
The live site Cloudflare Pages — the cutover happened on 2026-08-19. www.traveller.org answers server: cloudflare with a CF-RAY and serves the new site, built from the production branch and current. Since 2026-08-30 the site has ONE address: the bare traveller.org forwards to www with a 301 instead of serving a second copy — cookies and stored logins are per-host, so the apex was the whole site with none of your keys, and that is how a signed-in owner came to see no banner and no hidden albums (mail is untouched; MX is DNS, not HTTP) — verified the same day, counting 60 countries on the live home page against 60 in the data. Since 2026-09-09 the host it runs on is in maintenance mode: Cloudflare has moved all new development to Workers and Pages now only receives upkeep. Nothing is broken and nothing has a deadline — Pages still builds and serves, with no announced shutdown — so the site stays put; you chose on 2026-09-09 not to spend development time on it. What a move would involve, and why the Spec Intake form should rehearse it first, is on the runbook’s open list. Nothing further for now. It updates whenever Publish is pressed. One day, a move to Workers — when there is a reason, not because Pages stopped working.
The friends door Built 2026-08-27; the owner set FRIEND_KEY and switched it on 2026-08-28. One shared friend password (FRIEND_KEY on the Cloudflare Pages project) opens every unlisted album’s pages on www.traveller.org; friends sign in at /friends/, land on the home page under a signed-in banner, and stay signed in about a year per device; hidden albums appear in the country timelines for them, and friends-only photographs carry a Friends Only flag. The gate is the Pages Function in front of the live site, so it exists only there: the test site is plain files with no door, and the photograph files on images.traveller.org remain public addresses (the 2026-08-19 call, unchanged). With no FRIEND_KEY set the gated pages answer “switched off” — closed, not open. The same door also covers single photographs: any photo of a public album can be marked friends-only from Gallery cleanup — off the public page and every count, woven back in for signed-in friends. Since 2026-08-30 the Photographs page (/galleries/) lists every unlisted album for a signed-in friend or admin, country-named cards under the public listing — before that a country’s own page was the only page that could show one, and an unlisted album looked lost from everywhere else. The same push fixed the silent way those cards could fail to draw: a workers.dev Worker address remembered by an old build is now dropped on the public pages exactly as the admin login drops it. Since 2026-09-02 an iPad held upright shows the album cards side by side — the meta column beside the four-photograph strip, as a desktop does. The phone layout (details stacked above the strip) used to begin at 900px, which is wider than every iPad portrait, so a tablet got a full-width band of empty cream above each strip; the stack now begins at 560px, where the thumbnails beside the details column would genuinely be too small — and there, stacked, the card’s details are a compact header with the Gallery link beside them (~80px) rather than a floor of empty cream above each strip. Since 2026-09-01 a film can be unlisted, exactly as an album can: off the films pages, the home strip, every count and every feed, while a signed-in friend sees it on the films pages under a purple ⊘ Unlisted poster chip and plays it there. Set at upload (both film forms have the switch) or any time in Gallery cleanup → Films. The film’s .mp4 keeps its public storage address, the same rule as photographs — unlisted means undiscoverable, not unwatchable, and that trade was the owner’s call. Since 2026-09-01 the old shop’s photograph pages land on their country rather than on the home page: 43 addresses under /store/cart/ whose filename names a country (gua-yellow-wall, nepal-bhaktapur-boy) now forward to that country’s page. A product page forwarded to a home page is what Google calls a soft 404, which is why they kept coming back in the indexing report. The dozen that name no country — the shop indexes, bodie-table, wedding-day — still land on the home page, there being nothing truer to send them to. Since 2026-09-01 the banknote band on a country page holds its notes in a row at every width. On a screen between roughly 760px and 1050px — a laptop, a landscape tablet, a browser window that is not full-screen — the notes were being squeezed into a column one note per line beside the heading. Every euro country had it, because the “Shared currency · … since 2002” line takes width on the same line; on Spain at 764px the notes were left a strip 17px wide. The notes now ask for the width they need and the band wraps around them, which is what the phone layout already did. Nothing further — the manual’s Friends section says what to send people, and rotating the password is one edit in the same Pages setting.
The phone uploader Since 2026-08-30 its album list is complete. The Worker finds unlisted albums by walking the bucket, and it read only the first page of each listing — so countries late in the alphabet were never walked and their unlisted albums were missing from the picker (and from Gallery cleanup’s droplists) while opening perfectly on the site. Fixed by following the cursor to the end of every listing. The upload Worker serves a phone-built page at /m since 2026-08-24 — searchable album picker, album creation in the flow, and upload-then-import as one action. Phones opening the upload page land there automatically; the desktop form is unchanged and always reachable at ?stay=1. Deploys from the trunk on every push, like the rest of the Worker. Since 2026-08-27 an upload with no file extension on its name is named from its own contents — the share sheet does not always hand a photograph to the Shortcut with its filename intact, and four photographs of Madrid reached storage as IMG_5064IMG_5067, were counted as stray files and were published by nothing while the run reported success. Corrected as the object is stored, and repaired at import for anything already in the bucket. See the pipeline page. Since 2026-08-27 it also carries a way through to the admin tools — a Publish to the live site card at the foot of the home screen, below the Upload Photos button, with Gallery cleanup, Cards & strips, Banknote pages and this manual beside it. They point at the staging admin deliberately: that is the master set, and the live copies sit behind Cloudflare Access. Nothing further — it is live. The manual’s pages about it reach production on the next Publish.
The test site Unchanged: GitHub Pages at brarob100.github.io/traveller.org takes every commit, and traveller-org.pages.dev still answers as the Pages project's own address. Both are useful — one shows the newest work, the other shows exactly what the public sees. Its admin pages are the master set (2026-08-22): they act on the same storage and the same pipeline as the live ones, they carry the newest controls, and the album list every admin page shows — live one included — is read from here. See Staging admin or live admin? Stays. Neither is going away at cutover.
Visitor statistics Google Analytics 4, property G-80N129L5BJ, live since 2026-08-26 on the public pages of the live site only — not /admin/ (the tools and this manual), not the test site, not the 622 old-URL forwarding pages. Any GA figure dated before 26 August is measuring the old WordPress site; the page titles say which is which (| Traveller's Tales - The Journal is the old one). Nothing further — it is live. Chosen over Cloudflare Web Analytics for continuity with the old figures. See the pipeline page.
DNS Cloudflare, zone Active since 2026-08-19. Porkbun is the registrar and nothing more; no record is ever created there again. Three names must stay grey-clouded (DNS only) — mail, cpanel, ftp — because Cloudflare's orange cloud proxies HTTP and HTTPS only. That trap was hit and repaired the same day: the import left the apex MX pointing at the proxied apex itself, which silently stops inbound mail. The 600-second TTL goes back up at step 25, once the dust settles.
Photographs images.traveller.org is attached to the bucket and works — a thumbnail answers 200 over it, verified 2026-08-19. The live site is still asking the old address. PUBLIC_R2_BASE is reported set, but the build that is serving was made before it took — a variable is read at build time only. Nothing is broken meanwhile: both addresses serve the same bucket. Every image request on images.traveller.org, one green build after the variable lands, finishing step 14.
Email Still InMotion, forwarding eight @traveller.org addresses to Gmail. The apex MX now reads mail.traveller.org, a grey-clouded A record at 216.194.165.246 — written 2026-08-19 to repair the proxied-MX fault above. Cloudflare Email Routing is not switched on: it refuses to activate while a non-Cloudflare MX sits on the apex, and that record is the one it means. Resend's MX on send.traveller.org is a different hostname, is not the conflict, and must not be deleted. Cloudflare Email Routing for receiving (step 15) and Resend SMTP for sending (step 16).
Contact form Live on www.traveller.org (checked 2026-08-20: the page carries the real site key, the Turnstile script and the live endpoint, and the contact Worker answers past its secret check). And now on staging too — the staging build was the one still missing both public keys, which is why brarob100.github.io showed the grey “Test build” line and a drawn checkbox that cannot be clicked. That is a build that never saw the keys, not a broken CAPTCHA. One dashboard step for staging: Cloudflare → Turnstile → the traveller.org contact widget → SettingsHostname Management → add brarob100.github.io. The widget renders only on hostnames it lists; without this it is a blank space where the checkbox should be.
Search & answer engines Switched on and correct (2026-08-19). robots.txt allows everything except /admin/ and names the sitemap; llms.txt and llms-full.txt are served and every link in them reads www.traveller.org. No AI crawler is blocked, deliberately — the llms files exist to be read, and blocking the crawlers that read them is the one change that would cost the site its answer- engine visibility. Checked against the live host, not the build. Nothing further. Step 22 tells Search Console, which is confirmation rather than introduction.
Admin on the phone Gallery cleanup and Cards & strips work one-handed since 2026-08-25: below phone width, Gallery cleanup becomes an album picker plus Browse / Select / Arrange modes with slide-up sheets, and Cards & strips becomes a hub of three doors with one Save bar that follows you and names what is unsaved — both built from the approved boards (design/ios-admin/). Reordering was flatly impossible on iOS before: the desktop pages drag with a mechanism iPhones do not have. Same machinery, same refusals; the desktop rendering of both pages is untouched. Nothing further — the manual pages describing both reach production on the next Publish.
The duplicate check Site-wide from 2026-08-25, and switched on album by album. Every import now stores a fingerprint of each photograph it publishes (in the bucket, under hashes/) and compares its arrivals against every fingerprint on file — so a photograph already published in another album is named in the import email. Nothing is deleted across albums, by design. An album that has not been fingerprinted yet takes no part in the check and the run log says so. Done for the exact check: the backfill ran on 25 August 2026 and fingerprinted 23,997 photographs across 210 albums. It found 622 published in more than one album (nothing deleted — see the import email and the pipeline page) and none doubled inside one album.
Open, and worth a look: reading every photograph turned up 274 that the site asks for at an address where nothing answers — broken pictures on the site today, none of it caused by the fingerprinting. Three separate reasons, each with its own fix: 186 in srilanka/2005 are filed in a place folder while the record and the page both ask for the flat address (the same photographs also publish correctly as srilanka/2005/colombo); 78 in turkey/2022, cambodia/2009/siem-reap and belgium/2016 were rotated again and the record still names the previous version of the file; 10 are simply not in storage. A sync run per album settles the last two, and it is your call, because it takes the missing ones out of the album’s count.
The upload Worker Answers on this domain since 2026-08-20, at www.traveller.org/api/ — a Workers route, no DNS record, created by the deploy that ships its code. Verified rather than assumed: /api/galleries refuses a wrong passphrase with 403, which is the Worker itself talking. Its own traveller-upload.brarob.workers.dev address still works and still carries the iPhone Shortcut. The first attempt was refused because the deploy token could write Workers Scripts and not Workers Routes; the owner added Zone · Workers Routes · Edit and the next push created it.
Since 2026-09-14 it also answers at upload.traveller.org — a plain address to type on a phone or hand to somebody helping with a batch. A Worker custom domain declared in wrangler.toml, so the deploy created the DNS record and the certificate itself; the dashboard cannot do this at all, which is what the 2026-08-20 attempt ran into. Verified the same minute, not assumed: the name resolves to Cloudflare, / and /m both serve the uploader carrying this deploy’s own build stamp, /galleries answers 403 without a passphrase (the Worker itself talking), and the certificate verifies. All three addresses are live and none replaced another: /api/ is still what the admin pages call and still the only same-origin one, and workers.dev still carries the iPhone Shortcut.
The route exists so the admin pages call it on a hostname that resolves: after the Access lockdown every interactive one went blank on “Failed to fetch”, and staging then sat for minutes on end with empty panels — the same workers.dev hostname, black-holed rather than refused. The staging build now points at www.traveller.org/api too, which is the same Worker by a name that answers.
Access must never cover /api/*, or the pages break again. If a browser still holds the old address, press Sign out on the admin login and connect again — a remembered address wins over the offered one, deliberately.
The admin pages Behind Cloudflare Access since 2026-08-19 on www.traveller.org/admin* (owner; verified again 2026-08-20 and 2026-08-26 — an anonymous request is answered with a 302 to the one-time-PIN login, not a page), with the upload Worker behind them passphrase-gated and rate-limited on top. A consequence worth knowing when something looks wrong: a script or a session checking whether a change reached the live manual is reading the body of that redirect, not the page — which is exactly how a landed deploy was once reported as stuck (2026-08-26).
Work on the live domain, not on staging, when a browser is fussy. Staging is on github.io, so every call it makes to the Worker is cross-site — there is no same-origin route to offer and GitHub Pages cannot have one. A browser that refuses cross-site requests therefore refuses all of them, whatever address is typed: on 2026-08-20 the owner's browser answered the Worker perfectly in a tab and blocked the same call from the staging page at the same moment. On www.traveller.org/admin/ the call goes to /api on the same name, and there is nothing cross-site left to refuse.
Fast again since 2026-08-20 (evening). The week's slowness had one active cause left after the address moved to /api: the Worker's /galleries endpoint — which every interactive page asks on connect — was reading the entire photo bucket object by object, ten to twenty seconds a call. It walks directories in parallel now and answers in under a second; the Pipeline page has the mechanics. Access was never the cause: it has never covered /api/*, and the pages were loading their data the whole time — late. The PUBLIC_UPLOAD_URL variable on the Pages project no longer matters either way: every page on this domain now prefers its own /api address in code.
DMARC reports Watched, not monitored, since 2026-09-10 — the switch is thrown. The policy published on 19 August 2026 is p=none — monitor only, nothing is blocked — and the daily reports have been arriving at dmarc@traveller.org ever since. The first five (31 Aug – 3 Sept, Google and Outlook, 24 messages) were read on 2026-09-10 and were entirely clean: every message that claimed the domain proved it, from just two senders — Resend (import notices, contact form) and Cloudflare Email Routing forwarding to Gmail.
On 2026-09-10 the owner re-pointed dmarc@traveller.org from their Gmail to the traveller-dmarc Worker (that rule had forwarded to Gmail since cutover step 15) and set the Worker’s RESEND_API_KEY. So the reports no longer land in the inbox: the Worker reads each one and writes only when something needs acting on, forwarding the original report alongside the alert. Silence from it means checked and clean.
Nothing, until it speaks. The Worker’s request count on its own page shows it being invoked in the meantime, which is what distinguishes “quiet because clean” from “quiet because nothing is arriving”. After a few quiet months the accumulated evidence supports moving the policy from p=none to p=quarantine, which is what actually stops somebody sending as this domain.
The guides shelf Live and open since 2026-09-05 — no door, by the owner’s choice. www.traveller.org/guides/ is the shelf of end-user guides for every app on the domain. Since 2026-09-06 every app but one has its guide: the owner asked for first versions of all of them in one pass, and seven were written from their repositories’ own documentation and code — 26 pages — each app’s pages living in its own repository and synced onto the shelf within a minute of the push, which also proved the whole pipeline end to end (the read token reached all seven private repositories; the dispatch ping fired from every one). The exception is contacts, whose repository is empty — nothing exists to document accurately, so it stays listed and waiting. (apartment-alerts came off the list earlier the same day, the owner’s call — no longer part of the documentation project.) and waiting for their first page. The shelf is unlinked from the public site, noindex, disallowed in robots.txt and absent from the sitemap, but anyone with the address can read it — every guide is written as if public. The Cloudflare Access application for www.traveller.org/guides* was designed and deferred (owner, 2026-09-05); it sits on the runbook’s open list. Since 2026-09-07 an app's own documentation reaches the live shelf by itself — the owner's golden rule: a push to any tracked app's docs/guides pings this repository, the sync copies the pages in, and it then publishes them to production without anyone pressing anything, usually within a few minutes. It publishes only when those guides are the sole thing waiting on the trunk; anything else pending and it stops at staging and says so, since a publish moves the whole trunk head. That is the one standing exception to “nothing goes live until the owner says publish it”. The shelf has a door, since 2026-09-14. A second Cloudflare Access application covers www.traveller.org/guides* — separate from the admin’s, with its own allow-list of two: you and Reynald, by one-time PIN. Verified the same day from outside: /guides/ and every page under it answer with the login instead of the page, and the public site is untouched. Because the door exists, the cards now carry each app’s own address, including ones withheld while the shelf was open. The test site on github.io can never have this door, so a staging build strips every address — if the Access application is ever removed, the addresses come out of src/data/guides.json in the same change. Since 2026-09-10 an app can be held off the shelf while it is being rebuilt: the Amapas 353 website’s guide is written and waiting in that app’s repository, kept off the shelf at your request because the site changed faster than the guide could be written. The runbook’s open list says how to bring it back. Since 2026-09-07 each app has a colour: its name rides a band of it on the shelf and carries it on the left edge of the side nav, which now lists every app on every guide page (the open one filled with its own colour, its pages nested under it) so no one has to scroll the shelf to see what exists. Colours live in src/data/guides.json; an app without one is given one automatically, and the suite measures that no two apps share a colour and that each is dark enough to carry the white name on it. Since 2026-09-06 the shelf is visually its own place — the owner asked twice, first for a standalone area, then for one markedly different from the site: no public-site nav (the App Guides masthead, the back link and each app’s sidebar are the wayfinding), a cool near-white ground with a teal accent against the site’s warm cream and amber, the reader’s own system type instead of Archivo, and code panels highlighted on a light ground. The divergence is asserted by tests/guides.test.mjs, so re-unifying the two designs by accident turns a test red. Since 2026-09-06 the shelf carries its own manual: a Centralized Documentation card whose page, Adding a new app to the shelf, holds the five steps end to end — each one a stand-alone “what you do” list, none needing a terminal — and closes with a prompt the owner pastes into a Claude Code session on a new app’s repository to have the repository work done for them. Since 2026-09-05 the sync is armed: the owner created its read token — named CENTRAL_DOCS_GIT_TOKEN, the owner’s choice, and every page of this manual now uses that name — so the nightly pass can read the app repositories the moment any of them has a docs/guides folder. Each app adds docs/guides/*.md; from then on the nightly sync brings guides across as commits the Publish page lists. The door goes up whenever the owner wants it: the second Access application (allow-list of one, widened app by app), which locks the shelf without any change to the site.
Uploading from the phone The Shortcut picks its destination from a list — and only from the list (2026-08-21). The recipe on Uploading from an iPhone reads /albums.txt from the Worker: every existing album, one per line, nothing typed. The + New album… row and the typed fallback behind it were retired on 2026-08-21 — that fork (an If, a second prompt, two tokens to re-point) was where every failure the recipe had actually lived, and the owner asked for it to go. A new album's first batch starts on the upload page, which can ask for what a new album needs; from then on it is a line in the list. Legacy albums are deliberately not offered: uploading "into" one would land a batch beside a record that cannot hold it. Typing one is still possible, and now survives being typed badly. A country is snapped onto the site's roster at both ends of an upload — United States, united-states and Hong Kong all land in the country that exists — and a batch already stranded under a wrong spelling relocates itself on import. Folders that pre-date this show as · not imported on Gallery cleanup, with an Import button of their own. Since 2026-09-13 a same-name upload never overwrites by accident: a file that differs from the one already in the album is filed under the next numbered name, and only an explicit Replace on the desktop or phone page writes over — the Shortcut, which cannot ask, can no longer replace anything. The desktop destination list now reads and sorts Country — year — album name (same date).
Photographs from an iCloud link Switched on since 2026-09-13 — a third way in, beside dragging and the phone. A Photos Copy iCloud Link pasted on the desktop upload page or the phone page (/m) makes the pipeline fetch the originals from Apple into the album and import them; nothing passes through the owner’s machine. Same sizes, same thumbnails, same checks as any batch. Built the day a runner-side probe proved the originals were reachable behind a link (Addenda 331–332); the recipe rests on Apple’s undocumented web client, which is the one thing that can change without notice. Overlap with an album is decided by rule on the runner, not shown up front; a pre-flight with counts and a fetch-new / replace-all choice is planned and set aside on the runbook’s open list (2026-09-13). Every “started” message on the upload pages now says staging, since that is where an import lands; Publish is what sends it live (2026-09-13). Nothing. When a link stops working at the fetch stage, run the probe workflow on it and read its log — the shape that moved will be there. The two zero-code routes (iCloud for Windows to a folder; the link’s own Download button) remain as they were.

Cutover progress lives on Launch & cutover, where finished steps carry a green Done chip with the date. As of 2026-08-20, steps 1–21 are done (18 and 21 both closed that day) — the domain is on Cloudflare, the new site is what the world sees, mail is routed and DMARC published, the contact form is live, and the admin door is locked (step 21): Access asks for the one-time PIN and let the owner in the same day.

The admin door starts at www.traveller.org/admin/, which lists every tool. It had no page of its own until 2026-08-20 — the tools all live a level down — so that address answered with the site's own 404.

Services in the stack

Every row links straight to the console page where that service is actually changed — no hunting through dashboards. Cloudflare links land on the account picker if more than one account is signed in; choose the traveller.org account once and they go direct thereafter.

Why photographs sit on images.traveller.org. Not for tidiness — they cannot go anywhere else. There are 46,174 objects in the bucket (a 2048 px copy and a thumbnail for each of 23,087 photographs, about 14.5 GB). A Cloudflare Pages site on the free plan may contain 20,000 files, so they could not ship with the site even if the repository were willing to hold them; the whole production build is 973 files by comparison. They live in R2, which is a separate service and has to be addressed by some hostname — the only real choice is between a memorable one and the bucket's default pub-….r2.dev. Two things follow for free: the bucket can be swapped or moved without a single URL on the site changing, and images are cached under their own rules, immutable and forever, independent of how often the pages themselves change.
Who hosts what, today. The pages you are reading are served by GitHub Pages (the test address), which takes every commit. Cloudflare Pages builds the production branch and has been www.traveller.org since 2026-08-19, as well as traveller-org.pages.dev. The photographs are on Cloudflare R2, and the upload page is a Cloudflare Worker. InMotion no longer serves the website at all — every visitor now lands on Cloudflare — but it is not finished: it still holds the 393 redirects for old URLs, still runs cPanel and webmail on mail.traveller.org, and still forwards every @traveller.org address to Gmail. That last job moves to Cloudflare Email Routing the moment the apex MX is freed, and InMotion can be cancelled after the redirect overlap. Prices below were re-checked against Cloudflare's published rates in August 2026.
ServiceWhat it doesCostOpen its console
GitHubHolds the repository — all content, code, design, and the full history of every change. Editor identity: collaborators on the repo are the people who can publish.GitHub Pro (private repo + Pages)Repository · Collaborators · Secrets
GitHub ActionsThe build robot. Every push runs npm run build (Astro) and deploys the result. Nothing is ever built by hand.includedAll runs · Photo import · Deploy · Worker deploy
GitHub PagesThe test server: brarob100.github.io/traveller.org. Serves whatever the last successful build produced.includedPages settings · Live test site
Cloudflare PagesThe production host since 2026-08-19 — same static output, unlimited bandwidth, 500 builds/month free. Cloudflare put Pages into maintenance mode (confirmed 2026-09-09): new work goes to Workers instead, but Pages still runs and builds with no announced shutdown date, so this site stays where it is — the runbook's Still open list carries what a move would cost and when to make it. Builds from the production branch, which only moves when the Publish button is pressed — staging takes every commit, the live site takes what you approve.$0 — unlimited bandwidth and requests, 500 builds/month, 5 custom domains per project. The paid tier ($5/mo) buys more builds, which this site will not need.Workers & Pages
Cloudflare R2Object storage for full-size images (the photo library outgrows any static host's file limits). Zero egress fees; serves images on the same CDN.~$1/yr today (23,087 photographs ≈ 14.5 GB; first 10 GB free, then $0.015/GB-month). Roughly $2/yr per additional 10,000 photographs.traveller-images bucket · API tokens
Decap CMSThe editing interface at /admin (in progress). Saves records straight to the GitHub repo under your name; each save publishes automatically.$0not built yet
Cloudflare AccessThe door on www.traveller.org/admin*, switched on 19 August 2026 (decided 2026-08-18, no longer optional; set up at runbook step 21, ticked on the 20th): an email allow-list verified by one-time PIN, so only named people ever see the admin pages (including this manual). It does not cover the upload Worker's own workers.dev address — that door is the passphrase, which rate-limits failed guesses.$0 (≤50 users)Zero Trust dashboard
Friends door (Pages Function)The shared-password gate on the unlisted albums: friends sign in once at www.traveller.org/friends/ and the unlisted album pages open for a year per device. Runs inside the site’s own Cloudflare Pages Functions (functions/_middleware.js + functions/friends/login.js) — no extra service, no extra cost. The password is the FRIEND_KEY Pages project setting (see the credentials table on the pipeline page); it is deliberately not Cloudflare Access (that door is an email allow-list for you) and not the upload passphrase (that key starts machinery).$0Pages project → Settings → Variables and secrets
Guides shelf + Cloudflare Access (second app)The end-user guides of every app on the domain at www.traveller.org/guides/, built from a copy of each app’s docs/guides/*.md that the nightly Sync app guides workflow commits into this repository (src/data/guides.json lists the apps). Its door is a second Cloudflare Access application on www.traveller.org/guides* with its own allow-list — separate from the admin’s so an app’s users can be let in without being let into the admin. Deferred by the owner on 2026-09-05 — the shelf is live and open (see “Where the domain stands today”). The sync’s read token is CENTRAL_DOCS_GIT_TOKEN (credentials table on the pipeline page).$0 (Access ≤50 users; Actions minutes negligible)Zero Trust dashboard · Sync runs
Upload WorkerThe traveller-upload Cloudflare Worker, reachable at three addresses, all declared in wrangler.toml and all live: upload.traveller.org (a custom domain, since 2026-09-14 — the plain one to type), www.traveller.org/api/ (a route, since 2026-08-20 — the one the admin pages call, because it is same-origin) and its own traveller-upload.brarob.workers.dev (which the iPhone Shortcut carries). See “Where the domain stands today” above. It serves: the passphrase-gated bulk upload page (no file limit, keeps place folders, accepts RAW) and the zip-records API behind the zip records page. Source: cloudflare/upload-worker.js.$0Upload page · Edit code · Secrets
ResendTwo jobs: sends the zip-record download link after each photo import (API key = the RESEND_API_KEY GitHub secret), and after cutover is the SMTP relay that lets Gmail send as brobinson@traveller.org and friends (a second API key, typed into Gmail's Send-mail-as settings — cutover step 16).$0 (100 emails/day free, both jobs combined)Sent emails · API keys · Domain
Anthropic (Claude API)Drafts the banknote write-ups: when a note scan arrives, the pipeline sends both faces to Claude, which reads the note and writes The artwork / In circulation / Details straight onto the note page; you edit or re-generate it afterwards at Banknote pages. Key = the ANTHROPIC_API_KEY GitHub secret; the model is the ANTHROPIC_MODEL GitHub variable (see the credentials table on the pipeline page).pennies per note (pay-as-you-go API credit)Console · API keys · Usage & billing
Cloudflare TurnstileThe contact form's CAPTCHA — the "I'm not a robot" check. Privacy-preserving and usually invisible: most visitors are never asked to click anything. Site key is public (in the build — on the Pages project for production and in deploy.yml for staging), secret key lives on the contact Worker. The widget renders only on the hostnames listed in its Hostname Management: today traveller.org, www.traveller.org and brarob100.github.io. On any other name it draws nothing, which looks exactly like a broken key.$0 (unlimited)Turnstile widgets
Contact WorkerThe traveller-contact Worker: verifies the CAPTCHA, then emails the message to you through Resend with Reply-To set to the sender, so Reply just works. Deliberately separate from the upload Worker — a public endpoint never shares an isolate with the admin passphrase. Source: cloudflare/contact-worker.js.$0Edit code · Secrets
DMARC WorkerThe traveller-dmarc Worker (cloudflare/dmarc-worker.js): reads the daily DMARC aggregate reports sent to dmarc@traveller.org — the machine-readable record of everything that claimed to be this domain and whether it proved it — and emails you only when something needs acting on. A clean report produces no mail at all. Built 2026-09-10 so the reports do not have to be read by hand; a by-hand reader for a pile of them is .github/scripts/dmarc_check.py.$0Edit code · Secrets
Cloudflare Email RoutingForwards the eight @traveller.org addresses to Gmail after cutover, replacing InMotion's forwarders. No credentials — destination inboxes verify themselves by clicking a link. The one two-inbox address, theboys@, routes through the traveller-email Worker (cloudflare/email-worker.js, auto-deployed). Set up in cutover step 15 on Launch & cutover.$0 (unlimited forwards)Email Routing rules
Google AnalyticsVisitor statistics for the public pages of the live site, property G-80N129L5BJ, installed 2026-08-26. Excluded on purpose: everything under /admin/, the test site, and the old-URL forwarding pages. The measurement ID is not a secret — it ships in the HTML of every public page, which is how the tag works, so there is nothing to rotate and no credential to store.$0Google Analytics
InMotionThe legacy host. No longer serves the website (2026-08-19) — it now only holds the 393 old-URL redirects, cPanel and webmail on mail.traveller.org, and the domain's email forwarders until Email Routing takes over. Retires after that plus a redirect overlap period.existing plan, then $0AMP control panel
Two links that are not services but get used most: the photo import workflow (watch a batch process) and batch zip records (download or clear the record copies). Credentials — which key lives where, and how to roll each one — are on the publishing & image pipeline page.

How the site is put together

Content (the part you own)

  • src/data/galleries.json — every gallery: country, year, photo list, and an optional base: "r2" flag marking galleries already upgraded to high-res.
  • src/data/journal.json — all 161 journal entries (title, city, date, country, body, images).
  • src/data/currency.json — 33 countries of banknotes with scans and artwork notes.
  • src/data/places.json — photo→place tags per gallery (Rangoon, Bagan, …). These drive the city-grouped gallery sections and lightbox captions.
  • src/data/home.json — what's featured: carousel slides, home-page journal cards, country card photos.
  • public/thumbs/ — 460 px thumbnails for legacy galleries, deployed with the site. Upgraded galleries serve thumbnails and full-size images from R2; unreplaced full-size images hotlink the legacy server until their batch arrives.

Design (the part a future redesign replaces)

  • src/layouts/ + src/pages/ — the page templates.
  • src/styles/global.css — the whole "Darkroom Daylight" design system: colors, type, every component.
  • design/site-daylight.html — the approved design reference, every page type in one file.

Design boards — the source of truth for UI projects (25 August 2026)

Every screen that was designed before it was built lives in the repo as a set of boards, and the boards are what to reopen — by you, or by Claude in a future session — before changing one of those screens or building a new one in the same idiom. They record the approved look and the decisions behind it (annotations on the boards say why a control is where it is), which the built page alone cannot.

  • design/ios-upload/ — 9 boards: the iPhone uploader at /m (unlock, album picker and search, new-album, currency, uploading, done and failure screens). traveller-uploader-iphone.html beside them is the whole set as one browsable file.
  • design/ios-admin/ — 17 boards: Gallery cleanup on the phone (Browse / Select / Arrange modes, the sheets, import, delete) and Cards & strips (hub, carousel, letters home, country covers, the pickers), plus the two flow maps. traveller-admin-iphone.html is the one-file render.
  • design/iphone-nav/ — 4 boards: the site’s own navigation on an iPhone in portrait, where the four sections used to straggle across two ragged lines under the wordmark. The four directions drawn were a centred masthead, a sideways-scrolling swipe strip, a menu sheet, and an iOS tab bar; you chose the masthead on 30 August 2026 and it is what the site now does below 700px — wordmark centred on its own line, a hairline, then the four sections centred beneath it. The other three boards stay as the record of what was weighed.
  • design/iphone-photo-view/ — 5 boards (2026-09-06; you chose Option A the same day and it is built — the slim bar, the full-width photograph, the caption below, the admin verbs behind one chip; the masthead stays, per your 2026-08-30 nav decision, and so do the filmstrip arrows. Option C followed the same day (“iPhone horizontal should snap to the window”): turning the phone sideways pins the photograph over the whole window on the board’s dark ground, count pill fading, rotate back to leave. Board B remains the record of what was weighed, and its built-in zoom is the natural next step if the appetite returns): the public photo view on an iPhone, drawn because the photograph was getting about a tenth of the screen and detail needed pinching. Beside the current layout sit three combinable options — A, the postcard promoted (light page, photo full-width under one slim bar, admin verbs behind one chip); B, a dark-room viewer (photo edge-to-edge and centered, chrome overlaid and tap-hidden, pinch or double-tap zooming into the 3000px large); and C, turn-the-phone (rotating fills the screen — 2.4× the photo area — with no new chrome). iphone-photo-view-options.html is the one-file render.
  • design/site-daylight.html (with its editable -source.html) — the public site, every page type; unlike the boards above it is kept in sync: any visual change to a public page updates it in the same push.
  • design/design-directions.html and design/home-variations.html — the exploration that preceded the approved design: the directions considered and the home-page variants. History, not truth — kept because they show what was rejected.

The two ios-* folders are design-canvas projects: each screen is its own .dc.html artboard (open it in any browser), canvas.json lays them out, and the single-file render beside them is the shareable copy. The phone pages were built from these boards screen for screen — where a built page and its board disagree, the decision log says which one moved and why (Addenda 249–250 for the uploader, 258–260 for the admin pages).

Computed, never typed

Country stats (galleries, photographs, years, banknotes), region groupings, listing filters, counts on cards, llms.txt / llms-full.txt, the sitemap and every page’s structured data are all derived at build time from the data files. Add a photo and every number that should change, changes on the next publish.

Where documentation lives

  • This manual (/admin/docs/) — how to operate the site. Updated in the same push as any change to a process it describes. Golden rule: any service added to or removed from the stack must appear in the table above in the same push.
  • docs/ in the repo — dated decision logs: what was decided, when, and why.
  • CMS.md and REDESIGN.md in the repo — the full CMS plan and the original redesign analysis.
Access note. The live site's admin pages have been behind Cloudflare Access since 19 August 2026 (owner) — every address under www.traveller.org/admin*, this manual included, answers a stranger with the one-time-PIN login rather than a page. The upload Worker's own workers.dev address is not covered by it and never can be: its door is the passphrase, which rate-limits failed guesses.
The test site is a different matter. There these pages are unlinked and marked noindex, and there is no login at all — GitHub Pages cannot take one, and putting staging behind Access was offered and declined (see the runbook). So: don't put secrets in these pages, on either site. The credentials table on the pipeline page says where each secret is changed, never what it is.
Reading this on a phone or iPad: the manual sets itself larger there — bigger type, a shorter line, and thumb-sized tabs. On a phone the tab row becomes a single strip you swipe sideways, opened at whichever page you are on, so the five tabs off the edge are a swipe away rather than missing. Wide tables get their own sideways scroll inside the page; the services table above instead drops its description and cost columns on a phone, since what you want from it there is the console link.

Every page under /admin/ (this manual, the zip records page) shows a different browser-tab icon than the public site — an easy visual cue that you are on an admin page, and the bulk upload page carries the same icon. Source: public/admin-favicon.ico, inlined into the Worker page so it needs no external file.