Troubleshooting
Most of what goes wrong with this theme goes wrong loudly, because a lot of its guards are deliberate build-time throws. This chapter covers those first, then the genuinely silent failures.
Build throws that are working as intended
“SITE_URL is unset or still the placeholder”
Error: SITE_URL is unset or still the placeholder. Set it to your production domain in your
host's environment variables before deploying.
Your build environment reports itself as production and site is still example.com. Set SITE_URL in your host’s build environment — not the Worker’s, not wrangler.jsonc.
If you are seeing this on a preview deploy, something is setting a production signal where it should not be. The gate reads Netlify’s CONTEXT, Vercel’s VERCEL_ENV and a generic DEPLOY_ENV; set DEPLOY_ENV=production in your production build environment and nowhere else.
Note that pnpm deploy sets DEPLOY_ENV=production for you, so running it without a SITE_URL will always throw. That is the point.
“siteData.socials still holds platform homepages for: …”
The same gate applied to the four social caps on the cabinet. Shipped as-is, they are four buttons that send a reader to facebook.com.
Replace them with your real profile URLs, or delete the entry — a platform left out renders as an inert moulding rather than a dead link, which is a perfectly good answer for a platform you do not use.
“No channel in navData for page "X"”
channelFor(label) threw. A page declared a name that no channel in navData.channels carries.
Either the label has a typo, or you added a page without adding its channel. The theme’s rule is to do both in the same commit.
“404 lineup names X, which is not a channel in navData”
assertChannelLabels threw. The 404’s lineup map has a key that no longer matches a channel — usually after renaming a channel and missing the map.
“Author "x" not found for "y"”
A blog entry’s authors references an id with no matching folder in src/data/authors/. The reference is by folder name.
A Zod error naming a content entry
Frontmatter does not match the schema. The message names the entry and the field. The most common ones: a missing hero on a blog post (it is required, because it is the OG image), an empty categories array (non-empty is enforced — every frame draws a FILED UNDER line), a missing order on a work entry, or an image path that does not resolve.
The contact form returns 405
The single most confusing failure in the theme, because curl does not reproduce it.
Symptom: the form works in every scripted check, and a real browser submit returns 405 Method Not Allowed.
Cause: run_worker_first: ["/api/*"] is missing from wrangler.jsonc. Cloudflare’s asset router sees a request before the Worker, and not_found_handling: "404-page" makes it claim every browser navigation that matches no file — which is what a <form method="post"> submit is, since it carries Sec-Fetch-Mode: navigate. The asset router serves GET and HEAD only. A curl POST has no navigate header, so it sails past to the Worker and returns its 303.
If you moved the endpoint, move the glob with it.
The contact form says “mail me directly”
RESEND_API_KEY is unset. This is a supported state, not a bug — the form validates the message and shows the reader the direct mailto, and the endpoint logs the misconfiguration:
RESEND_API_KEY is unset — the contact form accepted a message and could not send it.
Check your Worker’s logs. On Cloudflare: pnpm exec wrangler secret put RESEND_API_KEY.
Mail sends but never arrives
You are on the default CONTACT_FROM, which is Resend’s shared sandbox sender. It delivers only to the address that owns the API key. Sending anywhere else needs a domain verified in Resend, then CONTACT_FROM set to an address on it.
The site does not match the screenshots
The three drawn faces — Space Mono, JetBrains Mono, Courier Prime — do not ship. Each stack falls back to system monospace, which is legible but not the design. Install them and add the @font-face blocks; Typography has the steps.
Something renders un-themed, or a layer goes opaque black
Almost always a token declared in .dark but not in :root. In light mode var() then hits “invalid at computed-value time” and the property falls back to its initial value — so opacity: var(--missing) becomes opacity: 1, and a 9%-opacity scanline layer becomes an opaque black sheet. Nothing throws and nothing logs.
pnpm test catches it. theme-parity.test.ts asserts that nothing exists only in .dark, and that every @theme inline bridge points at a variable :root actually defines.
If an element renders with no colour at all, check that the bridge in tailwind-theme.css uses @theme **inline** — without inline these resolve to literals at build time and stop flipping.
Something is unreadable
contrast.test.ts scores 25 pairs against WCAG AA and will tell you which one dropped. Note that opacity is part of the calculation: if you dim a label, you have changed its contrast.
If you dim a fastext F-number, change FKEY_DIM in Sections/Global/_fastext.ts rather than the components — the check imports that constant, and the components read it. That module exists precisely because the two components once held a copy each and drifted, leaving one at opacity-60 and 3.72:1 with every gate green.
A scroll animation freezes
Two likely causes:
overflow-hidden on an ancestor. A hidden box is a scroll container, so view() tracks it instead of the real scrolling ancestor. Use overflow-clip. This is the constraint TubeStill exists to own.
No scroll-timeline support. In Firefox, an unsupported animation-timeline declaration is dropped and the animation runs once, time-based. For entrances that is the documented degradation. For scroll-only shapes it is broken, which is why parallax-*, ken-burns and fade-through are made inert by the @supports guard. If you added a scroll-only animation, add it to that list.
An animation still plays under reduced motion
The global guard zeroes durations — and a scroll-driven animation has none to zero, because its progress is tied to scroll position. Add motion-reduce:animate-none explicitly. This is the theme’s most reusable motion lesson.
Content-config edits do nothing
Astro loads the content config once at startup, so edits to src/content.config.ts do not hot-reload. A running dev server keeps the old schema and can silently drop or mis-validate entries. Restart the dev server.
The same holds for astro.config.mjs, and in practice for navData.json.ts — a stale server can leave you debugging a channelFor throw you already fixed.
The install warns about ignored build scripts
Working as designed. pnpm-workspace.yaml’s allowBuilds denies post-install build scripts for packages that do not need to compile for the project to work.
The production bundle is bigger than expected
The dev catalog. /examples/ui emits no HTML in a production build, but that guard stops the pages, not the assets — the demo primitives’ styles and scripts still reach the shared stylesheet and dist/_astro/. Measured: 24 JS chunks with it against 6 without, and a 22.6% larger shared stylesheet.
Delete src/components/Sections/UiCatalog/ and src/pages/examples/ before launch.
The Worker bundle appears as public files
assets.directory in wrangler.jsonc is pointing at dist rather than dist/client. This adapter splits the build — pages and assets in dist/client/, the Worker in dist/server/ — so dist publishes the server bundle.
Stale comments in the source
The code is heavily commented and the comments are usually right, but three have fallen behind. None of them affects behaviour; all three can waste an afternoon.
| Where | Says | Actually |
|---|---|---|
wrangler.jsonc header |
the Worker lands in dist/_worker.js/ |
it lands in dist/server/. The assets.directory setting below it is correct |
styles/fonts.css header |
the woff2 “is also preloaded in BaseHead” | there is no font preload, deliberately — BaseHead explains why |
styles/tube-type.css header |
“the seven steps” | there are fourteen .tube-* steps |
The README.md figure for motion utilities is right at 89, and the icon count of 572 is right too — those were checked against the source for this documentation.
When you are genuinely stuck
Run the full gate and read what fails:
pnpm lint && pnpm check && pnpm build && pnpm test && pnpm wiki:lint
The theme also ships its own knowledge base in wiki/, one page per subsystem, with path:line citations that pnpm wiki:lint keeps honest. wiki/index.md is the entry point, and wiki/log.md records why things were removed — including the i18n layer and the Keystatic CMS, with the shape to restore if you want either back.