Troubleshooting
Most of what goes wrong in this theme goes wrong loudly, at build time, with the offending value named. That is a design choice. This page collects the messages you will actually see, plus the handful of behaviours that look like bugs and are not.
Build errors that name their fix
No logo for integration "linear" — add src/assets/logos/linear.svg
An integration entry has no matching brand glyph. The convention is that the collection entry id doubles as the logo key, and @js/integrationUtils throws rather than falling back to a placeholder. Add the SVG.
A content collection error naming an entry and a field.
Zod rejected the frontmatter. The usual causes: a missing heroImage (it is required on blog posts, not optional), an authors reference to a slug with no file in src/data/authors/, a rating outside 0–5, or an empty requirements array. The message names both the entry and the field.
ActionsWithoutServerOutputError
You added src/actions/index.ts or uncommented prerender = false without installing an adapter. Both are steps in connecting the contact form, and they only work together — see Contact Form. This failure is the guardrail working; the alternative would be a form that silently drops messages.
A pnpm check error in pricingData.json.ts about a missing property.
The comparison matrix is keyed by a plan-name union derived from your own plans array. Adding or renaming a plan makes every comparison row fail to compile until you have given it a value for the new name. This is deliberate — it is the only mechanism that keeps the matrix and the plan cards in step.
Note that this one surfaces under pnpm check and not under pnpm build, because astro build does not type-check. If you only run the build, a broken matrix ships.
Looks like a bug, is not
Every page has a violet background.
--background is violet-200 in light mode and violet-950 in dark — the whole body carries a brand tint, extended from the design’s footer band. For a neutral page, set --background: var(--color-base-50) in global.css. See Colors & Theming.
Dark mode does not follow the operating system.
Light is the default and dark applies only when the visitor pins it with the ThemeToggle, which writes localStorage("colorTheme"). The device prefers-color-scheme is deliberately not followed. If you want it followed, that is an edit to the inline script in BaseHead — and it must stay inline, or you reintroduce a flash of the wrong theme on first paint.
A motion-reduce setting does not stop a scroll animation.
The global guard zeroes animation and transition durations. A scroll-driven animation is progressed by scroll position, not time, so zeroing its duration does nothing. Anything using timeline-* must also carry motion-reduce:animate-none. Every shipped reveal does.
A parallax or Ken Burns animation is frozen at its start.
Its clipping frame is using overflow-hidden. A hidden box is a scroll container, so view() resolves against the frame — which never scrolls — instead of the page scrollport. Use overflow-clip.
Scroll animations do nothing in Firefox.
Native scroll timelines are Chromium and Safari. Where they are unsupported, entrance animations run once, time-based, and end at identity — content ends visible, which is the documented degradation. The scroll-only shapes (parallax, Ken Burns, fade-through) are made deliberately inert by a @supports guard, because a played-once parallax would leave content permanently offset. If you add a scroll-only keyframe, add it to that guard’s list.
An animate-* class seems to do nothing.
Keyframes only emit when a utility is actually used, so the class was probably tree-shaken because nothing referenced it at build time. Confirm with pnpm build and grep dist/ for the @keyframes name.
Every section arrives already animated after a navigation.
Check that html[data-astro-transition] { scroll-behavior: auto; } is still in global.css. Without it, the view-transition router’s post-swap scroll restoration inherits smooth scrolling and glides from the old offset to zero, firing every reveal on the way past.
A state="error" prop appears to do nothing.
A consumer class outranks a variant. tv() merges the caller’s class last, so a border-* in a shared surface string silently beats the border-error the state would set. Check by rendering the error state and grepping the output for border-error, not by reading the class list. The fix is to keep the border colour out of the shared string and pick it per state in the same consumer string — which is what Sections/Contact/_surface.ts does.
/examples/ui 404s in production.
By design. Its getStaticPaths emits no paths when import.meta.env.PROD, so no HTML ships. astro dev still serves it.
A blog post is missing everywhere.
Check for draft: true. Drafts are filtered at the source in getPublishedPosts(), so the post is absent from the index, from getStaticPaths (no page is built), from related posts and from the RSS feed simultaneously.
Both sign-in and sign-up are missing from the sitemap.
Also by design. Four routes set noindex — /404/, /examples/ui/, /signin/ and /signup/ — and astro.config.mjs filters all four out so the sitemap cannot contradict the directive. If you add a noindexed route, add it to the filter in the same commit.
Related posts do not match the current post’s topic.
The sort is same-first-category first, then everything else, newest-first within each group, sliced to three. A post with no categories gets the newest three rather than an empty section.
The table of contents skips a heading.
It filters to depth === 2. Only ## headings appear.
Things that fail silently — check these first
Canonical URLs point at example.com.
site in astro.config.mjs is still the placeholder. Nothing guards it. It feeds canonical, og:url, the sitemap, RSS, robots.txt, llms.txt and the JSON-LD @ids, so one wrong value poisons seven artifacts. Verify after a build:
grep -r "example.com" dist/ | head
Mail sends without error but never arrives.
You are on the default [email protected] sender, which can only deliver to the address that owns the Resend account. Verify a domain in Resend and set CONTACT_FROM_EMAIL.
A form POST does nothing and re-renders empty.
The POST target is ?_action=contact, not the bare path. A POST to /contact/ runs no action. If you wired the form by hand rather than by flipping CONTACT_WIRED, this is the state that switch exists to prevent.
Organization structured data is thin.
siteData.sameAs ships as []. Without profile URLs, a search engine has no way to connect your Organization node to your accounts. The contact block and the Organization logo also ship as placeholders.
Footer links go nowhere.
Most navData.columns links and all four navData.social hrefs are # placeholders for pages that do not exist yet. The header entries are all real routes.
Dev server
A renamed content entry keeps 404ing.
Astro’s content layer caches entries; renaming a file while the server runs can leave the old id in place and the new one missing. Restart pnpm dev — touching the file will not clear it.
A new utility class has no effect. In a long-running dev server, classes used only in newly created files can be missing from the generated stylesheet. Restart before you go looking for a bug in your markup.
It works in dev and fails in the build.
astro dev runs a server whether or not an adapter is installed, so a route that depends on request-time behaviour works locally and fails statically. Run pnpm build early and often.
One genuinely strange one
A component’s props all became untyped and nothing complained.
A bare < anywhere in an .astro frontmatter — including inside a comment, including between digits — makes the Astro compiler stop typing Astro.props. It fails silently: the file still checks clean, but every prop degrades to untyped.
This happened once in this codebase. Navbar.astro carried (< lg) in a comment and was the only one of 125 components with unchecked props. The fix is to write “below lg” in prose. If a component’s props stop being checked for no apparent reason, search its frontmatter for a stray <.
Where to look when nothing above fits
The repository documents itself in three layers. CLAUDE.md at the root carries the house rules and the gotcha list. .claude/rules/*.md hold the detailed conventions for TypeScript, Tailwind, Astro, motion and SEO. And wiki/ is a maintained knowledge base with a page per subsystem — start at wiki/index.md.
Beyond that, the code comments are unusually load-bearing. Decisions in this theme are recorded where they were made: why a class is opt-in, why a switch is one boolean and not four, why a listener lives in a shared module. A ponytail: comment specifically marks a deliberate shortcut and names its ceiling and upgrade path. When something surprises you, the file usually says why.