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

# Theme Manifest Reference

> Complete reference for the outpost-manifest.yaml schema, including event types, field definitions, constraints, custom fields, and error handling.

This reference documents every option available in the `outpost-manifest.yaml` file. For a quick introduction, see [Getting Started](/themes/manifest-getting-started).

## File Location

The manifest file must be named `outpost-manifest.yaml` and placed in your theme's root directory, alongside `package.json`.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
your-theme/
├── package.json
├── outpost-manifest.yaml  ← here
├── default.hbs
├── post.hbs
└── ...
```

## Schema Overview

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
manifest_version: 1           # Required: must be 1

events:                       # Optional: include for event-capable themes
  default_tags: []            # Optional: tags for all event types
  types:                      # Required within events: at least one type
    type_key:                 # Your type identifier (e.g., "webinar")
      label: "Display Name"
      post_template: "custom-template"  # Required
      tags: []
      supported_fields: []
      required_fields: []
      field_constraints: {}
      custom_fields: []
      messaging_states: []
      default: true           # Optional: preselect this type in the event editor
```

## Top-Level Fields

### manifest\_version

**Required.** Must be `1`. This enables future schema evolution without breaking existing manifests.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
manifest_version: 1
```

### events

**Optional.** Contains all event-related configuration. Include it when your theme supports Outpost events; a manifest can declare `events`, `homepage_curation`, or both.

## Events Section

### events.default\_tags

**Optional.** Array of Ghost tag names applied to ALL event types when published.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
events:
  default_tags:
    - "event"
    - "registration-open"
```

Tags are strings. Invalid entries (non-strings) are silently ignored.

### events.types

**Required when `events` is present.** Object where each key is an event type identifier and the value is that type's configuration.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
events:
  types:
    webinar:
      # ... webinar config
    workshop:
      # ... workshop config
    meetup:
      # ... meetup config
```

Type keys should be lowercase with underscores (e.g., `in_person`, `virtual_workshop`). These keys are stored on event records and used for identification.

## Event Type Configuration

Each event type supports the following fields:

### post\_template

**Required.** The Ghost custom template filename (without `.hbs` extension) used for this event type's posts.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    post_template: "custom-webinar"
```

This template must exist in your theme. Events using this type will have their Ghost posts set to use this template.

### label

**Optional.** Human-readable name shown in the Outpost UI. Defaults to the type key with underscores replaced by spaces and the first letter capitalized.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  in_person:
    label: "In-Person Event"  # Shows as "In-Person Event" in dropdown
```

### tags

**Optional.** Array of Ghost tags specific to this event type. Merged with `default_tags` when publishing.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    tags:
      - "webinar"
      - "online-event"
```

### supported\_fields

**Optional.** Array of standard field keys to show for this event type. If omitted, all standard fields are shown.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    supported_fields:
      - name
      - description
      - event_date
      - event_time_start
      - event_time_end
      - event_time_zone
      - platform_url
      - access
```

See [Standard Field Keys](#standard-field-keys) for all available fields.

### required\_fields

**Optional.** Array of field keys that must be filled before publishing. Must be a subset of `supported_fields` (or standard fields if `supported_fields` is omitted).

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    required_fields:
      - name
      - event_date
      - platform_url
```

<Warning>
  If a required field is not in `supported_fields`, it will be ignored with a warning.
</Warning>

### field\_constraints

**Optional.** Object holding constraint hints for standard fields. Outpost surfaces `feature_image` constraints as recommended image dimensions in the event editor; constraints on other standard fields are stored with the manifest but are not enforced when publishing.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  in_person:
    field_constraints:
      feature_image:
        aspect_ratio: "16:9"
        min_width: 1200
        min_height: 630
      description:
        max_length: 500
```

A `feature_image` constraint reaches the publisher as a comma-joined phrase built from `min_width` ("1200px wide"), `min_height` ("630px tall"), and `aspect_ratio` ("16:9 aspect ratio"). The phrase shows under the Featured Image field and again inside the Crop Image dialog. Only `aspect_ratio` shapes the crop stencil. `min_width` and `min_height` set the smallest image the editor will produce: a crop smaller than those values is scaled up to meet them after the crop is applied.

If an event type sets no `feature_image` constraint, the editor falls back to the site's Event Settings, under **Default Featured Image Size**. If neither is set, it shows a built-in recommendation of 1200 x 630px, so publishers always see a recommended size.

See [Field Constraints](#field-constraints) for available constraint options.

### custom\_fields

**Optional.** Array of custom field definitions for theme-specific data.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    custom_fields:
      - key: speaker_name
        label: "Speaker Name"
        type: text
        required: true
```

See [Custom Field Types](#custom-field-types) for complete options.

### messaging\_states

**Optional.** Array of messaging state names. Outpost validates the names in this key but does not act on it. Event Settings offers all seven messaging states no matter what an event type declares here.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    messaging_states:
      - can_register
      - registered
      - event_passed
```

<Warning>
  A state name Outpost does not recognize produces a finding, and any finding keeps Homepage Curation from activating. Because the key has no effect, leaving it out is the safer choice.
</Warning>

See [Messaging States](#messaging-states) for the names Outpost recognizes.

### default

**Optional.** Boolean. Mark one event type with `default: true` and Outpost preselects it in the **Event Type** dropdown when a publisher creates a new event.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
types:
  webinar:
    default: true
    post_template: "custom-webinar"
```

If more than one type claims it, Outpost keeps the first and records a finding for the rest.

## Standard Field Keys

These are the built-in fields available for `supported_fields` and `required_fields`:

| Key                     | Description                              | Data Type                          |
| ----------------------- | ---------------------------------------- | ---------------------------------- |
| `name`                  | Event name/title                         | string                             |
| `slug`                  | URL-safe identifier                      | string                             |
| `description`           | Event description (used as post excerpt) | string                             |
| `event_date`            | Date of the event                        | date                               |
| `event_time_start`      | Start time                               | string (e.g., "10:00 am")          |
| `event_time_end`        | End time                                 | string (e.g., "11:00 am")          |
| `event_time_zone`       | IANA timezone                            | string (e.g., "America/New\_York") |
| `location`              | Venue or location text                   | string                             |
| `platform_url`          | Virtual event platform link              | URL string                         |
| `max_attendees`         | Capacity limit                           | integer                            |
| `feature_image`         | Featured/hero image                      | URL string                         |
| `access`                | Who can register                         | enum: `anyone`, `free`, `paid`     |
| `rsvp_labels`           | Labels applied to registrants            | array of strings                   |
| `registration_deadline` | Registration cutoff date                 | date                               |
| `deadline_time_end`     | Registration cutoff time                 | string                             |

## Field Constraints

Available constraints vary by field type:

<Info>
  Constraints on standard fields are guidance for editors, not publish-time validation. Outpost currently reads `feature_image` constraints to show recommended image dimensions in the event editor; the rest are stored with the manifest for future use. With no `feature_image` constraint, the editor falls back to the site's Event Settings, under **Default Featured Image Size**, and then to a built-in 1200 x 630px recommendation.
</Info>

### Text/Textarea Fields

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
field_constraints:
  description:
    max_length: 500        # Maximum character count
    pattern: "^[A-Z]"      # Regex pattern (optional)
```

### Image Fields

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
field_constraints:
  feature_image:
    aspect_ratio: "16:9"   # Width:height ratio
    min_width: 1200        # Minimum width in pixels
    min_height: 630        # Minimum height in pixels
```

`min_width` and `min_height` set the smallest crop the editor will produce: a smaller selection is scaled up to meet them.

### Number Fields

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
field_constraints:
  max_attendees:
    min: 1
    max: 1000
```

## Custom Field Types

Custom fields support these types:

### text

Single-line text input.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: speaker_name
  label: "Speaker Name"
  type: text
  required: true
  help_text: "Full name of the presenter"
  max_length: 100
  pattern: "^[A-Za-z\\s]+$"
```

### textarea

Multi-line text input.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: speaker_bio
  label: "Speaker Bio"
  type: textarea
  required: false
  max_length: 500
```

### number

Numeric input with optional range constraints.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: session_duration
  label: "Duration (minutes)"
  type: number
  required: true
  min: 15
  max: 480
```

### select

Dropdown with predefined options.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: session_type
  label: "Session Type"
  type: select
  required: true
  options:
    - Keynote
    - Breakout
    - Workshop
    - Panel
```

<Warning>
  Select fields **must** include an `options` array. Fields without options are skipped with an error.
</Warning>

### url

URL input with validation.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: slides_url
  label: "Slides URL"
  type: url
  required: false
  help_text: "Link to presentation slides"
```

### date

Date picker input.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: early_bird_deadline
  label: "Early Bird Deadline"
  type: date
  required: false
```

### toggle

Boolean switch input.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: is_recorded
  label: "Will Be Recorded"
  type: toggle
  required: false
```

### image

Image upload with optional constraints.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- key: speaker_photo
  label: "Speaker Photo"
  type: image
  required: false
  aspect_ratio: "1:1"
  min_width: 400
  max_file_size_kb: 2048
```

## Custom Field Properties

All custom field types support these properties:

| Property    | Type    | Required | Description                                          |
| ----------- | ------- | -------- | ---------------------------------------------------- |
| `key`       | string  | Yes      | Unique identifier (no spaces, lowercase recommended) |
| `label`     | string  | No       | Display label (defaults to formatted key)            |
| `type`      | string  | Yes      | One of the field types above                         |
| `required`  | boolean | No       | Whether field must be filled (default: false)        |
| `help_text` | string  | No       | Helper text shown below the field                    |

<Warning>
  Custom field keys **must not** collide with standard field keys. Keys like `name`, `event_date`, `access`, etc. will be rejected with an error.
</Warning>

<Note>
  At publish time, Outpost enforces `required`, number `min`/`max`, URL format, select `options`, and `max_length` on text and textarea fields. Other values (`pattern`, and image `aspect_ratio`, `min_width`, `max_file_size_kb`) are stored with the field but not yet enforced.
</Note>

## Messaging States

These are the messaging state names Outpost recognizes:

| State                 | When Shown to Members                 |
| --------------------- | ------------------------------------- |
| `can_register`        | Eligible member who hasn't registered |
| `login_required_free` | Guest on a free event                 |
| `login_required_paid` | Guest on a paid event                 |
| `upgrade_required`    | Free member on a paid event           |
| `registered`          | Member who has RSVP'd                 |
| `registration_closed` | After registration deadline           |
| `event_passed`        | After the event ends                  |

All seven are editable in Event Settings, a site-wide screen. See [messaging\_states](#messaging_states) for what an event type's key does.

## Homepage Curation Section

**Optional.** Declares curated homepage regions that a publisher can fill from the Outpost UI. When a theme's manifest includes a top-level `homepage_curation` key, Outpost shows a **Homepage Curation** screen (under Posts) where editors pick which post occupies each slot, without hand-editing tags in Ghost.

`homepage_curation` and `events` are independent top-level keys in the same `outpost-manifest.yaml`; a theme can declare either, both, or neither.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
homepage_curation:
  sections:                              # Required: one or more sections
    - key: featured-us                   # Section identifier (unique)
      name: "Featured (US)"              # Display name in the curation UI
      description: "Top-of-homepage hero carousel."   # Optional helper text
      picker:                            # Optional: controls the post picker
        default_view: recent             # Default tab: recent or search, nothing else
        recent_limit: 30                 # Recent posts to offer: default 20, maximum 50
        recent_filter:                   # Optional: pre-scope picker to a tag
          tag: local-news
      slots:                             # Required: one or more slots
        - { key: slot-1, label: "Slot 1", tag: hash-home-hero-slot-1 }
        - { key: slot-2, label: "Slot 2", tag: hash-home-hero-slot-2 }
```

### How slots work

Each **slot** binds one Ghost tag to one post. The theme renders whichever single post currently carries that tag; the curation UI swaps which post wears it. Slot tags are ordinary Ghost internal tags: a `hash-` prefix maps to a `#` internal tag.

| Field                                 | Required | Description                                                                               |
| ------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `sections[].key`                      | Yes      | Unique identifier for the section                                                         |
| `sections[].name`                     | Yes      | Display name shown in the curation UI                                                     |
| `sections[].description`              | No       | Helper text shown under the section                                                       |
| `sections[].picker.default_view`      | No       | Default picker tab. Exactly `recent` or `search`, defaulting to `recent`                  |
| `sections[].picker.recent_limit`      | No       | How many recent posts to list. A positive whole number, defaulting to 20 and capped at 50 |
| `sections[].picker.recent_filter.tag` | No       | Restrict the picker to posts carrying this tag                                            |
| `slots[].key`                         | Yes      | Unique identifier for the slot within its section                                         |
| `slots[].label`                       | Yes      | Label shown above the slot in the UI                                                      |
| `slots[].tag`                         | Yes      | Ghost tag that binds a post to this slot                                                  |

Be exact with these two values. Both are corrected for you rather than rejected, but each records a finding, and any finding keeps Homepage Curation from activating. `default_view` accepts only `recent` and `search`; any other value is replaced with `recent`. `recent_limit` falls back to 20 when it is not a positive whole number. A value above 50 is reduced to 50, with no finding.

<Note>
  Homepage Curation is an early Labs-status feature, found under **Posts** in the left menu, and currently ships in the single-file manifest format. A complete worked example is bundled with the docs as `cascade-homepage-curation.yaml`.
</Note>

## Error Handling

Outpost validates the manifest every time a theme is uploaded or the manifest is refreshed. Each problem it finds is recorded alongside the manifest data as a finding, carrying a field path, a message, and a severity of either `error` or `warning`.

### Activation requires a clean manifest

Homepage Curation only activates from a manifest that validated with **zero findings**, warnings included. One warning anywhere in the file is enough to stop the whole manifest from activating, even when it sits on a section unrelated to the curation slots you are adding.

<Warning>
  Severity changes how a finding is worded, not what happens to your manifest. A finding marked `warning` blocks Homepage Curation exactly as a finding marked `error` does. Treat the findings list as a to-do list that must reach empty, not as a set of suggestions.
</Warning>

The practical consequence for a manifest that has never validated cleanly: the **Homepage Curation** screen never appears under Posts. There is no partially-activated state to look for. If the menu is missing, open the findings list and clear every entry in it, whatever the severity says.

Once a manifest has activated, Outpost keeps that copy as the last known-good version. A later push that fails validation does not take a live feature offline: Homepage Curation keeps running on the last version that validated cleanly, and your new changes do not appear until the findings are cleared. This is the only sense in which anything is "kept" across a failed push.

The same protection covers a refresh that cannot complete. If Outpost cannot reach GitHub, or the token it uses is rejected, or the request fails for any other reason, it reports the failure and leaves both the stored manifest and the last known-good copy exactly as they were.

<Warning>
  There is one exception. If the refresh finds no `outpost-manifest.yaml` in the repository, Outpost treats the file as deliberately removed and clears the stored manifest along with the last known-good copy. Homepage Curation then switches off. Deleting or renaming the manifest file and refreshing is the one action that takes the feature offline. Restoring the file and refreshing again brings it back.
</Warning>

<Note>
  Event types behave differently. The event editor reads the most recent manifest directly, so it does drop the parts that failed validation and keep the parts that passed. An unknown field name in one event type does not hide your other event types. Homepage Curation is the feature with the all-or-nothing activation rule.
</Note>

### Error Categories

Every category below blocks Homepage Curation activation. The severity column is the label Outpost puts on the finding, and the effect column describes what validation does to that part of the manifest before storing it.

| Category                                        | Reported as | Effect on the stored manifest                       |
| ----------------------------------------------- | ----------- | --------------------------------------------------- |
| YAML syntax error                               | Error       | Nothing is stored, manifest ignored entirely        |
| Unknown manifest\_version                       | Warning     | Nothing is stored, manifest ignored entirely        |
| Missing or invalid `types` section              | Warning     | No event types are stored                           |
| Missing post\_template                          | Error       | Event type dropped                                  |
| Multiple event types marked as default          | Warning     | The first type stays default, the rest lose the key |
| Unknown standard field                          | Warning     | Field key dropped                                   |
| Required field not in supported                 | Warning     | Requirement dropped                                 |
| Unknown messaging state                         | Warning     | State name dropped                                  |
| A list key given a non-list value               | Warning     | Key ignored                                         |
| Custom field missing `key`                      | Error       | Custom field dropped                                |
| Custom field missing `type`                     | Error       | Custom field dropped                                |
| Custom field key collision                      | Error       | Custom field dropped                                |
| Unknown custom field type                       | Error       | Custom field dropped                                |
| Missing options on select                       | Error       | Custom field dropped                                |
| Missing or invalid `sections` array             | Warning     | Whole `homepage_curation` section dropped           |
| Missing or duplicate section key                | Error       | Curation section dropped                            |
| Missing section name                            | Error       | Curation section dropped                            |
| Section with no valid slots                     | Error       | Curation section dropped                            |
| Missing or duplicate slot key                   | Error       | Slot dropped                                        |
| Missing slot label                              | Error       | Slot dropped                                        |
| Missing or duplicate slot tag                   | Error       | Slot dropped                                        |
| Invalid picker `default_view` or `recent_limit` | Warning     | Value replaced with the default                     |
| Invalid picker `recent_filter.tag`              | Warning     | Filter dropped                                      |
| Unknown picker key                              | Warning     | Key dropped                                         |

The list keys are `default_tags`, `tags`, `supported_fields`, `required_fields`, `custom_fields`, and `messaging_states`.

Two rows show why severity is not a guide to consequences. An unknown `manifest_version` is labelled a warning and discards the entire manifest. An unknown picker key is labelled a warning, has no effect on the stored manifest at all, and still keeps Homepage Curation from activating.

### Viewing Errors

Findings are visible in:

* The **Manifest Status** column of the Themes table, in **Labs → Theme Connect Wizard**. Click the info icon next to the count to list them.
* The event editor, as a banner at the top of the form.
* The theme upload or refresh API response.

The Manifest Status column is the check to run before anything else. It reads **Valid** only when the manifest produced zero findings, which is also the only state in which Homepage Curation activates. Anything else reads as a count of warnings, and that count includes both severities: `3 warning(s)` can mean three errors. Aim for **Valid**, not for a low count.

### Example Error Response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "_errors": [
    {
      "field": "types.webinar.post_template",
      "message": "Missing required 'post_template'",
      "severity": "error"
    },
    {
      "field": "types.in_person.supported_fields",
      "message": "Unknown standard field 'title'",
      "severity": "warning"
    }
  ]
}
```

## Complete Example

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
manifest_version: 1

events:
  default_tags:
    - "event"

  types:
    webinar:
      default: true
      label: "Webinar"
      post_template: "custom-webinar"
      tags:
        - "webinar"
        - "online"

      supported_fields:
        - name
        - description
        - event_date
        - event_time_start
        - event_time_end
        - event_time_zone
        - platform_url
        - feature_image
        - access
        - registration_deadline
        - deadline_time_end

      required_fields:
        - name
        - event_date
        - platform_url

      field_constraints:
        feature_image:
          aspect_ratio: "16:9"
          min_width: 1200
        description:
          max_length: 300

      custom_fields:
        - key: speaker_name
          label: "Speaker Name"
          type: text
          required: true
          help_text: "Full name of the presenter"

        - key: speaker_bio
          label: "Speaker Bio"
          type: textarea
          required: false
          max_length: 500

        - key: session_type
          label: "Session Type"
          type: select
          required: true
          options:
            - Live Presentation
            - Q&A Session
            - Workshop

        - key: is_recorded
          label: "Will Be Recorded"
          type: toggle

    in_person:
      label: "In-Person Event"
      post_template: "custom-in-person"
      tags:
        - "in-person"
        - "conference"

      supported_fields:
        - name
        - description
        - event_date
        - event_time_start
        - event_time_end
        - event_time_zone
        - location
        - max_attendees
        - feature_image
        - access

      required_fields:
        - name
        - event_date
        - location
        - feature_image

      custom_fields:
        - key: venue_address
          label: "Venue Address"
          type: textarea
          required: true

        - key: track
          label: "Track"
          type: select
          required: true
          options:
            - Main Stage
            - Workshop Room A
            - Workshop Room B
            - Networking Hall
```
