Skip to content
AstroCraft Docs
On this theme

Deployment

8-BitQuest is static everywhere except one route. /contact/ sets export const prerender = false because it takes the Resend form POST and re-renders with the result, so the project mounts the @astrojs/node adapter in standalone mode. That single choice shapes the whole deployment: pnpm build produces a small Node server plus a folder of prerendered HTML, and you run the server rather than dropping files on a static CDN.

What the build produces

pnpm build

The output splits in two:

  • dist/client/ — every prerendered page and asset. This is almost the entire site.
  • dist/server/entry.mjs — the one on-demand route, /contact/, and the request handler that serves it.

Run it with:

pnpm start        # node ./dist/server/entry.mjs

The standalone server honours HOST and PORT, defaulting to :4321. pnpm preview also works for a quick local look at the built output.

This shape suits a container target — a Docker image behind Traefik, which is the setup the theme was built to deploy behind (Dokploy). You build the image, run node ./dist/server/entry.mjs, and put a reverse proxy in front. Nothing about the app assumes a specific host beyond “can run a Node process.”

The SITE_URL gate

One environment variable is load-bearing: SITE_URL, your production domain. It feeds the canonical link, Open Graph tags, JSON-LD, the sitemap, robots.txt, llms.txt and the RSS feed. Set it once and all of them resolve correctly; leave it wrong and all of them point at the wrong place.

To make that impossible to ship by accident, astro.config.mjs throws at build time if a production deploy is still on the https://example.com placeholder:

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

The gate reads each host’s own build signal, so it fires on a real production deploy and stays quiet for local builds and deploy previews — which is why it only ever bites at the moment it should. On Netlify or Vercel you set nothing extra; on any other host, set DEPLOY_ENV=production in the production build environment so the gate can see it there too. The placeholder is matched by exact hostname, and an unparseable SITE_URL is treated as still-placeholder, so a malformed value fails the gate rather than slipping through.

Changing hosts

The adapter is the only thing that knows how the site is served. Nothing else in the tree references it, so switching hosts is a two-line change in astro.config.mjs:

// import node from "@astrojs/node";
import vercel from "@astrojs/vercel";      // or @astrojs/netlify
// adapter: node({ mode: "standalone" }),
adapter: vercel(),

Install the new adapter, swap the import and the adapter: line, and the contact route runs as a serverless function on that host instead of your Node process. The one thing to remember is the SITE_URL gate: Netlify and Vercel are recognised automatically, any other host needs DEPLOY_ENV.

Going fully static

If you do not want a server at all, drop the contact form’s server dependency and the adapter goes with it. Remove export const prerender = false from src/pages/contact.astro (or remove the contact route entirely), then remove the adapter: line from astro.config.mjs. pnpm build now emits a plain static dist/ that any static host serves directly — no Node process, no server route. You lose server-side form delivery; wire the form to a third-party endpoint if you still want it.

Before you deploy

Seven things to do once, in roughly this order:

  1. SITE_URL — your production domain, in the host’s environment variables. A production deploy fails without it; local builds and previews are unaffected.
  2. Contact keysRESEND_API_KEY and CONTACT_TO_EMAIL, and, to send beyond your own inbox, a verified domain plus CONTACT_FROM_EMAIL. See Contact Form.
  3. public/og.jpg — replace the placeholder with a real 1200×630 social image.
  4. src/config/*siteData, portfolioData, and the legalData terms/privacy copy (the last is placeholder text, not legal advice — have it reviewed).
  5. Faviconspublic/favicon.svg and public/favicon.ico.
  6. Delete the dev catalogsrc/components/Sections/UiCatalog/ and src/pages/examples/, once you have finished picking primitives. It builds no pages in production, but Tailwind still scans its markup, so its demo classes sit in the stylesheet every page loads; removing it trims the shared CSS by roughly a quarter (measured 76.4 KB → 56.7 KB) and drops around 70 unused @keyframes.
  7. Remove the authoring notestasks/ holds the theme’s own build and handoff prompts, not your project’s work. Delete the folder. Keep wiki/ and .claude/ if you want the documented workflow they describe.

The final check

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

The build is the real check — content-schema and config mistakes surface there rather than in the browser. That same chain runs in CI on every push and pull request (.github/workflows/ci.yml), on the placeholder SITE_URL, so a green CI run means the code builds; it does not mean you have set your production domain. That is on you, at the host.

NEXT STEPContent Collections