> ## Documentation Index
> Fetch the complete documentation index at: https://docs.artifacta.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Artifact Pages

> Publish artifacts as shareable public web pages — rendered in the browser, no account required.

Artifact Pages let you publish any stored artifact as a web page at `artifacta.io/a/{slug}`. The page is viewable by anyone with the URL — no Artifacta account required. Agents can publish a build report, a rendered chart, or a Markdown summary and hand the URL to a human in the same step.

## How it works

1. **Upload** an artifact with `POST /v1/artifacts` (or `artifacta push`, or `store_artifact` via MCP).
2. **Publish** it with `POST /v1/artifacts/{id}/publish`. The API mints a stable `page_id` (`pg_...`) and returns a `public_url`.
3. The viewer at `artifacta.io/a/{slug}` fetches page metadata via the `get_public_page` RPC (anon key, no tenant data exposed) and renders the content.
4. **Unpublish** at any time with `DELETE /v1/artifacts/{id}/publish`. The URL stops resolving immediately. The artifact itself is unaffected.

The `page_id` (and therefore the URL) is stable across re-publishes. Calling publish again on the same artifact updates the title, visibility, and access mode without changing the URL.

## Content types and rendering

The viewer selects a renderer based on the artifact's MIME type:

| Content type                       | Renderer         | Notes                                                                                                                                           |
| ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `text/html`                        | Sandboxed iframe | `sandbox="allow-scripts"` — no `allow-same-origin`. Scripts run in a separate origin (`artifactausercontent.com`).                              |
| `text/markdown`, `text/x-markdown` | Markdown         | Rendered with `react-markdown` + `rehype-sanitize`. Inline images allowed (HTTPS and `data:` only). Links restricted to `https:` and `mailto:`. |
| `image/*`                          | `<img>`          | SVG served as bytes, never inlined into the page.                                                                                               |
| Anything else                      | Download card    | A styled card with a download button. No inline rendering.                                                                                      |

<Note>
  `application/xhtml+xml` is treated as HTML **for gating** — password protection is rejected for it at publish time, same as `text/html` — but it currently **renders as a download card**, not in the iframe (only `text/html` routes to the iframe). Markdown is also matched by the `.md`/`.markdown` filename extension, so an artifact stored as `text/plain` or `application/octet-stream` with one of those extensions still uses the Markdown renderer.
</Note>

Common image formats render inline via `<img>`, including PNG, JPEG (and `.jfif`), GIF, WebP, SVG, and APNG. `.webp`, `.jfif`, and `.apng` are stored with the correct `image/*` type by a server-side content-type override even on hosts whose MIME database omits them.

## Content origin isolation

Artifact bytes are never served from `artifacta.io` directly. They are served from a separate registrable domain:

```
https://artifactausercontent.com/c/{slug}
```

This is a Cloudflare Worker route. Serving bytes from a separate origin means:

* Untrusted HTML or scripts in an artifact cannot access `artifacta.io` cookies or session data.
* The iframe `sandbox` attribute provides a second layer: no `allow-same-origin` means the framed document cannot escalate past its sandbox even if the content-origin were compromised.
* The Worker sets strict `Content-Security-Policy` and `X-Content-Type-Options: nosniff` headers.

## Visibility

| Value                  | Behavior                                                                                        |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| `"unlisted"` (default) | The page is accessible via URL but is not indexed or listed in any gallery.                     |
| `"public"`             | Reserved for future gallery / search features. Functionally the same as `"unlisted"` at launch. |

## Password protection

Pages can be protected with a passcode (Pro plan). The access flow:

1. The viewer calls `GET /v1/public/pages/{slug}/gate-info` to check whether a password prompt is needed.
2. The visitor submits the password.
3. The viewer calls `POST /v1/public/pages/{slug}/unlock` with `{"password": "..."}`.
4. The API verifies the passcode (argon2id) and returns a `content_token` — a short-lived JWT (5 minutes).
5. The viewer passes the token to the content-origin Worker, which serves the gated bytes.

<Note>
  Password protection is not available for HTML artifacts (`text/html`, `application/xhtml+xml`). The restriction is enforced at publish time.
</Note>

Brute-force is mitigated by a per-slug attempt cap on the API plus a per-IP edge rate-limit rule. An incorrect password returns the same `unauthorized` error as a missing or unpublished slug — the endpoint never reveals whether a slug exists.

<Note>
  After unlock, gated (password-protected) content selects its renderer from the artifact's **stored MIME type only** — there is no filename-extension fallback on this path. So, for example, Markdown renders after unlock only when it is stored as `text/markdown`. The platform ensures this for `.md` and `.markdown` uploads via a server-side content-type override, so artifacts uploaded with those extensions render correctly whether or not the page is password-protected.
</Note>

## Abuse reporting

Any visitor can report a page via `POST /v1/public/pages/{slug}/report`. The endpoint:

* Accepts an optional `reason` (`"spam"`, `"phishing"`, `"malware"`, `"abuse"`, `"copyright"`, `"other"`) and a free-text `detail`.
* Always returns `202 Accepted`, regardless of whether the slug exists.
* Forwards the report to PostHog (and optional Slack webhook) for operator review.
* Does not automatically take down the page. Confirmed violations are unpublished by an operator.

## Indexing policy

All Artifact Pages are served with `noindex` at launch. They do not appear in search engine results.

## API reference

| Action             | Endpoint                                                       |
| ------------------ | -------------------------------------------------------------- |
| Publish            | [`POST /v1/artifacts/{id}/publish`](/api/publish-artifact)     |
| Unpublish          | [`DELETE /v1/artifacts/{id}/publish`](/api/unpublish-artifact) |
| Gate info (public) | [`GET /v1/public/pages/{slug}/gate-info`](/api/gate-info)      |
| Unlock (public)    | [`POST /v1/public/pages/{slug}/unlock`](/api/unlock-page)      |
| Report (public)    | [`POST /v1/public/pages/{slug}/report`](/api/report-page)      |

## CLI

```bash theme={null}
# Publish a file
artifacta publish report.pdf --title "Q2 Report" --public

# Unpublish by artifact ID or page slug
artifacta unpublish art_abc123
artifacta unpublish pg_aB3xK9mP1qR5sT2u
```

## Python SDK

```python theme={null}
from artifacta import Client

client = Client()

# Upload and publish in two steps
artifact = client.push("report.pdf")
page = client.publish_artifact(
    artifact.id,
    title="Q2 Report",
    visibility="public",
)
print(page["public_url"])

# Unpublish
client.unpublish(artifact.id)
```

## MCP (agent usage)

```
"Upload ./out/report.html and publish it as an unlisted page. Return the URL."
```

The agent calls `store_artifact` then `publish_artifact`. The `public_url` is available in the tool result immediately.

```
"Unpublish the page for artifact art_abc123."
```

The agent calls `unpublish_artifact` with `artifact_id: "art_abc123"`.
