Skip to content
AstroCraft Docs
On this theme

Deployment

Develi is a fully static site with no adapter. pnpm build produces a directory of HTML, CSS, JS and images, and that is the whole deliverable. There is no server-rendered route, no serverless function, no runtime environment to configure. Anything that can serve files can host it.

That is a deliberate constraint rather than a limitation to work around, and it shapes two things worth knowing up front: the output tree is flat (dist/, not dist/client/), and every dynamic-looking endpoint — robots.txt, llms.txt, rss.xml, the sitemap — is a .ts endpoint that prerenders to a static file at build time.

Build

pnpm build

On the shipped content this produces 29 pages and around 116 optimized image variants in a few seconds. Preview the real output before shipping it:

pnpm preview

pnpm dev runs everything through a server, so a route that accidentally depends on request-time behavior works locally and fails in a static build. Run pnpm build early and often — it is the check that tells the truth.

SITE_URL is the one required variable

It feeds seven things: the canonical link, Open Graph tags, JSON-LD @ids, the sitemap, robots.txt, llms.txt and the RSS feed. One wrong value poisons all seven at once, and none of them looks broken in review.

Set it in your host’s environment variables — not in the repo. A fresh clone builds happily on the placeholder https://example.com so you can see the site before you own a domain. What it will not do is let that placeholder reach production:

const isProductionDeploy =
  process.env.CONTEXT === "production" ||        // Netlify
  process.env.VERCEL_ENV === "production" ||     // Vercel
  process.env.DEPLOY_ENV === "production";       // anything else

if (isProductionDeploy && site.includes("example.com")) {
  throw new Error("SITE_URL is unset or still the placeholder. …");
}

The theme is host-agnostic, so the guard reads each host’s own build signal rather than assuming one. Local builds and deploy previews set none of those three, so they build freely — which is exactly why this only ever bites at deploy time.

If you deploy anywhere other than Netlify or Vercel, set DEPLOY_ENV=production in your host’s build environment yourself. The guard is only as good as the signal it can see, and on an unrecognised host it silently does nothing. That includes Cloudflare Pages, GitHub Pages, Dokploy, a plain nginx container, and anything else.

Host configuration

Two settings need to match the theme rather than the default:

Trailing slashes. astro.config.mjs sets trailingSlash: "always", and with the default directory build format every route emits path/index.html — so every canonical URL, OG URL and sitemap entry carries a trailing slash. Configure your host to serve directory indexes and, if it offers a choice, to keep trailing slashes rather than stripping them. A host that redirects /about/ to /about puts every canonical URL one redirect away from the page it names.

The 404. dist/404.html is built. Most static hosts pick it up automatically; some need it named explicitly.

Nothing else is required. There is no build plugin, no adapter package, no runtime.

Before you deploy

Seven items. The first five are minutes of work; the last two are decisions.

1. Set SITE_URL to your production domain, in your host’s environment variables. See above.

2. Replace public/og.jpg with a real 1200×630 social image. The shipped file is a placeholder, and its dimensions are what BaseHead claims in og:image:width/height for every page that has no image of its own.

3. Fill in src/config/siteData.json.ts — name, title, description, the author block, and the public contact email and phone the footer renders. The shipped values are Your Name, [email protected] and +1 (000) 000-0000.

4. Replace the legal copy in src/config/legalData.json.ts. It is placeholder text that announces itself as placeholder text in its own intro paragraph. Have it reviewed by a qualified professional; it is not legal advice.

5. Replace the faviconspublic/favicon.svg and public/favicon.ico.

6. Resolve the eight nav rows that 404. src/config/navData.json.ts ships the design’s full information architecture so the header and footer look complete out of the box. /careers/, /process/ and the six /services/<slug>/ targets have no pages. Build them or delete the rows. Until you do, every page on the site links to eight dead URLs — a real SEO and UX cost, not a cosmetic one. Deleting a row is a one-line edit; note that the services array is also what the /services/ tab rail reads, so remove an entry there only if you genuinely do not offer that service.

Worth checking at the same time: navData.social ships six placeholder profile URLs (https://x.com/yourhandle and friends). Those are the source of the sameAs array in your Organization structured data, so a made-up URL here ships a made-up claim to search engines. Drop the rows you do not have rather than leaving them pointing at nothing.

7. Delete the dev catalogsrc/components/Sections/UiCatalog/ and src/pages/examples/, once you no longer need the showroom. It builds no pages in production (getStaticPaths returns [] when import.meta.env.PROD), but Tailwind still scans its markup, so its demo classes sit in the stylesheet every real page loads.

Measured on the shipped theme: removing both takes the shared BaseLayout stylesheet from 103,249 to 84,802 bytes (−17.9%) and drops the @keyframes count from 93 to 2. Eighty-eight of the ninety-three keyframes in the production bundle are referenced by no built page.

Keep it while you are still choosing primitives — the cost is CSS, not JS, and it is the fastest way to see all 46. Re-measure after your own sections land; the saving moves with them.

Do not try to fix this with @source not "…/UiCatalog". It was tried and reverted in a theme built from this codebase. @source rules are not build-mode conditional, so the same directive strips the demo rules in astro dev too and the catalog silently stops animating — removing the exact check the Motion chapter tells you to run. Deleting the directory is the remedy, and it needs no configuration.

Verify the output

After a build, confirm the artifacts in dist/ directly rather than trusting the console:

pnpm build
ls dist/robots.txt dist/llms.txt dist/rss.xml dist/sitemap-0.xml dist/404.html

Four things are worth eyeballing once:

  • dist/sitemap-0.xml lists only indexable, trailing-slash URLs. The filter in astro.config.mjs excludes /examples/ and /404/, because both set noindex in their markup and a sitemap entry would contradict that.
  • dist/robots.txt and dist/llms.txt carry absolute URLs on your real domain. If they say example.com, SITE_URL did not reach the build.
  • The JSON-LD in any page’s <head> is a @graph with matching @ids. Paste it into Google’s Rich Results Test.
  • dist/blog/<slug>/index.html carries og:type=article plus article:published_time, and a BlogPosting node in the graph.

The full pre-flight

pnpm lint && pnpm check && pnpm build && pnpm test

Run all four. pnpm build is the one that catches content-schema and config errors, pnpm check catches type errors across .astro files that ESLint does not, and pnpm test runs the eleven self-checks. See Commands & Testing.

A note on view transitions and inlined scripts

astro.config.mjs sets vite.build.assetsInlineLimit: 0. That is intentional and should stay: inlined short scripts break under <ClientRouter /> view transitions. If you raise it to reduce request count, expect interactive primitives to stop re-initialising after a navigation.

NEXT STEPContent Collections