URL: /code/apps/sites-and-content/publish-page · Id: `page:/code/apps/sites-and-content/publish-page`

# Page.publish

> Puts one page live at its path and submits its URL to Google and Bing in the same call.

POST https://api.sb/&#123;your-domain&#125;/pages/publish

Puts one [page](https://startupbuilder.dev/business/nouns/page) live at its path and submits its URL to Google and Bing in the same call.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Page.published`
- **Price:** Free

```text title="Run it · Ask"
Put the rent ledger template page live and tell Google and Bing about it.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Page.publish({ page: 'page_Rt5lG9pT2e', submit: ['google-search', 'indexnow'] })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "page": {
    "$id": "https://api.sb/leaseline.app/pages/page_Rt5lG9pT2e",
    "$type": "Page",
    "status": "published",
    "url": "https://leaseline.app/rent-ledger-template",
    "publishedAt": null
  },
  "submitted": [
    {
      "venue": "google-search",
      "status": "accepted",
      "reason": null
    },
    {
      "venue": "indexnow",
      "status": "accepted",
      "reason": null
    }
  ],
  "action": {
    "operation": "Page.publish",
    "mode": "try",
    "status": "done"
  }
}
```

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `page` | `Page` | Required | The draft page to put live, as `Page.create` made it. |
| `submit` | `('google-search' \| 'bing' \| 'indexnow')[]` | `['google-search', 'indexnow']` | Where to submit the URL. `indexnow` reaches Bing and every engine that shares its feed in one request; Google needs your Search Console connected. |
| `sitemap` | `boolean` | `true` | Add the URL to the Site’s sitemap and ping it. |
| `at` | `datetime` | `now` | When it goes live. A later time holds the page as `scheduled` until then. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `page` | `Page` | Never null: the page as published. |
| `page.status` | `'published' \| 'scheduled'` | Never null. |
| `page.url` | `string` | Never null: the live URL, on your own domain. |
| `page.publishedAt` | `string \| null`, in ISO 8601 | In try mode, and on a scheduled page until it goes live. |
| `submitted` | `{ venue: VenueId; status: 'accepted' \| 'skipped'; reason: string \| null }[]` | Never null: one row per engine in `submit`; `skipped` names why, such as an unconnected Search Console. |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Runs every check the publish meets and returns the page as it would go live; nothing is published or submitted.
- `send`: Returns a pending Action at once; `Page.published` fires when the URL serves and the submissions are back.
- `do`: Waits until the URL serves and returns the page with each submission’s answer.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. A page already live at its path is never published twice: a repeat returns it as it stands and submits nothing.

**Saved or shared**

The page’s status, URL and publish time, and each submission’s answer. `Page.checkIndex` reads the result over the next days.

**Emits**

`Page.published` with `{ page, url, submitted }`

**Performed by**

[Content lead](https://startupbuilder.dev/agents/marketing/content-lead)

**Price**

Free on your own account: the platform charges nothing for the call.

## On each Venue
It acts on your own records and reaches no Venue, so every account answers the same way.

## What try does
Runs every check the call meets and returns the record as it would be; nothing is stored, sent or posted.

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A reference page that fails its Evaluators | [`content-evals`](/code/refusals#check-content-evals) | Fix the lines the Evaluator lists, then publish again. | [`Content.draft`](/code/apps/sites-and-content/draft) |
| A page that matches another in its cell by wording, meaning or facts | [`duplicate-page`](/code/refusals#check-duplicate-page) | Merge it into the page that owns the intent. | [`Content.draft`](/code/apps/sites-and-content/draft) |
| A sentence that no approved page and none of the Person’s own words back | [`approved-pages`](/code/refusals#check-approved-pages) | Cut the sentence, or publish the page that says it. | [`Site.publish`](/code/apps/sites-and-content/publish-site) |

```json
{
  "type": "https://startupbuilder.dev/errors/content-evals",
  "title": "Page failed its checks",
  "status": 422,
  "detail": "A reference Page that fails its Evaluators.",
  "fix": "Fix the lines the Evaluator lists, then publish again.",
  "next": {
    "verb": "Content.draft",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
In `send` mode it returns a pending Action, `reason: 'awaitingTime'` when `at` is later, then `running` until the URL serves, and `Page.published` delivers the result.

```ts
await $.Watch.create({ selector: 'Page.published', url: 'https://leaseline.app/hooks/sb' })
```

## Example
A refusal: its wording matches a live page, so the publish refuses before the URL serves.

**Ask**

```text
Put the rent ledger template page live and tell Google and Bing about
it, for `page_test_duplicate`.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/pages/publish?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "page": "page_test_duplicate"
}'
```

**CLI**

```bash
npx startup-builder do Page.publish --data '{
  "page": "page_test_duplicate"
}'
```

The CLI runs in try mode unless you pass `--do`.

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Page.publish({ page: 'page_test_duplicate' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Page.publish({ page: 'page_test_duplicate' })
```


```json title="Response, in try mode"
{
  "type": "https://startupbuilder.dev/errors/duplicate-page",
  "title": "Page matches another Page",
  "status": 422,
  "detail": "A Page that matches another in its cell by wording, meaning or facts.",
  "fix": "Merge it into the Page that owns the intent.",
  "next": {
    "verb": "Content.draft",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/checks#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `page_test_published` | `{"page":{"status":"published","url":"https://test.leaseline.app/rent-ledger-template"}}` | Goes live on a test host that no engine reads, so the next step can run on a known URL. |
| `page_test_duplicate` | Refuses with [`duplicate-page`](/code/refusals#check-duplicate-page) | Its wording matches a live page, so the publish refuses before the URL serves. |

## Used by
None.

## Related
[`Content.draft`](/code/apps/sites-and-content/draft) · [`Content.publish`](/code/apps/sites-and-content/publish-content) · [`Site.publish`](/code/apps/sites-and-content/publish-site) · [`Page.create`](/code/apps/sites-and-content/create-page) · [`Page.update`](/code/apps/sites-and-content/update-page) · [`Content.create`](/code/apps/sites-and-content/create-content) · [`Content.update`](/code/apps/sites-and-content/update-content) · [`Domain.connect`](/code/apps/sites-and-content/connect)