# Deployment

## Deployment & Launch

Ecozen builds a static site into `dist/`, so it runs on any static host. It ships ready-made settings for **Netlify** (`netlify.toml`), **Vercel** (`vercel.json`, `vercel.sh`), and **Cloudflare Pages** (`wrangler.toml`), plus `public/_headers` and `public/.htaccess`. This holds for all three [editions](../features/editions/); the Tina and Sanity notes are [below](#tina-and-sanity-editions).

| Setting           | Value                   |
| ----------------- | ----------------------- |
| Build command     | `npm run build`         |
| Output directory  | `dist`                  |
| Node.js version   | 22.23.4 or later (`engines` in `package.json`) |
| Package manager   | npm (`package-lock.json`) |

### Before You Launch
Go through this list once. Each item links to the page that explains it.

- [ ] `[site] baseUrl` is your real address, not the shipped `https://example.com` placeholder, and `npm run build` shows no `[site]` warning ([SEO & AI Visibility](/ecozen/customization/seo-ai-visibility/))
- [ ] Site name, tagline, logos, favicon, social image, and colors are yours ([Quick Start](/ecozen/getting-started/quick-start/))
- [ ] `[settings.contactInfo]` has your phone, email, address, and hours, and `[seo.localBusiness]` your towns and opening hours
- [ ] The header and footer layouts are the ones you want (`[settings.header] layout` and `[settings.footer] layout`, and `headerLayout`, `headerAnnouncement`, and `footerLayout` in the homepage you keep), and the placeholders of the layout you use are replaced: `[settings.newsletter] action` for `newsletter`, the photo and `footer.cta.projects` for `cta`, and `[settings.footer.contact] postcodes` and `footer.contactPanel.badges` for `contact` ([Footer](/ecozen/customization/footer/))
- [ ] The quote form posts to your own account, and `hello@example.com` is replaced in the form messages and in `contactForm.fallbackTicketMessage` ([Forms](/ecozen/customization/forms/))
- [ ] The careers **Apply** button (`apply.button.url` in `src/content/careers/english/-index.md`) points to your email or hiring page
- [ ] The calculator assumptions and currency match your region ([Savings Calculator](/ecozen/customization/savings-calculator/))
- [ ] Packages and prices are yours, and the `note` states your real terms ([Pricing](/ecozen/customization/pricing/))
- [ ] The **Pages** demo menu is hidden ([Menus](/ecozen/customization/menus/))
- [ ] Demo pages you don't need are deleted, for example the three demo homepages you don't use (`/home-two/`, `/home-three/`, `/home-four/`) and `/components/` ([Homepages](/ecozen/customization/homepages/), [Pages](/ecozen/customization/pages/))
- [ ] Demo content is replaced: services, projects, testimonials and their summary rating, the hero and about rating lines, statistics, team, logo strip names, and blog posts. The sample job roles are deleted or replaced with real openings ([Collections](/ecozen/customization/collections/))
- [ ] The demo photos and team portraits are replaced with your own ([Icons & Images](/ecozen/customization/icons-images/))
- [ ] The privacy policy and terms are reviewed for your business, with every `[Company name]` placeholder filled in
- [ ] `footer.copyright` and `ai.siteSummary` in every file in `src/i18n/` describe your business
- [ ] Social links in `src/config/social.json` are yours
- [ ] Analytics scripts are in `[head]`, gated on consent if the cookie banner is on ([Cookies & Analytics](/ecozen/customization/cookies-analytics/))
- [ ] `npm run astro-check` and `npm run build` finish without errors, and you have looked at the section, about, contact, FAQ, pricing, and calculator pages you changed (`astro-check` doesn't check those files)

### Deploy
<Tabs>
  <TabItem label="Netlify">

    <Steps>

    1. Push your project to GitHub, GitLab, or Bitbucket.
    2. In Netlify, choose **Add new site → Import an existing project** and pick the repository.
    3. Netlify reads `netlify.toml`: build command `npm run build`, publish folder `dist`, Node.js 22. Click **Deploy**.

    </Steps>

    The build uses `baseUrl` from `config.toml` unless you add a `SITE_URL` environment variable in the Netlify site settings.

    `netlify.toml` also sets long-term caching for `/_astro/*` and `/images/*`, and sends missing `/fr/...` and `/ar/...` URLs to that language's 404 page. Add a block for each extra language.

    Using Netlify Forms? Set `contactFormProvider = "netlify"` and `contactFormAction = "/"`, enable form detection under **Forms**, and redeploy.

  </TabItem>
  <TabItem label="Vercel">

    <Steps>

    1. Push your project to GitHub, GitLab, or Bitbucket.
    2. In Vercel, choose **Add New → Project** and import the repository. Framework preset: **Astro**.
    3. Vercel reads `vercel.json`, which builds with `sh vercel.sh`. Click **Deploy**.

    </Steps>

    `vercel.sh` sets the site address (`SITE_URL`) for each build, and on Vercel it replaces `baseUrl`:

    - Production builds use the `SITE_URL` environment variable if you set one in the Vercel project settings. Otherwise they use your Vercel production domain. Set `SITE_URL` (for example `https://www.your-domain.com`) when you want one exact address, such as the `www` or the bare domain.
    - Preview builds use `https://staging.<your production domain>`.

    Edit `vercel.sh` if you want something else.

    `vercel.json` also enables trailing slashes, redirects `/sitemap.xml` to `/sitemap-index.xml`, and serves the `.md` and `llms` files as text.

  </TabItem>
  <TabItem label="Cloudflare Pages">

    **From git:** in the Cloudflare dashboard, open **Workers & Pages → Create → Pages → Connect to Git**, pick the repository, and set the build command to `npm run build` and the output directory to `dist` (the same folder as `pages_build_output_dir` in `wrangler.toml`).

    The included `wrangler.toml` sets `NODE_VERSION = "22.23.4"` under `[vars]`, the version Ecozen needs. Cloudflare treats this file as the source of truth for the settings in it and shows them read-only in the dashboard, so change them in the file. Set your address with `baseUrl` in `config.toml`.

    **From your computer:** Ecozen doesn't install Wrangler (Cloudflare's command-line tool) for you. Add it once, change `name` in `wrangler.toml` to your project name, then log in, build, and deploy:

    ```bash
    npm install -D wrangler
    npx wrangler login
    npm run build
    npm run deploy:cf
    ```

    `public/_headers` serves the `.md` and `llms` files as text on Cloudflare Pages and Netlify.

  </TabItem>
  <TabItem label="Other hosts">

    Run `npm run build` and upload the **contents** of `dist/` to your web server or static host (GitHub Pages, S3, a shared host, and so on).

    For Apache servers, `public/.htaccess` (copied into `dist/`) sets browser caching for CSS, JavaScript, images, and fonts, and serves the `.md` and `llms` files as text.

  </TabItem>
</Tabs>

<Aside type="note">
  When a host builds from your repository, commit `package-lock.json` and keep
  `dist/`, `.astro/`, and `node_modules/` out of git (the included `.gitignore`
  already does).
</Aside>

### Missing Pages In Each Language
Each language has its own 404 page. Only `netlify.toml` includes a rule that sends a missing `/fr/...` or `/ar/...` address to it. On other hosts, those addresses may show the default language's 404 page unless you add a rule.

### Tina And Sanity Editions
**Tina.** The site builds and works with no account and no variables. For online editing, add `PUBLIC_TINA_CLIENT_ID` and `TINA_TOKEN` to your host, and keep `tina/tina-lock.json` committed. `npm run setup:tina` walks you through it, and the README adds one launch step: decide who edits and where. See [Tina](../features/tina/).

**Sanity.** Give your host `PUBLIC_SANITY_PROJECT_ID`, `PUBLIC_SANITY_DATASET`, and `PUBLIC_SANITY_STUDIO_URL`. Without a project, the site builds from the bundled demo content. Most items in the list above are set in the Studio instead: **Site settings** for the address, contact details, forms, and analytics, and **Other pages** for the legal pages. A static site shows newly published content only after a rebuild. See [Sanity](../features/sanity/).

<Aside type="caution">
  Don't set `SANITY_API_READ_TOKEN` or `SANITY_VISUAL_EDITING` on your static
  host. `.env.example` explains every variable.
</Aside>

### After You Launch
<Steps>

1. Open the live site and click through the main pages, on a computer and on a phone, including the mobile menu.
2. Send a test request through the quote form, all three steps, and check that it arrives.
3. Move the calculator slider and compare one result with a real quote.
4. Submit `https://your-domain/sitemap-index.xml` in [Google Search Console](https://search.google.com/search-console) and [Bing Webmaster Tools](https://www.bing.com/webmasters).
5. Check a service page and a job page with Google's [Rich Results Test](https://search.google.com/test/rich-results).
6. Share a link in a chat app to check the preview image.

</Steps>

### Updating The Live Site
With a git-connected host, every push to your main branch deploys automatically. Build locally first (`npm run build`) to catch errors before they reach your visitors.