37d5320664
A link to tera.lumbridgecorp.com or office.lumbridgecorp.com unfurled as a bare
blue URL. No picture, no sentence, and a title — "Lumbridge Simulate — San
Francisco" — that was wrong at one of the two doors and stale at the other.
There were no `og:` or `twitter:` tags in the document at all. For a project
whose entire pitch is that you should look at it, that is the most expensive
missing markup in the repo.
Both cards are screenshots of the running app, not drawings of it, and that is
the load-bearing decision rather than a shortcut. `scripts/brand-assets/` builds
them in two passes: shoot the city and the office out of `dist/`, then render
`og.html` over those shots at exactly 1200x630. The thing is worth looking at,
and a drawing of it goes stale in silence — which is not hypothetical. The card
currently live on lumbridgecorp.com is a viewport screenshot of a marketing page
that has since been rewritten, so that preview advertises a positioning the site
no longer uses, and it has been doing so for a month with nobody noticing. A
card regenerated from `dist/` by one command is a card that can be kept true by
running the command.
The clock is shifted to midday for the capture, because the sun is real —
`observe()` computes it from `new Date()` — and a card regenerated at two in the
morning is an honest photograph of a black rectangle. Shifted rather than
frozen: everything else runs off `requestAnimationFrame`, and a stopped clock
stalls the frame loop the screenshot is waiting on.
Every string on the cards is the project's own. The headlines are what
`README.md` already says each half is; "Clone it and it works" is CONTRACT.md
§0's acceptance test in the words `main.ts` uses for it; the licence in the
corner is the one in the repo root. A share card is the most-read and least
reviewed sentence a project has, which is exactly why it should not be where new
claims get invented.
The harder half was that the two doors are one bundle. The app sorts out which
door it is by reading its own hostname; a crawler cannot, because it reads the
HTML and nothing else — so one `index.html` means both doors unfurl as the same
place, and being a different place is the office door's whole reason to exist. A
second hand-written shell is what the Caddy config already argues against for the
static root ("sharing the root rather than copying it means a deploy cannot leave
the two doors on different builds"), and two 900-line files each carrying the
inline stylesheet would drift on the first CSS change with nothing to notice.
So the build emits both. Everything outside the `ogc:` markers is copied byte for
byte — verified: `office.html` and `index.html` are identical below `</head>` and
point at the same bundle hash — and only the head block differs. A change to the
interface reaches both doors by construction. It runs in `writeBundle` rather
than `transformIndexHtml` because it needs the finished document, after Vite has
rewritten the asset URLs, and it errors rather than no-oping if the markers go
missing: a card that is quietly the wrong one is the failure the plugin exists to
prevent.
`deploy/Caddyfile.snippet` documents the one line that turns it on — the office
door's `try_files` fallback pointing at `/office.html` off the same shared root.
A deployment that skips it is not broken; the office door keeps working and
unfurls with the city's card, which is what it did before.
Not deployed. The live Caddyfile still falls back to /index.html for the office
host, so this needs that one-line change on cloud-2 before office.
lumbridgecorp.com unfurls as the office.
Both shells boot clean in Chrome with zero console errors, 31 client tests and
the no-binary gate still pass — `public/` is exempt from it, which is where the
two PNGs live.
70 lines
2.5 KiB
Caddyfile
70 lines
2.5 KiB
Caddyfile
# The one Caddy snippet.
|
|
#
|
|
# Three server designs each brought their own, on three different ports, and all
|
|
# three wrote to this filename. This is the one that replaced them: one service,
|
|
# one port, one prefix. CONTRACT.md §5.
|
|
#
|
|
# Install it beside your Caddyfile and import it into whichever site serves the
|
|
# Tera browser build:
|
|
#
|
|
# import /etc/caddy/snippets/tera-api.snippet
|
|
#
|
|
# tera.lumbridgecorp.com {
|
|
# import tera_api
|
|
# root * /srv/tera/dist
|
|
# file_server
|
|
# }
|
|
#
|
|
# The API and the static build are deliberately the same origin. Nothing here
|
|
# needs CORS, which is why TERA_CORS_ORIGIN defaults to empty — set it only for
|
|
# a Vite dev server on another port.
|
|
#
|
|
#
|
|
# ## Two doors out of one root
|
|
#
|
|
# `office.` and `tera.` are one build, and the app reads its own hostname to
|
|
# decide which one it is. A crawler cannot do that — it reads the HTML and
|
|
# nothing else — so `npm run build` emits a second shell, `office.html`, which is
|
|
# byte-identical below `</head>` and carries the office's title, description and
|
|
# share card. Point the office door's fallback at it and the two unfurl as the
|
|
# two places they are:
|
|
#
|
|
# office.lumbridgecorp.com {
|
|
# import tera_api
|
|
# root * /srv/tera/dist # the SAME root as tera., not a copy
|
|
# try_files {path} {path}/index.html /office.html
|
|
# file_server
|
|
# }
|
|
#
|
|
# The root is shared rather than copied on purpose — a deploy cannot then leave
|
|
# the two doors on different builds — and `office.html` comes out of the same
|
|
# `npm run build` as `index.html`, so the shells cannot drift from each other
|
|
# either. The only line that differs between the two site blocks is the
|
|
# `try_files` fallback.
|
|
#
|
|
# A deployment that skips this is not broken: the office door keeps working and
|
|
# simply unfurls with the city's card, which is what it did before there was a
|
|
# second shell at all.
|
|
|
|
(tera_api) {
|
|
handle /api/v1/* {
|
|
reverse_proxy 127.0.0.1:8431 {
|
|
# Fail fast rather than holding a browser connection open while the
|
|
# API is restarting. systemd brings it back in under two seconds.
|
|
transport http {
|
|
dial_timeout 2s
|
|
}
|
|
}
|
|
}
|
|
|
|
# The API stamps its own Cache-Control — `private, no-store` by default, and
|
|
# `public, max-age=…` only where a route opted in. Do not add a cache
|
|
# directive here: this file cannot tell which route answered, and the
|
|
# fail-closed policy is only fail-closed if nothing downstream overrides it.
|
|
|
|
header {
|
|
X-Content-Type-Options nosniff
|
|
Referrer-Policy strict-origin-when-cross-origin
|
|
}
|
|
}
|