# Publishing

Publishing delivers a finished article to its destination — the built-in
Terradium CMS or your own webhook. Articles can publish automatically when they
finish, or stay as drafts until you publish them by hand.

## How publishing works

When an article is published, Terradium delivers it to the project's **target**.
Delivery runs on a background sweep (about once a minute), so a just-published
article may show **Publishing…** briefly before it goes live. A project's publish
state is derived from what's actually been delivered, so it always reflects
reality.

## Auto-publish vs draft

This is set per project under **project settings → Publishing → After generation**:

- **Save as draft** (default) — finished articles wait as drafts for your review.
- **Publish automatically** — finished articles are delivered on their own, after
  a review window and once your site is ready to be cited.

### The review window

Auto-published articles don't go out the instant they finish. They wait for the
project's **review window** — 24 hours by default — so you have a chance to read
one before it's live. Set it to **0** to never auto-publish, or up to 7 days.

Publishing an article yourself is always immediate; the window only applies to
auto-publish.

### The GEO readiness gate

Auto-publish also waits until your site passes its
[GEO readiness](/docs/ai-visibility) checks. There's no point publishing an
article to a page AI engines can't read: if your blog renders its content only in
the browser, returns errors, blocks AI crawlers, or has no brand entity set, an
auto-published article is held rather than delivered.

Held articles show as **on hold** in the publish queue with the reason. They are
not failures and are never dropped — fix the site, and the next readiness check
releases the backlog automatically. This is also why your **first** articles
usually need publishing by hand: until something is live, there's no page for the
readiness check to inspect.

You can always publish a held article yourself — the gate only applies to
auto-publish.

## Rebuild your site on publish

If your site is statically built, publishing an article in Terradium doesn't help
until your site rebuilds. Connect the repository that builds it and Terradium
will trigger a build every time an article goes live.

Under **project settings → Publishing → Rebuild your site on publish**:

1. **Connect GitHub** — installs the Terradium GitHub App and lets you choose
   which repository (or repositories) it can see. Grant it one repo; it does not
   need access to anything else. Start from this button rather than installing
   from GitHub directly, so the installation is linked to this project.
2. **Pick the repository and workflow.** The default workflow file is
   `terradium-publish.yml` on `main`; point us at an existing build workflow
   instead if you already have one. It must have a `workflow_dispatch:` trigger.
3. **Trigger a test build** to confirm the wiring without waiting for an article.

### What Terradium does and doesn't do

Terradium fires a `workflow_dispatch` and nothing else. **Your articles are never
committed into your repository** — the content stays in Terradium and your build
pulls it from the [public content API](/docs/api-reference) at build time. The
repo only ever needs the one workflow file.

The app's permissions are correspondingly narrow: trigger workflows, and write
the single workflow file. No pull requests, no source access, no PAT, and nothing
tied to a Terradium employee's GitHub account.

### A starting workflow

```yaml
name: Terradium publish

on:
  workflow_dispatch:
    inputs:
      reason:
        description: "What triggered this build"
        required: false
        default: manual
      project_id:
        required: false
  push:
    branches: [main]

concurrency:
  # A burst of publishes should produce one deploy, not a queue of them.
  group: terradium-publish
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - name: Build
        env:
          TERRADIUM_API_KEY: ${{ secrets.TERRADIUM_API_KEY }}
        run: npm run build
      - name: Deploy
        run: echo "Add your deploy step here"
```

Store your project API key as a repository **secret** (`TERRADIUM_API_KEY`) —
never commit it. Fetch posts once with `?include=body` rather than one request
per slug: the content API is rate-limited per IP and CI runners share addresses.

### When a build fails

A failed trigger never affects the article — it is already published to the
Terradium CMS and served by the content API. The failure is recorded against the
connection and shown on this settings card, and Terradium retries a few times to
ride out a transient GitHub outage.

The most common cause is a workflow file that doesn't exist on that branch, or
one without a `workflow_dispatch:` trigger. The error message says which.

## Publish targets

Also in project settings, choose where published articles go:

- **Terradium CMS** — a built-in content store served read-only by the
  [public content API](/docs/api-reference). Best if you want to pull posts into
  your own frontend by API key.
- **Webhook** — Terradium `POST`s the finished article as a **signed** JSON
  payload to your URL, so you can ingest it into your own system. Verification
  details are in the [API reference](/docs/api-reference).

## Manual publish

When auto-publish is off, you publish on demand:

- From an **article**, click **Publish** (or **Retry publish** if a delivery
  failed).
- From the **Publish** screen's **Drafts** tab, publish a single article or use
  **Publish all** to send every draft for the selected project.

## The publishing queue

The **Publish** screen has three tabs:

- **Drafts** — finished articles not yet published.
- **Published** — live articles, with their slug, a **copy URL** action, and an
  **Unpublish** button.
- **Queue** — in-flight and failed deliveries, with retry counts and any error.
  Failed rows can be **Retried**; pending rows can be **Cancelled**.

## Publish states

An article shows one of these states:

- **Draft** — finished but not published.
- **Publishing** — queued or mid-delivery.
- **Published** — live (served by the API or delivered to your webhook).
- **Failed** — a delivery error you can retry.
- **Unpublished** — previously live, now taken down.

## Unpublishing

You can take a published article down at any time — from the article's publishing
card or the **Published** tab — with **Unpublish**. It stops being served by the
public API; you can publish it again later.
