# Publishing & versions

A publish takes an HTML file and stores a frozen artifact served at a URL. This
page covers the model behind that: versions, slugs, visibility, and what
actually gets uploaded.

## Immutable versions

Every publish and every update creates one new immutable version. Prior
versions are frozen and retained; nothing is overwritten in place. A
**publish** mints a new page (version 1) at a fresh URL. An **update** adds a
version to an existing page and keeps the same URL.

```bash
emit publish ./page.html          # -> page, v1, new URL
emit update pg_abc123 ./page.html # -> same URL, v2
```

Use [`emit get <id>`](/docs/cli#emit-get-id) to see a page's full version
history.

### Idempotency

Both publish and update accept `--idempotency-key`. A retry with the same key
returns the same result instead of creating a duplicate version, so an agent
can safely re-send a request after a network hiccup:

```bash
emit publish ./page.html --idempotency-key launch-2026-08-14
```

## Slugs and URLs

Free pages serve at a per-page subdomain on `emits.page`:

```
https://<slug>.emits.page
```

Pass `--domain <slug>` to choose the slug; omit it and one is assigned. If the
slug you want is taken, publish returns a `409` and the CLI prints the id of
the page already holding it, plus the `update` command to reuse it.

On Pro, a bound custom domain also serves every page at `your-domain/<slug>`.
See [custom domains](/docs/domains).

## Visibility

Every page is `public`, `unlisted`, or `private`. Set it at publish time with
`--visibility`, or change it later without republishing:

```bash
emit publish ./page.html --visibility private
emit visibility pg_abc123 public
```

- **public**: served to anyone and discoverable.
- **unlisted**: served to anyone with the URL, not discoverable.
- **private**: not served publicly.

## Tags

Attach tags at publish time (`--tag`, repeatable) and filter on them in `find`.
Tags are how an agent groups and re-locates its own pages:

```bash
emit publish ./page.html --tag launch --tag q3
emit find --tag launch
```

## Multi-page publishes

A single publish can ship more than one page. Pass `--bundle` and point it at
an entry file; the CLI publishes that file's whole directory as one linked unit
under a single slug:

```bash
emit publish ./site/index.html --bundle --domain handbook
```

- The **entry** file (`index.html` above) serves at the slug root:
  `https://handbook.emits.page`.
- Every other `.html` file in the directory serves at its **authored path**:
  `site/about.html` becomes `https://handbook.emits.page/about.html`,
  `site/docs/guide.html` becomes `.../docs/guide.html`.
- Links between pages are ordinary **relative** links (`<a href="about.html">`,
  `<a href="../index.html">`). They resolve exactly as written, because each
  file is served at the path you authored it at. No rewriting, no absolute
  URLs.

The whole unit is one page as far as the rest of Emits is concerned: it has one
slug, one visibility, one version, and a takedown or delete affects all of its
pages together.

A few constraints for this first release:

- **`.html` files only.** CSS, JS, and images in the directory are not bundled
  yet; style with inline CSS or a CDN for now.
- **Relative links only.** A root-absolute link (`/about.html`) is treated as a
  site-root link, not a bundle link.
- **Re-publishing updates in place.** Publishing a bundle to a slug your
  account already owns updates it: the entry keeps its URL, a new immutable
  version is appended (prior versions retained), and added/removed sub-pages
  are reflected. A slug held by a non-bundle page, or by a deleted or
  quarantined one, still 409s.
- Caps: up to 2000 files and 50 MB per bundle.

## How recipes travel with a publish

Most pages are plain HTML, uploaded and served untouched. This section is only
for pages authored with the [Shortwind](https://shortwind.dev/docs) class
layer's `@recipe` shorthand. When you publish such a page from a project that
has a `recipes/` directory, Emits expands the shorthand to Tailwind server-side
at publish time, and the publish carries what the expander needs: your `html`,
the `lockfile` pinning recipe versions, and only the recipe families the page
actually touches. The CLI assembles this for you by walking up from the current
directory to find the project. A recipe that is missing expands to nothing and
goes out as raw text, so compose only from recipes you actually have. Published
pages stay frozen against the versions they shipped with.

## Finding and removing pages

```bash
emit find --q "launch"    # search your pages
emit get pg_abc123        # metadata + version list
emit delete pg_abc123     # tombstone the page (prompts unless -y)
```

Deleting tombstones the page so it stops resolving. See
[trust & safety](/docs/api#trust-and-safety) for how abuse takedowns differ
from a user delete.
