# Content Management Guide

This guide explains the RiverbankCMS content model and how to work with content via the SDK.

## Content Model Overview

RiverbankCMS organizes content into five main types:

### 1. Entries

Content items for a specific content type (e.g., blog posts, products, team members).

- Defined by content types in `riverbank.config.ts`
- Each entry has an identifier, slug, and type-specific data
- Can be draft or published

**Local file structure:**

```
content/
  entries/
    blog-post.json    # All blog-post entries
    product.json      # All product entries
```

### 2. Pages

Standalone pages that contain blocks.

- Have a path (URL), title, and SEO metadata
- Contain an ordered list of blocks
- Can be draft or published

**Local file structure:**

```
content/
  pages/
    home.json         # Home page with blocks
    about.json        # About page with blocks
```

### 3. Blocks

Content components within pages.

- Each block has a kind (e.g., `block.hero`, `block.body-text`)
- Contains kind-specific content data
- Ordered by position within a page

**Common block kinds:**

| Kind                  | Description                                            |
| --------------------- | ------------------------------------------------------ |
| `block.hero`          | Hero/banner sections with heading, image, CTA          |
| `block.body-text`     | Rich text content                                      |
| `block.image`         | Image with caption                                     |
| `block.image-gallery` | Image gallery (grid/carousel/masonry/marquee variants) |
| `block.cta`           | Call-to-action buttons                                 |
| `block.embed`         | Embedded content (videos, etc.)                        |

See `block-types.md` for the full list.

### 4. Navigation

Menu structures for site navigation.

- Named menus (e.g., "main", "footer")
- Hierarchical items with labels, URLs, and children

**Local file structure:**

```
content/
  navigation.json     # All navigation menus
```

### 5. Themes

Site theme objects used for rendering blocks.

**Local file structure:**

```
content/
  themes/
    current.json      # Default theme (pulled from CMS or edited locally)
```

### 6. Footer Content

Site footer content (non-navigation), currently just optional bottom text.

- Optional rich text for copyright notices, legal text, etc.

**Local file structure:**

```
content/
  footer.json         # Footer bottom text (non-navigation)
```

**Note:** Footer links are managed via navigation menus. `content/footer.json` is for non-link content only.

### 7. Event-Related Content

For sites with events functionality, additional content types are available:

**Venues** - Physical locations where events take place.

```
content/
  venues/
    main-hall.json    # Venue details with address, capacity, etc.
```

**Event Categories** - Categories for organizing events.

```
content/
  event-categories/
    workshop.json     # Category with name, description, color
```

**Events** - Event series with scheduling, pricing, and registration.

```
content/
  events/
    yoga-class.json   # Event with schedule, pricing, venue/category refs
```

---

## Working with Identifiers

Identifiers are stable, human-readable names for content:

- Used by the SDK to reference content reliably
- Should be kebab-case (e.g., `my-blog-post`, `about-us`)
- Are idempotent - upserting with same identifier updates existing content
- Allow content to be tracked across environments

**Examples:**

```bash
# Entry identifier
riverbankcms entry upsert blog-post my-first-post --data '...'

# Page identifier
riverbankcms page upsert about --title "About Us" --path /about

# Block identifier (within a page)
riverbankcms block upsert about hero-section --kind block.hero --data '...'
```

**Backfill identifiers for legacy content:**

```bash
riverbankcms identifiers backfill
```

---

## Media Identifiers (Local Sync)

For media fields, you can reference files by a stable identifier:

- Add `identifier` to the media field value (slug only, no extension)
- Place files at `content/media/<identifier>.<ext>`
- Pull/download media: `riverbankcms pull --local-media`
- Push/upload media: `riverbankcms push` (default-on)

Rich text images (TipTap JSON) are also identifier-based:

- Store images as `{ type: "image", attrs: { identifier: "..." } }` (no persisted `attrs.src`)
- External `attrs.src` URLs are auto-imported into CMS media on write

### Managing Site Logo and Favicon

Use `content/settings.json` as the repo-managed source of truth for branding:

```json
{
  "homepageId": null,
  "seoDefaults": null,
  "logoIdentifier": "positive-play-primary-logo",
  "faviconIdentifier": "positive-play-primary-logo"
}
```

Recommended workflow:

1. Add the branding files to `content/media/`, for example `content/media/positive-play-primary-logo.svg`
2. Reference those identifiers in `content/settings.json`
3. Run `riverbankcms push`
4. Run `riverbankcms pull settings` if you want to confirm the CMS state round-trips cleanly

Notes:

- `riverbankcms push` uploads missing branding files through the normal local-media sync path before updating site settings
- `riverbankcms pull settings` warns and omits `logoIdentifier` / `faviconIdentifier` if the current CMS branding asset has no media identifier yet
- Dashboard branding edits and CLI pushes both update the same site settings row, so treat them as last-write-wins and pull before pushing after dashboard-side edits

---

## Draft vs Published

All content has a publication status:

| Status    | Visibility                   |
| --------- | ---------------------------- |
| Draft     | Only visible in preview mode |
| Published | Visible on the live site     |

New content is created as a draft by default.

### Checking Status

```bash
# Check entry status
riverbankcms entry get blog-post my-post

# Check page status
riverbankcms page get about

# List with JSON to see status field
riverbankcms entry list blog-post --json | jq '.entries[] | {identifier, status}'
```

### Publishing Content

```bash
# Publish entry
riverbankcms entry publish blog-post my-post

# Publish page
riverbankcms page publish about
```

### Unpublishing Content

```bash
# Unpublish entry (revert to draft)
riverbankcms entry unpublish blog-post my-post

# Unpublish page
riverbankcms page unpublish about
```

### Unpublished Changes

When you modify published content, it stays published but gains unpublished changes. The live site shows the last published version until you publish again.

```bash
# Check for unpublished changes
riverbankcms entry get blog-post my-post --json | jq '.hasUnpublishedChanges'

# Publish to apply pending changes
riverbankcms entry publish blog-post my-post
```

---

## Content Sync Workflow

### Pulling Content

Downloads CMS content to local JSON files:

```bash
# Pull all content
riverbankcms pull

# Pull specific types
riverbankcms pull entries
riverbankcms pull entries blog-post
riverbankcms pull entries blog-post my-post
riverbankcms pull pages
riverbankcms pull pages home
riverbankcms pull navigation
riverbankcms pull footer               # Footer content
riverbankcms pull forms
riverbankcms pull venues               # Event venues
riverbankcms pull event-categories     # Event categories
riverbankcms pull events               # Event series
```

**Output directory structure:**

```
content/
├── entries/
│   ├── blog-post/
│   │   └── my-post.json
│   └── product/
│       └── featured-product.json
├── pages/
│   ├── home.json
│   └── about.json
├── navigation.json
└── footer.json
```

### Pushing Content

Uploads local changes to the CMS:

```bash
# Preview changes (recommended)
riverbankcms push --dry-run

# Apply changes
riverbankcms push
```

### Sync Configuration

Configure sync behavior in `riverbank.config.ts`:

```typescript
export default defineConfig({
  siteId: "...",
  sync: {
    // How to handle existing entries during push
    existingEntries: "update", // 'skip' (default) or 'update'
  },
});
```

| Option            | Values           | Description                                  |
| ----------------- | ---------------- | -------------------------------------------- |
| `existingEntries` | `skip`, `update` | Whether to update entries that already exist |

**Status Sync**: Push automatically syncs draft/published status based on the `status` field in your local content files. Set `"status": "published"` in a local file to publish on push, or `"status": "draft"` to unpublish.

### Environment Cloning (Remote ↔ Local) Best Practices

Broken images after env pulls/pushes usually happen when env-specific media fields (like `assetId` or storage paths) leak into portable content JSON. The SDK is designed so **content JSON is portable** and media references are **identifier-only**.

**Rules of thumb:**

- **Do not** commit or hand-write `assetId`, `storagePath`, `storageBucket`, or `src` into content JSON.
- Prefer `{ type: "image", identifier: "..." }` in structured content, and `{ type: "image", attrs: { identifier: "..." } }` in rich text.
- If you hit an `INVALID_MEDIA_REFERENCE` error during push, re-run a fresh pull with the latest CLI and push again.

#### Clone production CMS → local CMS (recommended)

This is the safest “env clone” flow (portable JSON + local media files → push into target CMS):

```bash
# 1) Pull remote content to a dedicated directory
riverbankcms pull --env=remote --output .tmp/remote-clone --yes

# 2) Download the media bytes referenced by identifiers into .tmp/remote-clone/media
riverbankcms pull --env=remote --output .tmp/remote-clone --local-media --yes

# 3) Push that snapshot into your local CMS (local DB gets its own assetIds)
riverbankcms push --env=local --content-dir .tmp/remote-clone --yes
```

#### Sync media bytes CMS → CMS (optional, faster for large libraries)

Use this when you explicitly want to copy media bytes between environments without going through local files:

```bash
# Remote storage -> local storage (identifier + checksum based)
riverbankcms pull --env=remote --sync-media --yes

# Fail-safe by default: checksum mismatches abort unless you explicitly overwrite
riverbankcms pull --env=remote --sync-media --overwrite-media --yes
```

Notes:

- `--sync-media` is **identifier + checksum** based. If the target already has an identifier with different bytes, the command fails unless you pass `--overwrite-media`.
- When pulling from remote, `--sync-media` may need to **write to the source CMS** to backfill missing identifiers, so remote sync requires `--yes`.

#### Promote local files → production CMS

```bash
# Always pull first to refresh stale detection metadata
riverbankcms pull --env=remote --yes

# Then push your local content snapshot
riverbankcms push --env=remote --yes
```

---

## Local Content Files

### Entry File Format

`content/entries/blog-post.json`:

```json
[
  {
    "identifier": "my-first-post",
    "slug": "my-first-post",
    "data": {
      "title": "My First Post",
      "content": "<p>Hello world!</p>",
      "publishedAt": "2024-01-15"
    }
  },
  {
    "identifier": "another-post",
    "slug": "another-post",
    "data": {
      "title": "Another Post",
      "content": "<p>More content...</p>"
    }
  }
]
```

### Page File Format

`content/pages/home.json`:

```json
{
  "identifier": "home",
  "title": "Home",
  "path": "/",
  "seo": {
    "title": "Welcome | My Site",
    "description": "The home page"
  },
  "blocks": [
    {
      "identifier": "hero-main",
      "kind": "block.hero",
      "content": {
        "heading": "Welcome to My Site",
        "subheading": "We build great things"
      }
    },
    {
      "identifier": "intro-text",
      "kind": "block.body-text",
      "content": {
        "content": "<p>Introduction text here...</p>"
      }
    }
  ]
}
```

### Navigation File Format

`content/navigation.json`:

```json
{
  "menus": [
    {
      "identifier": "main",
      "name": "Main Navigation",
      "items": [
        { "label": "Home", "link": { "kind": "page", "identifier": "home" } },
        {
          "label": "Get Started",
          "link": { "kind": "url", "href": "/signup" },
          "isCta": true
        },
        {
          "label": "Products",
          "children": [
            {
              "label": "Category A",
              "link": { "kind": "url", "href": "/products/a" }
            },
            {
              "label": "Category B",
              "link": { "kind": "url", "href": "/products/b" }
            }
          ]
        }
      ]
    },
    {
      "identifier": "footer",
      "name": "Footer Navigation",
      "items": [
        { "label": "Privacy", "link": { "kind": "url", "href": "/privacy" } },
        { "label": "Terms", "link": { "kind": "url", "href": "/terms" } }
      ]
    }
  ]
}
```

`isCta` is optional and only applies to root-level link items. Dropdown containers and nested child items cannot be CTAs.

### Footer File Format

`content/footer.json`:

```json
{
  "bottomText": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "content": [
          {
            "type": "text",
            "text": "© 2024 Your Company. All rights reserved."
          }
        ]
      }
    ]
  }
}
```

**Footer fields:**

| Field        | Type             | Description                                  |
| ------------ | ---------------- | -------------------------------------------- |
| `bottomText` | ProseMirror JSON | Rich text for copyright, legal notices, etc. |

Footer links are managed via the site's footer navigation menu (see `content/navigation.json`).

### Venue File Format

`content/venues/main-hall.json`:

```json
{
  "slug": "main-hall",
  "title": "Main Conference Hall",
  "description": "Our main venue for large events",
  "timezone": "America/Los_Angeles",
  "capacity": 200,
  "address": {
    "line1": "123 Main St",
    "city": "San Francisco",
    "region": "CA",
    "postcode": "94102",
    "country": "US"
  }
}
```

### Event Category File Format

`content/event-categories/workshop.json`:

```json
{
  "identifier": "workshop",
  "name": "Workshop",
  "description": "Hands-on learning sessions",
  "color": "#3B82F6",
  "sortOrder": 1
}
```

### Event File Format

`content/events/yoga-class.json`:

```json
{
  "identifier": "yoga-class",
  "title": "Morning Yoga Class",
  "slug": "yoga-class",
  "status": "active",
  "visibility": "public",
  "venueRef": "main-hall",
  "categoryRef": "workshop",
  "capacity": 30,
  "schedule": {
    "recurrenceRule": "FREQ=WEEKLY;BYDAY=MO",
    "startDate": "2026-01-05",
    "defaultStartTime": "07:00:00",
    "durationMinutes": 60
  },
  "pricing": {
    "requiresPayment": true,
    "priceAmount": 2500,
    "priceCurrency": "usd",
    "acceptsPasses": true,
    "acceptsMemberships": true
  },
  "content": {
    "summary": "Start your day with energizing yoga",
    "body": "<p>Join us for a revitalizing morning yoga session.</p>"
  }
}
```

**Event field notes:**

- `priceAmount` is in cents (2500 = $25.00)
- `recurrenceRule` uses iCal RRULE format
- `startDate` is `YYYY-MM-DD`; for one-off events use `recurrenceRule: null` plus `startDate`
- `venueRef` and `categoryRef` reference slugs/identifiers from venues and event-categories
- `status`: "draft", "active", "completed", "cancelled", or "archived"
- `visibility`: "private", "public", or "unlisted"

See `cli-reference.md` for event-related CLI commands: `riverbankcms push/pull venues|event-categories|events`.

---

## Configuration Reference

### riverbank.config.ts

Main SDK configuration:

```typescript
import { defineConfig } from '@riverbankcms/sdk';

export default defineConfig({
  // Required: Your site ID
  siteId: 'your-site-id',

  // Optional: Dashboard UI configuration (navigation visibility, etc.)
  dashboard: {
    navigation: {
      // Top nav (Overview/Content/Media/Settings/etc.)
      siteNav: {
        // mode: 'augment' | 'restrict'
        // allowIds: [...]
        // denyIds: [...]
        // roles: { editor: { ... }, administrator: { ... } }
      },
      // Settings sidebar (General/Emails/Members/etc.)
      settingsNav: {
        // mode: 'augment' | 'restrict'
        // allowIds: [...]
        // denyIds: [...]
        // roles: { editor: { ... }, administrator: { ... } }
      },
    },
  },

  // Custom blocks (see schema.md)
  customBlocks: [...],

  // Extend built-in blocks with new fields
  blockFieldExtensions: {...},

  // Override select field options
  blockFieldOptions: {...},

  // Content scaffolding
  content: {
    contentTypes: [...],
    pages: [...],
    entries: {...},
    navigation: {...},
  },

  // Sync settings
  sync: {
    existingEntries: 'update',
  },
});
```

### Environment Variables

Local development (default):

```bash
RIVERBANK_LOCAL_SITE_ID=your-site-id
RIVERBANK_LOCAL_DASHBOARD_URL=http://localhost:4000
RIVERBANK_LOCAL_MGMT_API_KEY=bld_mgmt_sk_...
```

Production (`--env=remote`):

```bash
RIVERBANK_REMOTE_SITE_ID=your-site-id
RIVERBANK_REMOTE_DASHBOARD_URL=https://your-dashboard.riverbankcms.com
RIVERBANK_REMOTE_MGMT_API_KEY=bld_mgmt_sk_...
```

---

## Related Documentation

- `cli-reference.md` - Full CLI command reference
- `schema.md` - This site's content types and custom blocks
- `block-types.md` - Available block types
- `workflows/` - Step-by-step guides for common tasks, including editor workflows
