Deployment
Medice is a static site with one exception, and the exception shapes the whole deployment story. Every page prerenders. One endpoint — POST /api/book/ — does not, because it needs a secret to send mail, and a secret needs a server.
That single prerender = false in src/pages/api/book.ts is the only reason the project carries an adapter.
The build produces two trees
SITE_URL=https://your-domain.example pnpm build
Because there is an adapter, the output splits:
dist/client/— the whole prerendered site. All 52 pages, the assets,robots.txt,llms.txt,rss.xmland the sitemap.dist/server/— the one endpoint.
Nothing about canonical URLs, OG tags, the sitemap or the trailing-slash shape changes because of the split. What changes is that a static host receives dist/client, not dist — and that any script walking the build walks dist/client/.
Set SITE_URL first
site in astro.config.mjs reads SITE_URL, and it feeds canonical, OG, JSON-LD, the sitemap, robots.txt and llms.txt together. Getting it wrong poisons six things at once, none of them visibly broken in a review.
It defaults to https://example.com so a fresh clone builds without a domain. A production deploy throws rather than ship that placeholder. The gate reads each host’s own build signal:
process.env.CONTEXT === "production" // Netlify
process.env.VERCEL_ENV === "production" // Vercel
process.env.DEPLOY_ENV === "production" // anything else — set it yourself
None of those is set by a local pnpm build or by a deploy preview, so previews and local work build freely. On a host not in that list the gate can see nothing, so set DEPLOY_ENV=production in your production build environment — and nowhere else, or your preview deploys will start failing on the placeholder too.
Cloudflare Workers
This copy ships configured for Cloudflare: the adapter is @astrojs/cloudflare and wrangler.jsonc at the repository root configures the Worker. The adapter reads that file at build time and writes the real config to dist/server/wrangler.json with the emitted paths filled in, which is why the deploy command names the generated one rather than the one you edit:
SITE_URL=https://your-domain.example pnpm build
npx wrangler deploy -c dist/server/wrangler.json
The prerendered site is served straight off Cloudflare’s asset store, so the Worker only wakes for /api/book/.
One line in astro.config.mjs exists specifically for this adapter and is worth not deleting by accident:
session: false,
Left unset, the Cloudflare adapter wires Astro’s session store to a KV binding and asks wrangler to provision the namespace at deploy time — a piece of stateful infrastructure, and an interactive prompt in the middle of an otherwise unattended deploy, for a feature nothing here uses. The booking endpoint reads a form and sends an email; it stores nothing between requests. Delete the line the day something genuinely needs state, and the KV namespace comes back with it.
Swapping the adapter
The adapter is the one line a buyer swaps, and swapping it is genuinely one line. The endpoint is a standard APIRoute over Web Request/Response using fetch, which is what every one of these runtimes runs natively — the endpoint itself does not change a character.
pnpm astro add netlify # or vercel, or node
@astrojs/node is the host-agnostic choice, and it is the one to reach for if you have not picked a host yet: pnpm preview starts a real server, so the booking form can be tested end to end before anything is deployed.
Netlify and Vercel both read their own production signal, so the SITE_URL gate works without a DEPLOY_ENV. Set SITE_URL and the three booking secrets in the site’s environment variables.
Deploying to a purely static host
If you do not want a running endpoint at all — GitHub Pages, S3, a plain nginx — you have two options.
The quick one is to keep the build as it is and upload only dist/client. The site is complete; the booking form’s native submit will 404 because nothing serves /api/book/.
The clean one is to remove the server half entirely: delete the adapter from astro.config.mjs and delete src/pages/api/book.ts together, then give the booking form your own endpoint — a Formspree URL, a Netlify Form, your own service — by changing the form’s action in src/components/Sections/Book/. With no on-demand route the output stops splitting and dist/ is the site again.
Deleting one without the other is the mistake to avoid: an adapter with nothing to serve carries the split for no reason, and an endpoint with no adapter fails the build.
Before you launch
SITE_URLin your host’s environment. The gate above will not let you forget on a supported host; on any other, setDEPLOY_ENV=productionso it can see you.src/config/siteData.json.ts— the brand name and description, the contact block,twitterCreator,sameAs. A check fails the build if the description still describes a starter template.src/config/legalData.json.ts— the terms and privacy copy are placeholders. Have them reviewed.public/og.jpg— replace the placeholder with a real 1200×630 social image.- Favicons —
public/favicon.svgandpublic/favicon.ico. - The booking secrets —
RESEND_API_KEY,BOOKING_FROM,BOOKING_TO.BOOKING_FROMmust be an address on a domain verified in Resend; unverified senders are rejected. Without all three the endpoint answers 500, logs which variable is missing, and shows the visitor the failure notice — it never pretends to have sent something. - Replace the photography. The specialist portraits are worse than placeholder: there are six photographs for twenty-seven slots, so a face recurs under different names across departments.
- Delete the dev catalog —
src/components/Sections/UiCatalog/andsrc/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 takes the shared CSS from 76,413 to 56,695 bytes — a 25.8% cut — and drops 70 unused@keyframes. Keep it while you are still choosing; the cost is CSS, not JS. - The thirteen planned routes. The header and footer draw a complete clinic information architecture, and thirteen of those destinations are ones only you can write — your locations, your insurers, your portal. They are declared as
PLANNED_ROUTESinsrc/config/navData.json.ts, andpnpm testasserts the dead links are exactly that list. Build them, or remove their links; either way the check keeps you honest, and nothing else on the site 404s. See Routing.
The pre-deploy chain
pnpm lint && pnpm check && pnpm build && pnpm test && pnpm wiki:lint
pnpm build is the real check — content-schema and config mistakes surface there rather than at runtime. If you have kept the repository’s wiki/, pnpm wiki:lint resolves every path:line citation in it and verifies the cited line still contains the symbol the prose names, so the documentation cannot drift out from under the code unnoticed. If you deleted the wiki, drop that command from the chain.