Troubleshooting
Grouped by what you are looking at when it happens.
The build fails
“SITE_URL is unset or still the placeholder.” You are running a production deploy on https://example.com. Set SITE_URL in your host’s environment variables. If your host is neither Netlify nor Vercel, also set DEPLOY_ENV=production — in the production environment only, or preview deploys will start failing too. This throw is deliberate: it makes it impossible to ship canonical URLs, a sitemap and JSON-LD pointing at a placeholder.
A content entry fails validation. The message names the entry file and the field. The usual causes are a category or industry that is not in its label set, a missing heroImageAlt, an authors: [], or an image path that does not resolve. Fix the frontmatter — the schema is the specification.
blog: post "…" references an author that does not exist. The authors array names a folder that is not under src/data/authors/. Check the spelling of the folder name, which is the id.
integrations: "x" and "y" both claim order N. Two directory entries want the same slot. The reading order is authored, so it has to be unique.
integrations: the featured row draws 3 cards but N entries set featured: true. Exactly FEATURED_COUNT entries may carry the flag.
integrations: no entry uses "…". A category label with nothing in it would build an empty filter page that the chip row links to. Add an entry or drop the label from CATEGORY_LABELS.
Two posts claim featured. pickFeatured throws rather than picking one, because picking silently would make the flag mean “whichever the loader read first”.
Logo "x" not found in src/assets/logos/. A logo stem with no SVG. pnpm test catches this earlier than the build does.
A planned route also has a page. plannedRoutes.test.ts failing means you built one of the seventeen redirected routes and left its entry in src/config/plannedRoutes.json.ts. Delete the entry.
pnpm test fails
“No checks found under src/”. The runner found zero *.test.ts files, which is a failure by design. Either you deleted them all, or you are running from the wrong directory.
A config check reports an href with no page. Read carefully: several checks print unbuilt routes without failing. If the run is green, that output is a report, not an error.
A check fails to import a config file. Almost always the import type convention. A config module must use import type { X } from "./types/…", not import { type X } — with only type bindings the second form leaves the whole statement in place after type stripping, so Node tries to resolve an extensionless specifier and fails.
Node version. The floor is 22.13.0. Older releases fail here even when the install appeared to succeed.
The page looks wrong
A utility class has no effect in dev. In a long-running dev server, classes used only in newly created files can be missing from the generated stylesheet. Restart pnpm dev before you go looking for a bug in your markup.
A renamed content entry keeps 404ing. Astro’s content layer caches entries, and moving or renaming a folder while the dev server runs can leave the old one in place and the new one missing. Restart; touching the file will not clear it.
An element is un-themed — right in light mode, wrong in dark, or the reverse. It is using a raw Tailwind colour instead of a token. Open /examples/ui in both themes; a missing token shows up there instantly.
The navbar pill or footer card looks inverted in dark mode. Something in it reached for a semantic token. Both are fixed-brand surfaces — stone-700 in both themes — so their surface and contents must use palette aliases (bg-base-700, text-base-400). bg-secondary flips to stone-300 in dark and turns the pill into a light slab under light text. Aliases do not flip; semantic tokens do.
A class override does not win. tailwind-merge replaces a conflicting utility only at equal specificity and through the same modifier. hover:bg-muted overridden by hover:bg-primary merges cleanly; reaching the same property through a different modifier does not, and then emit order decides. Match the modifier the primitive used.
The band colour looks brown in dark mode. You changed --band’s dark value to primary-950. The amber ramp’s darkest step is hue 45°, which is orange, and at that chroma it reads as burnt brown on stone. The shipped value is a color-mix off primary-300 for exactly this reason.
Motion does not behave
An animation does not play at all. Check siteSettings.useAnimations, then check whether your OS has reduced motion on. Both are honoured, independently.
A scroll-driven animation plays once on load instead of scrubbing. Native scroll timelines are Chromium and Safari. Elsewhere the animation-timeline declaration is dropped and the animation runs once, time-based. For entrances that is the documented degradation — content ends visible. For scroll-only shapes it is not, which is why parallax-*, ken-burns and fade-through are made inert by an @supports guard. If you add a scroll-only animation, add it to that list, or it will leave content offset or invisible in Firefox.
A reveal leaves content invisible under reduced motion. The global guard zeroes durations. An element armed at opacity: 0 with a zeroed duration is still at opacity: 0. Anything whose resting state is a transform or an opacity needs its own prefers-reduced-motion reset, which is what the reveal primitives carry.
An element is invisible but still clickable. This is the StaggerReveal trap. Its reveal units are the wrapper’s direct children; a child that is display:none when the batch plays never intersects, so it keeps its armed opacity: 0 — and a later resize un-hides an element that is transparent and still in the hit-testing tree. Put the responsive hidden / flex swap on an element inside a reveal unit, never on the unit itself.
A UI mock swims or looks sliced off. ImageReveal is for photographs. Its scale-settle makes a screenshot’s crisp 1px borders visibly swim, and it scales the frame wider than its own clip. Use a plain frame.
A scroll-driven animation freezes inside a frame. The frame is overflow-hidden, which makes it a scroll container, so view() tracks the frame instead of the page. Use overflow-clip.
Above-the-fold text flashes visible and then hides. The reveal primitives arm behind html.js, the pre-paint marker set by the inline script in BaseHead. If that script was moved into a bundled <script>, the marker arrives too late. It must stay inline.
Interaction breaks after navigating
A primitive works on first load and stops working after one link click. This is the view-transition re-init bug. ClientRouter replaces the DOM without reloading, so anything wired on load is wired to elements that no longer exist. Wire through onReady in src/components/ui/_client.ts, which runs on load and again on astro:after-swap.
The two exceptions are _dialog.ts and _popover.ts, which bind once at the document level with delegated listeners. Delegation survives a swap on its own; re-initialising those would only add duplicate handlers.
The theme resets after navigating. The pre-paint script re-runs on astro:after-swap for this reason. If you edited it, keep that listener.
Forms
Pressing submit does nothing. That is the shipped state. All six forms are inert, with their submit disabled and a note underneath naming the file that explains how to connect them. See Forms & Email.
A connected form posts and the page re-renders with the form still full. The receiving route is still prerendered. Add export const prerender = false to it, and add the route to the sitemap’s customPages.
A connected form returns a 405. A band drawn site-wide is posting to whatever page it sits on, and that page is prerendered. Pass the band’s at option naming the on-demand route that owns its action.
A connected form reports “unavailable”. Its environment variables are missing at request time. The server log names exactly which keys — the visitor-facing message deliberately does not, because naming environment variables on a public page tells a stranger how the form is wired and tells the visitor nothing they can act on.
Env vars are set on Cloudflare and still not seen. Use astro:env/server, not import.meta.env. On Workers, secrets live only in the runtime env, which import.meta.env never sees.
A form accepts obvious spam. The two gates stop drive-by bots, not a targeted attacker who reads the markup and waits two seconds — _ts is a forgeable hidden field. The stated upgrade path is Cloudflare Turnstile, at the cost of requiring JavaScript in every form.
Icons
An icon renders as nothing. The name is not in the registry. In markup that is an astro check error; in integration frontmatter it is a build error naming the entry file — the schema validates icon names against the live registry precisely so it cannot be a silent empty 24×24 box.
The icon renders, but it is the wrong picture. A near-name. arrow-rotate-left-01 draws an upload tray, and the three bar-group-* names all draw bar charts. Nothing but a browser pass can catch this — open the icons panel in /examples/ui.
An inlined logo is clipped or renders as an empty shape. Duplicate SVG ids. Figma names ids by position, so every export declares clip0_0_1 and the first one on the page wins. logoSvg namespaces them; make sure you are going through it rather than inlining raw source.
Deploy
Every internal link 404s, or every one redirects. A trailing-slash mismatch between trailingSlash: "always" and the host’s own rewriting. Configure the host to leave URLs alone.
The footer links 404 on the live site. Those routes should be redirecting. Check that plannedRoutes still lists them and that the host is serving Astro’s generated redirect pages — or, better, mirror the list in the host’s native redirect configuration.
The stylesheet is much larger than expected. The dev catalog is still in the tree. It builds no pages in production, but Tailwind scans its markup, so its demo classes ship in the sheet every page loads. Delete src/components/Sections/UiCatalog/ and src/pages/examples/ together.
Nothing on the page has the right colours after a rebrand. You edited @theme inline instead of @theme. Layer 3 is a bridge; inline is what makes tokens resolve to runtime variables rather than to literals, and editing it is how the theme stops flipping.