# Deployment

## Deployment & Launch

Margo 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`). The site needs no account or environment variables.

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

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

**Address and identity**

- [ ] `[site] baseUrl` is your real address, not the shipped `https://example.com` placeholder, and `npm run build` shows no `[site]` warning ([SEO & AI Visibility](/margo/customization/seo-ai-visibility/))
- [ ] `title`, `tagline`, `description`, and `logoText` in `[site]`, and `author` and `keywords` in `[seo]`, describe you or your studio. The tagline is added to every page title. French and Arabic have their own `site.tagline` and `site.description` in `src/i18n/fr.json` and `src/i18n/ar.json` ([Config Reference](/margo/customization/config-reference/))
- [ ] `[seo.identity]` says whether the site belongs to a `Person` or an `Organization`, with your name and, for a person, your job title
- [ ] The logo, favicons (`npm run generate-favicons`), social image (`npm run generate-og-image`, after the two items above), colors in `theme.css`, and fonts in `fonts.json` are yours ([Site Identity & Logo](/margo/customization/site-identity-logo/), [Colors & Fonts](/margo/customization/colors-fonts/))

**Contact and forms**

- [ ] `contactFormAction` and `contactFormProvider` post to your own account (Formsubmit, Formspree, or Netlify Forms), and the build prints no `[forms]` warning ([Forms](/margo/customization/forms/))
- [ ] The Mailchimp `formAction` under `[settings.subscription]` is your list, or `enable = false` there if you don't send a newsletter
- [ ] `email` and `booking` under `[settings.contactInfo]` are yours. `booking` is where every **Book a call** button goes ([Contact & Booking](/margo/customization/contact-booking/))
- [ ] The contact rows in `src/content/sections/<language>/contact-section.md`, and the email and phone in `footer.md` and `call-to-action.md`, are yours ([Footer](/margo/customization/footer/))
- [ ] A project-wide search for `example.com` and `555` finds nothing you still need to change

**Sample content**

- [ ] The sample projects and photo series in `src/content/projects/` are replaced with your own work, and the hero `cards` on the homepage point to them ([Projects](/margo/customization/projects/))
- [ ] The services, journal posts and authors, team members, testimonials, and client logos are yours or removed ([Collections](/margo/customization/collections/))
- [ ] Every price and question on the pricing and FAQ pages is yours ([Pricing & FAQ](/margo/customization/pricing-faq/))
- [ ] Every sample photo is replaced ([Icons & Images](/margo/customization/icons-images/))

**Pages and menus**

- [ ] The privacy policy, terms, and cookie policy in every language folder of `src/content/pages/` (`privacy-policy.mdx`, `terms-conditions.mdx`, `cookie-policy.mdx`) are rewritten for your practice and reviewed. They ship as sample text with a notice at the top
- [ ] The **Pages** demo menu is hidden (`"enable": false` in every menu file), and the menus point at your pages ([Menus](/margo/customization/menus/))
- [ ] The demo homepages you don't use are deleted ([Homepages](/margo/customization/homepages/)), and `/elements/` and `/components/` are removed if you don't want the layout and shortcode galleries online ([Pages](/margo/customization/pages/))
- [ ] `[settings.layoutSwitcher] enable = false`, and the `PUBLIC_DEMO_MODE` environment variable is not set on your host
- [ ] `footer.copyright` in `src/i18n/<language>.json` reads the way you want, and the links in `src/config/social.json` are yours

**Analytics and checks**

- [ ] Analytics tags are in `[head]`, marked with `data-consent` while the cookie banner is on ([Cookies & Analytics](/margo/customization/cookies-analytics/))
- [ ] `npm run astro-check` and `npm run build` finish without errors, and you have looked at every page whose section files you changed (`astro-check` doesn't check section files or the contact, FAQ, and pricing page files)
- [ ] The Arabic text has been reviewed by a native speaker, if you keep Arabic
- [ ] You checked the site at phone width, in dark mode, and in every language you keep

### 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. When you add a language, add its own block, as the comment in the file says.

    Using Netlify Forms? Set `contactFormProvider = "netlify"` and `contactFormAction = "/"`, enable form detection under **Forms** in the Netlify dashboard, 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 turns on trailing slashes, redirects `/sitemap.xml` to `/sitemap-index.xml`, and serves the `.md` copies (marked `noindex`) and the `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.13.1"` under `[vars]`, which is older than the 22.23.4 that Margo needs. Change it to `"22.23.4"` or a later version of Node 22 before your first deploy. 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:** Margo doesn't install Wrangler (Cloudflare's command-line tool) for you. Add it once, change `name` in `wrangler.toml` (shipped as `margo-astro`) 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
    ```

  </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). The included `public/.htaccess` covers Apache.

    Ask your host how to serve `.md` files as `text/markdown` and `llms.txt` as `text/plain`, if you keep `[seo.ai] markdownPages` and `llmsTxt` on. Without that, some browsers download those files instead of showing them.

  </TabItem>
</Tabs>

<Aside type="note" title="Keep public/_headers in your repository">
  `public/_headers` tells Netlify and Cloudflare Pages to serve the `llms.txt`
  files and the `.md` page copies as text. Commit it with the rest of the
  theme so a git-connected host sees it.
</Aside>

<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>

### 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, a project page, and dark mode.
2. Send a test message through the contact form, and sign up to the newsletter if you use it, and check that both arrive.
3. Open the search with <kbd>⌘</kbd> + <kbd>K</kbd> (<kbd>Ctrl</kbd> + <kbd>K</kbd> on Windows) and search for one of your projects.
4. Click a **Book a call** button and check that it opens your scheduling page.
5. 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).
6. Check the homepage and a project page with Google's [Rich Results Test](https://search.google.com/test/rich-results).
7. Share a link in a chat app to check the preview image.

</Steps>

#### Tell Search Engines About Changes With IndexNow
IndexNow tells Bing, Yandex, Seznam, and Naver which pages changed, so they visit them sooner. Bing also feeds ChatGPT search and Copilot. It is off until you set a key:

<Steps>

1. In `config.toml`, set `indexNowKey` under `[seo.ai]` to a key of 8 to 128 letters, numbers, or dashes that you make up:

   ```toml
   [seo.ai]
     indexNowKey = "a1b2c3d4-your-own-key"
   ```

2. Build and deploy. The build publishes the key file at `https://your-domain/<key>.txt`, which the search engines check.

3. After each deploy is live, run from your computer:

   ```bash
   npm run build                # indexnow reads the sitemap in dist/
   npm run indexnow             # pages changed in the last 7 days
   npm run indexnow -- --all    # every page: use it after your first launch
   ```

   `npm run indexnow -- --days=30` sends the pages changed in the last 30 days, and `npm run indexnow -- --dry-run` lists the pages without sending them.

</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.