# Deploying Your Site

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Ship It
Source: https://www.devtutorials.ai/learn/astro/ship-it/deploying-your-site

> astro build and preview, static vs server hosting and adapters, the site and base settings, SEO head tags, robots.txt, sitemaps and the 404 page, production environment variables, deploying from Git to Netlify, Vercel, Cloudflare and GitHub Pages, and the capstone checklist.

## Deploying Your Site

- **Deploying** means putting your site on the internet, so anyone can open it.
- Astro builds your project into files a host can serve.
- A static site needs only a **static host**. A site with on-demand pages needs a **server** and an **adapter**.
- In this lesson, you will build, preview, prepare the site for search engines, and publish it. Then you will finish Asha's journal as a capstone.

**Simple lesson:** build the site, check it, then upload it to a host.

### A Real-World Example

- Asha's journal works on her laptop at `localhost:4321`.
- She wants a real address, such as `https://asha.dev`.
- She sets `site` in the config, adds good `<head>` tags, runs `npm run build`, and connects her GitHub repo to a host.
- Every push to GitHub now rebuilds and republishes the site.

Press **▶ Run**. This layout has the head tags a real site needs. Open the **HTML** tab and find the canonical link and the Open Graph tags.

    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>{title} | Asha's Dev Journal</title>
      <meta name="description" content={description} />
      <link rel="canonical" href={canonical} />
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:url" content={canonical} />
      <link rel="icon" href="/favicon.svg" />
    </head>
    <body>
      <slot />
    </body>
</html>
`,
    "src/pages/index.astro": `---
---

    <h1>Asha's Dev Journal</h1>
    <a href="/about">About</a>
`,
    "src/pages/about.astro": `---
---

    <h1>About Asha</h1>
    <a href="/">Home</a>
`,
  }}
/>

What happened:

- `site` told Astro the real address of the site.
- `Astro.site` and `Astro.url` built a full **canonical** address for each page.
- Search engines and social apps read these tags to show the right title, text and link.

### What You Will Learn

In this lesson, you will learn:

1. How `astro build` and `astro preview` work.
2. Static hosting vs server hosting with adapters.
3. The `site` and `base` settings.
4. SEO basics: head tags, `robots.txt`, a sitemap and a 404 page.
5. Environment variables in production.
6. How to deploy to common hosts, and from Git.
7. A checklist to ship the capstone project.

## Build and Preview

### npm run build

```bash
npm run build
```

- `astro build` turns `src/` into a ready site in the **`dist/`** folder.
- Static pages become `.html` files. CSS and JavaScript are bundled and minified.
- Images from `astro:assets` are optimised.
- On-demand pages become server code, placed by the adapter.

```text
dist/
├── index.html
├── about/index.html
├── _astro/            (bundled CSS, JavaScript and images)
├── favicon.svg        (copied from public/)
└── robots.txt
```

### npm run preview

- `npm run preview` serves `dist/` on your computer, like a real host would.
- Always preview before deploying. Some bugs appear only in the built site, such as a wrong image path.
- Preview works for static sites and for adapters that support it, such as `@astrojs/node`.

Run `npm run build` on your computer before every deploy. If it fails locally, it will fail on the host too, and the error is easier to read here.

**Simple lesson:** `build` makes the `dist/` folder. `preview` lets you check it before it goes live.

## Static or Server?

### Choosing the Kind of Host

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-dep" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-dep">Animation: a push to GitHub starts a build on the host. The build output goes to a CDN for static files, and to a server function for on-demand pages.</title>
  <rect x="20" y="80" width="120" height="60" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} />
  <text x="80" y="106" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>git push</text>
  <text x="80" y="124" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 10 }}>GitHub</text>
  <rect x="220" y="80" width="140" height="60" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="290" y="106" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Host builds</text>
  <text x="290" y="124" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 10 }}>npm run build</text>
  <rect x="450" y="30" width="170" height="60" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="535" y="56" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>CDN</text>
  <text x="535" y="74" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 10 }}>static pages, images</text>
  <rect x="450" y="130" width="170" height="60" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="535" y="156" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Server function</text>
  <text x="535" y="174" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 10 }}>on-demand pages, islands</text>
  <path d="M140 110 H216" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <path d="M360 100 L446 64" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <path d="M360 120 L446 156" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <circle r="6" style={{ fill: "var(--ngc-blue)" }}><animateMotion path="M140 110 H216 M360 100 L446 64" dur="3s" repeatCount="indefinite" /></circle>
  <circle r="6" style={{ fill: "var(--ngc-orange)" }}><animateMotion path="M360 120 L446 156" dur="3s" begin="1.5s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>Static files go to a CDN. On-demand code runs as a server function.</figcaption>
</figure>

<div className="tscroll">

| Your site has | You need | Example hosts |
|---|---|---|
| Only static pages | Any static host, no adapter | GitHub Pages, Netlify, Vercel, Cloudflare Pages |
| Some on-demand pages, sessions, actions or server islands | A host with server functions + its adapter | Netlify, Vercel, Cloudflare |
| Your own server or VPS | `@astrojs/node` | Any machine that runs Node.js |

</div>

### Adding an Adapter

- `npx astro add netlify` (or `vercel`, `cloudflare`, `node`) installs the adapter and edits the config.
- The adapter turns your on-demand pages into code that runs on that host.
- Change hosts by changing **one adapter**. Your pages stay the same.

Press **▶ Run**. The config uses the Netlify adapter. The home page is static, the stock page runs on demand.

<a href="/stock">Shop stock</a>
`,
    "src/pages/stock.astro": `---
// On demand: runs as a server function on every request.
const items = await (await fetch("https://api.devjournal.dev/stock")).json();
---

<h1>Stock right now</h1>
<ul>{items.map((i: { name: string; left: number }) => <li>{i.name}: {i.left}</li>)}</ul>
<a href="/">Home</a>
`,
  }}
/>

- **[JS]** Leave out `: { name: string; left: number }`.

**Simple lesson:** a static site needs no adapter. On-demand features need the adapter for your host.

## site and base

### site

- `site` is the full address of your live site: `site: "https://asha.dev"`.
- Astro needs it for canonical links, sitemaps, RSS feeds and `Astro.site`.

### base

- Some hosts put your site in a **sub-folder**, for example GitHub Pages: `https://asha.github.io/journal/`.
- Then set `base: "/journal"`.
- Every link and asset path must start with the base. Use `import.meta.env.BASE_URL` to build them.

Press **▶ Run**. The links include `/journal` because they use `BASE_URL`.

<a id="about" href={base + "/about"}>About</a>
`,
    "src/pages/about.astro": `---
const base = import.meta.env.BASE_URL.replace(/\\/$/, "");
---

<h1>About</h1>
<a href={base + "/"}>Home</a>
`,
  }}
/>

A link written as `href="/about"` ignores `base`. On GitHub Pages it points to `https://asha.github.io/about`, which is a 404.

**Simple lesson:** `site` is your full address. `base` is the sub-folder, if any, and every link must include it.

## Getting Found: SEO Basics

### Head Tags

- `<title>`: unique per page, under about 60 characters.
- `<meta name="description">`: one or two sentences, unique per page.
- `<link rel="canonical">`: the one true address of the page.
- Open Graph tags (`og:title`, `og:image`...): the preview card on WhatsApp, LinkedIn and others.
- `<html lang="en">`: the language of the page.

### robots.txt, Sitemap and 404

- `public/robots.txt` tells search engines what they may crawl, and where the sitemap is.
- A **sitemap** lists every page. `npx astro add sitemap` adds `@astrojs/sitemap`, which writes `sitemap-index.xml` at build time (it needs `site`).
- `src/pages/404.astro` becomes the "page not found" page. Most hosts use it automatically.

Press **▶ Run**. Open **/robots.txt**, **/sitemap.xml** (a hand-made sitemap endpoint, to see what one contains), and a page that does not exist.

<p>That page does not exist. <a href="/">Go home</a>.</p>
`,
    "src/pages/index.astro": `---
---

<h1>Asha's Dev Journal</h1>
<ul>
    <li><a href="/robots.txt">robots.txt</a></li>
    <li><a href="/sitemap.xml">sitemap.xml</a></li>
    <li><a href="/missing">A missing page</a></li>
</ul>
`,
  }}
/>

- **[JS]** In `sitemap.xml.js`, leave out `import type` and `: APIRoute`.

**Simple lesson:** good head tags, `robots.txt`, a sitemap and a 404 page help people and search engines find your site.

## Secrets in Production

- Your `.env` file stays on your computer. It is **not** uploaded (keep it in `.gitignore`).
- On the host, add the same variables in its **dashboard** (Environment variables).
- Variables that start with `PUBLIC_` end up in browser code. Never put secrets in them.
- With `astro:env` (Chapter 3), a missing required variable stops the build with a clear message.

Press **▶ Run**. The build works because `.env` has the key. Now delete the `CONTACT_API_KEY` line from `.env` and run again: the build stops before anything broken goes live. On the host, this is what happens when you forget to add the variable in its dashboard.

<p>Contact form: {ready ? "ready" : "not configured"}</p>
`,
  }}
/>

If you ever commit a secret key to Git, treat it as leaked: create a new key at the provider and delete the old one.

**Simple lesson:** set production variables in the host's dashboard, never in Git.

## Deploying Step by Step

### From Git (Recommended)

1. Push your project to GitHub (or GitLab).
2. In the host's dashboard, choose "Import project" and pick the repo.
3. The host detects Astro. Check: build command `npm run build`, output folder `dist`.
4. Add environment variables.
5. Deploy. Every later push redeploys automatically. Pull requests get **preview** links.

### Host Quick Notes

<div className="tscroll">

| Host | Static | On-demand | Notes |
|---|---|---|---|
| Netlify | Yes | `@astrojs/netlify` | Preview deploys for every pull request |
| Vercel | Yes | `@astrojs/vercel` | Detects Astro automatically |
| Cloudflare | Yes | `@astrojs/cloudflare` | Runs on Workers, close to visitors |
| GitHub Pages | Yes | No | Use the official `withastro/action` GitHub Action; set `site` and often `base` |
| Your own server | Yes | `@astrojs/node` | Run `node ./dist/server/entry.mjs` |

</div>

### Before Every Deploy

- `npx astro check` passes.
- Tests pass (`npx vitest run`).
- `npm run build` works.
- `npm run preview` looks right.

**Simple lesson:** connect the Git repo once. After that, every push checks, builds and publishes.

## Capstone: Ship Asha's Journal

Put the whole course together. Build Asha's journal on your computer and deploy it:

- **Pages and layout**: a base layout with good head tags, `site` set (Chapters 2 and 7).
- **Content**: posts in a content collection, a post page per entry, tag pages, an RSS feed (Chapter 3).
- **Images**: an optimised cover image per post with `<Image />` (Chapter 3).
- **Rendering**: static pages by default, on-demand only where needed (Chapter 4).
- **Interactivity**: a like button island with `client:visible`, `<ClientRouter />` in the layout (Chapter 5).
- **Server**: a contact form with an action, a login with sessions and protected `/admin` pages (Chapter 6).
- **Fresh data**: a stock box as a server island (Chapter 7).
- **Quality**: `astro check`, a few Vitest tests, one Playwright test for the contact form (Chapter 7).
- **Deploy**: connect the repo to Netlify, Vercel or Cloudflare with its adapter.

Ship a small version first: home, one post, and the layout. Then add one feature per deploy. A small site that is live beats a big site that is "almost done".

**Simple lesson:** ship a small version first, then add one feature at a time.

## TL;DR

1. **Build** = `npm run build` → `dist/`. **Preview** = `npm run preview`.
2. **Static only** = any static host, no adapter.
3. **On-demand features** = the host's adapter: `npx astro add netlify` (or vercel, cloudflare, node).
4. **site** = full live address. **base** = sub-folder; use `import.meta.env.BASE_URL` in links.
5. **SEO** = title, description, canonical, Open Graph, `lang`.
6. **Files** = `public/robots.txt`, sitemap (`@astrojs/sitemap`), `src/pages/404.astro`.
7. **Secrets** = host dashboard, never Git. No secrets in `PUBLIC_` variables.
8. **Deploy from Git** = every push builds and publishes; pull requests get previews.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No `site` in the config | Astro knows the address | Set `site` | Wrong canonical links, no sitemap |
| Links without `base` on GitHub Pages | `/about` works everywhere | Add the base | 404s everywhere |
| On-demand pages on a static host | Any host runs any site | They need a server and an adapter | Pages missing or broken |
| Committing `.env` | The host needs the file | Use the host's dashboard | Leaked secrets |
| Skipping `npm run preview` | Dev and build are the same | Some bugs appear only in the build | Broken live site |
| The same title on every page | One title is enough | Each page needs its own | Poor search results |

</div>

## Check Yourself

## Exercises

## What to Remember

- `npm run build` makes `dist/`. Check it with `npm run preview`.
- Static sites need no adapter. On-demand features need your host's adapter.
- Set `site`. Set `base` for sub-folders and use `BASE_URL` in links.
- Good head tags, `robots.txt`, a sitemap and a 404 page help people find you.
- Keep secrets in the host's dashboard. Deploy from Git.

You have finished the Astro course. Ship your journal, then keep the **appendix** pages close: internationalisation, the cheat sheet and troubleshooting.
