Deployment
Olsa builds to static HTML. Every one of its 28 pages prerenders at build time, astro.config.mjs carries no adapter, and dist/ contains nothing that needs a runtime. That makes deployment the easy part: any static host will serve it, and there is no server bill.
The absence of an adapter is a deliberate design decision rather than an omission. A theme that mounts an adapter has picked a host for you before you have opened it. This one does not, which is also why the contact form’s server half ships written and type-checked but unmounted — see Contact Form for the four steps that change that.
Before you launch
1. Set site in astro.config.mjs. It still reads https://example.com. This one value feeds the canonical link, og:url, og:image resolution, the sitemap, the RSS feed’s channel and item links, the Sitemap: line in robots.txt, every link in llms.txt, and the JSON-LD @ids that identify your Organization and WebSite nodes. Ship it wrong and seven artifacts point at the wrong domain at once.
There is no build-time guard on this. It is the first item on the list because nothing will stop you.
2. Fill in src/config/siteData.json.ts. The name, title and description are the theme’s; the author block, the contact block and sameAs are placeholders. sameAs ships as an empty array, and the Organization JSON-LD needs those profile URLs to be disambiguated from anyone else with your name. The contact block ships with an example.com inbox, a reserved-for-fiction 555 phone number and the mock’s Austin address — all three render on /contact/ and all three appear in structured data.
3. Replace public/og.jpg. It is a placeholder. BaseHead uses it as the fallback og:image for every page without its own, and currently also as the Organization logo. A real 1200×630 image fixes both.
4. Replace the favicons. public/favicon.svg and public/favicon.ico are Astro’s defaults.
5. Point the placeholder links somewhere real. navData.columns and navData.social ship with # hrefs for the pages that do not exist yet (Changelog, Careers, Help Center, and the four social accounts). The header entries are all real routes and need nothing. pricingData’s “Contact Sales” CTA is a # too, as is the sign-in page’s “Forgot password?” link.
6. Replace the sample content. Seven blog posts, six authors and nine integrations ship as editorial placeholders. They are real, schema-valid entries, so the site looks finished — which is exactly why it is easy to forget they are not yours.
7. Have the legal copy reviewed. src/config/legalData.json.ts is template text that says so in its own intro.
8. Swap demoVideoId. It points at Blender’s Big Buck Bunny. Set it to your own demo, or empty the string and the pill degrades to a placeholder with no dialog rendered at all.
Build and verify
pnpm lint && pnpm check && pnpm build && pnpm test
pnpm build writes dist/. Because the build is fully static, the output is directory-shaped: dist/index.html, dist/about/index.html, dist/blog/<slug>/index.html and so on, which is what trailingSlash: "always" agrees with.
Run pnpm preview to serve dist/ locally before you push. It is the only way to see the production build’s behaviour — the dev server runs everything on a server whether or not you have an adapter, so a route that accidentally depends on request-time behaviour will look fine in dev and fail in the build.
Two checks worth doing on the built output rather than in the browser:
grep -r "example.com" dist/ | head # should return nothing once `site` is set
cat dist/robots.txt dist/llms.txt # both should name your real domain
Deploying the static build
The build command is pnpm build and the publish directory is dist/. That is the whole configuration on any static host — Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3 plus a CDN, or a plain nginx root.
Two host settings matter because of trailingSlash: "always":
- Do not enable a “strip trailing slash” redirect. The canonical tags, the sitemap, the RSS links and the internal links all carry the slash. A host that redirects
/about/to/aboutputs a redirect in front of every canonical URL on the site. - Serve
404.htmlfor unmatched routes. Most static hosts do this automatically; some need it named explicitly.
Set your environment variables in the host’s dashboard rather than in the repository. .env is gitignored and is not deployed. For a purely static build there are no variables to set at all — the three that exist belong to the contact form and only take effect once its server half is mounted.
What changes when you connect the contact form
Connecting the form makes exactly one route server-rendered. The steps are in Contact Form; their deployment consequences are:
- You install an adapter for your host (
@astrojs/netlify,@astrojs/cloudflare,@astrojs/vercelor@astrojs/node) and mount it inastro.config.mjs. The build changes from static to hybrid. src/pages/contact.astrogets itsexport const prerender = false;uncommented. That route — and only that route — is then rendered per request. The other 27 pages still build to static HTML.- You set
RESEND_API_KEYandCONTACT_TO_EMAILin the host’s environment. OptionallyCONTACT_FROM_EMAIL, which otherwise defaults to Resend’s shared sandbox sender — that sender works immediately but can only deliver to the address that owns the Resend account, so mail to any otherCONTACT_TO_EMAILfails at the provider with everything else correct. security.checkOriginstarts doing real work. It is already set explicitly in the config, so it is correct the day the adapter arrives rather than a thing you remember afterwards. It 403s a form POST whoseOrigindoes not match the request URL.
If you deploy to a host with a Node runtime and you have not installed the matching adapter, the build fails loudly with ActionsWithoutServerOutputError rather than shipping a form that silently drops messages. That failure mode is intentional.
Images on a static build
sharp is a dev dependency and does the image optimization at build time, so nothing in dist/ depends on an image service. Sources live in src/assets/, get transformed and content-hashed into the build, and <Image> emits real intrinsic dimensions — which is what keeps cumulative layout shift at zero. If you move to a host with its own image CDN and swap the adapter accordingly, that path changes; the markup does not.
pnpm-workspace.yaml denies sharp’s post-install build script. That is fine — sharp ships prebuilt binaries — and it keeps installs free of native toolchain requirements on your CI runner.
After the first deploy
Submit https://yourdomain.com/sitemap-index.xml to Google Search Console and Bing Webmaster Tools. Confirm robots.txt resolves and names the right sitemap. Run the built pages through Google’s Rich Results Test to see the Organization, WebSite, BlogPosting and BreadcrumbList nodes the theme emits — see SEO & Structured Data for what to expect on each page type.