Skip to content
AstroCraft Docs
On this theme

Troubleshooting

Most of what follows is documented in the source at the point it can go wrong. These are the failures worth knowing before you hit them, ordered roughly by how hard they are to diagnose from the symptom.

A script works on the first page and then stops

The hardest one to see, because everything works until you click a link. Under <ClientRouter /> a navigation swaps the DOM without reloading the page, so a script that bound its listeners once on load has bound them to elements that no longer exist.

Two patterns in this codebase are safe. Either delegate to the document and bind once — what _dialog.ts and _popover.ts do — or write an idempotent init() and re-run it:

init();
document.addEventListener("astro:page-load", init);

filter.ts and saved.ts both do the second, each marking its root with a data-*ready flag so a re-run is cheap. For a primitive, use onReady from _client.ts. A bare DOMContentLoaded listener is the bug.

A class you built at runtime has no styles

Tailwind scans source files, so a class name assembled from fragments at runtime was never compiled. This bites exactly where you would expect — a script that builds rows or chips:

// broken: nothing generated `bg-primary-900`
el.className = `bg-primary-${shade}`;

filter.ts and saved.ts both write their classes out as literal strings for this reason, which is why those files look repetitive. If you need a conditional, write both full class strings.

Ticking a filter empties the whole list

A rail option whose value no record carries. The rail is authored copy and the records are data; nothing in the type system joins them. Run pnpm test — carsData.test.ts and hubsData.test.ts assert every rail option matches at least one record in that hub’s dataset, and they name the offender. If you added the option deliberately, add a record that satisfies it.

The build refuses with “SITE_URL is unset or still the placeholder”

Working as designed. site feeds canonical, OG, JSON-LD, the sitemap, robots.txt and llms.txt, and all six fail invisibly — a canonical pointing at example.com renders perfectly. Set SITE_URL in the host’s build environment. Local builds and deploy previews are unaffected; only a build your host marks as production is gated. On a host that is not Netlify or Vercel, set DEPLOY_ENV=production yourself, as .env.example describes.

pnpm check reports two errors in rss.xml.ts

src/pages/rss.xml.ts:7:19 - error ts(2304): Cannot find name 'APIRoute'.
src/pages/rss.xml.ts:7:39 - error ts(7031): Binding element 'site' implicitly has an 'any' type.

A missing import, and the two errors are one bug — without the type, the destructured parameter has nothing to infer from. Add the line the other two endpoints have:

import type { APIRoute } from "astro";

The build and the feed are unaffected, which is exactly why a type pass belongs in the chain: a type-only error cannot break a render, so nothing else will ever tell you.

“No checks found under src/”

pnpm test fails when discovery finds nothing, on purpose — a discovery-based runner that quietly stops finding checks is worse than no runner. Either you renamed the last check file to something that is not *.test.ts, or you are running it from the wrong directory.

BaseLayout is chrome-free: the header and footer are named slots, and a route that does not fill them renders without navigation. Fourteen pages currently do — the twelve information pages plus Terms and Privacy, all of which compose only their article section. The 404 does it deliberately. Routing has the three-line fix.

A collection entry fails the build

Zod names the entry and the field. The two that catch people out: heroImage is required on every post, because a post without an OG image is a post nobody shares; and authors is a reference("authors"), so a byline slug that does not match a folder under src/data/authors/ is an error rather than an empty author box.

blogData.featured.slug throws its own error naming the missing slug — the index asserts the featured post exists rather than rendering a hole.

An element is unthemed

Almost always a raw Tailwind colour where a token belongs: bg-sky-700 instead of bg-primary, text-zinc-400 instead of text-muted-foreground. Raw colours work, look close, and survive a rebrand unchanged, which is the bug. Open /examples/ui after a change — an unthemed element stands out there faster than in a diff. Colors & Theming has the token layers.

Dark mode does nothing

There is no dark mode. No .dark block, no theme script, no toggle primitive — the token system is light-only and html carries scheme-light. Colors & Theming has the shape to add it back, which is a second :root block over the semantic layer only.

One page ships half a megabyte of JavaScript

You imported the icon registry into a client script. icons.ts is 465 KB and is meant to stay build-time — importing ICONS from a <script> ships all 578 glyphs. This already happened once, on the saved dashboard; the fix in place is a four-glyph map inside that module. Icons names the upgrade path if a page ever needs the whole set.

A short script stops working after a navigation

Check that vite.build.assetsInlineLimit is still 0. Inlined scripts break under view transitions; the zero is not a size preference and the comment in astro.config.mjs says so.

/examples/ui 404s in production

Intended. getStaticPaths returns [] under import.meta.env.PROD, so no HTML ships. Use astro dev to browse the catalog.

Scroll-driven motion looks wrong in Firefox

Firefox drops animation-timeline, so a scroll-driven animation plays once, time-based. For an entrance that is harmless — it ends visible. For the scroll-only shapes it is not, which is why @supports not (animation-timeline: view()) makes parallax-up, parallax-down, ken-burns and fade-through inert. If you add a scroll-only animation, add it to that list, and give any timeline-* element motion-reduce:animate-none — the global reduced-motion guard zeroes time, which cannot stop a scroll-progressed animation.

The theme’s own numbers do not match

Trust the code. A few figures in Indexa’s own prose have fallen behind the source as pages were added, and these docs use recomputed values:

Its README says Actually
45 UI primitives 37 folders, 84 components
47 car records 69, across three datasets
Deleting the catalog: 76,413 → 56,695 bytes, 70 keyframes 113,715 → 95,172 bytes, 69 keyframes
@images/* path alias Not in tsconfig.json; @assets/* is the one for images

Three source comments have drifted the same way: carsData.json.ts still describes a 30-record dataset, used-cars.astro a 34-record one, and carImages.ts says eight photos cover thirty records (thirteen cover sixty-nine). AGENTS.md still introduces the project as the astro-boiler skeleton it was forked from, and package.json still carries that name. None of it affects a build; all of it is worth knowing before you quote a figure back to yourself.