---
name: emdash-integration-guide
description: Lessons learned when integrating EmDash CMS into an existing Astro static site — seed file pitfalls, template data access patterns, image/media handling fixes, and database reset procedures. Use when setting up EmDash CMS, debugging seed failures, or fixing 404 images.
---

# EmDash CMS Integration Guide

## Overview
This document summarizes the problems encountered and solutions when integrating EmDash CMS into an existing Astro static site, covering seed file setup, template design, and image/media handling.

---

## 1. Database & Seed File

### 1.1 Reserved Field Slugs

**Problem**: Adding `slug` or `published_at` as collection fields in `seed.json` causes the seed to fail. The `ec_blog` table is created but custom fields (like `excerpt`, `body`, `featured_image`) never get their columns added, resulting in errors like:
```
SqliteError: table ec_blog has no column named excerpt
```

**Root Cause**: EmDash has a `RESERVED_FIELD_SLUGS` set in `node_modules/emdash/src/schema/types.ts` that includes:
- `id`, `slug`, `status`, `author_id`, `primary_byline_id`
- `created_at`, `updated_at`, `published_at`, `scheduled_at`, `deleted_at`
- `version`, `live_revision_id`, `draft_revision_id`, `locale`, `translation_group`
- Plus runtime-hydrated fields: `terms`, `bylines`, `byline`

When `createField()` encounters a reserved slug, it **throws a SchemaError**, which stops the transaction and prevents all subsequent fields from being created.

**Fix**: Do NOT include reserved slugs in the `fields` array of your collection definition. The system already creates these columns automatically in the content table (e.g., `ec_blog`).

```json
// ❌ WRONG - slug and published_at are reserved
"fields": [
  { "slug": "title", "type": "string" },
  { "slug": "slug", "type": "slug" },
  { "slug": "excerpt", "type": "text" },
  { "slug": "published_at", "type": "datetime" }
]

// ✅ CORRECT - only define custom fields
"fields": [
  { "slug": "title", "type": "string" },
  { "slug": "excerpt", "type": "text" },
  { "slug": "body", "type": "portableText" },
  { "slug": "featured_image", "type": "image" },
  { "slug": "tags", "type": "string" }
]
```

### 1.2 Database Reset

**Problem**: The old database (`data.db`) caches schema and seed data. Changes to `seed.json` aren't reflected unless the database is fully deleted.

**Fix**: Remove all database files before restarting:
```bash
cd /path/to/project
rm -f data.db data.db-shm data.db-wal
npm run dev
```

The `npx emdash dev` command hardcodes port 4321 and ignores `astro.config.mjs`'s `server.port` setting. Always use `npm run dev` for daily development.

### 1.3 Seed Content Data Format

**Problem**: Content entries in the seed file must only include non-system fields in `data`. System fields like `slug`, `status`, `published_at` are stored as direct columns on the `ec_*` table — they go at the same level as `data`, not inside it.

```json
// ✅ CORRECT structure
{
  "id": "my-post",
  "slug": "my-slug",
  "status": "published",
  "data": {
    "title": "My Title",
    "excerpt": "Description",
    "featured_image": {
      "src": "/images/photo.jpg",
      "alt": "Photo description"
    },
    "body": [
      {
        "_type": "block",
        "style": "normal",
        "children": [
          { "_type": "span", "text": "Hello world" }
        ]
      }
    ],
    "tags": "news"
  }
}
```

---

## 2. Template Data Access

### 2.1 Date Field Name (published_at -> publishedAt)

**Problem**: In templates, `article.data.published_at` returns `undefined`, showing "Invalid Date".

**Root Cause**: EmDash's loader (`mapRowToData` in `node_modules/emdash/src/loader.ts`) maps system date columns to **camelCase** using an `INCLUDE_IN_DATA` map:
```js
const INCLUDE_IN_DATA = {
  published_at: "publishedAt",
  created_at: "createdAt",
  updated_at: "updatedAt",
  scheduled_at: "scheduledAt",
};
```

**Fix**: Use camelCase property names in templates:
```astro
{/* ❌ WRONG */}
<time datetime={article.data.published_at}>

{/* ✅ CORRECT */}
<time datetime={article.data.publishedAt}>
```

Note: `orderBy` in queries uses the actual column name (snake_case) because it's passed directly to SQL:
```js
orderBy: { published_at: 'desc' }  // ✅ column name for SQL
```

### 2.2 Featured Image URL

**Problem**: Images uploaded via the admin UI show as 404:
```
Failed to load resource: the server responded with a status of 404 (Not Found)
/_emdash/api/media/file/01KSETDVHCWSAWF8HM72DSD8KT
```

**Root Cause**: EmDash's `normalizeMediaValue()` function (in `node_modules/emdash/src/media/normalize.ts`) **strips the `src` property** from local media objects during save:
```js
if (provider === "local") {
    delete result.src;  // src is removed
}
```

The local media provider stores files with `storageKey = "{ulid}.{ext}"` (e.g., `01HM2xyz.jpg`), and the URL must include the file extension. The stored value becomes:
```json
{
  "id": "01HM2xyz",
  "provider": "local",
  "meta": { "storageKey": "01HM2xyz.jpg" }
  // NO "src" property!
}
```

**Fix**: Handle three possible formats when rendering images:

| Source | Format | URL Resolution |
|--------|--------|----------------|
| Seed data (plain path) | `"string/path.jpg"` | Use string directly |
| Seed data (object with src) | `{ src: "/images/pic.jpg" }` | Use `img.src` |
| Admin UI uploaded | `{ provider: "local", id: "xxx", meta: { storageKey: "xxx.jpg" } }` | Use `img.meta?.storageKey \|\| img.id` |

```astro
---
function getImageUrl(img) {
  if (typeof img === 'string') return img;
  if (img?.src) return img.src;
  if (img?.provider === 'local') {
    return `/_emdash/api/media/file/${img.meta?.storageKey || img.id}`;
  }
  return null;
}
---
<img src={getImageUrl(article.data.featured_image)} alt={article.data.title} />
```

---

## 3. Google OAuth Setup

### 3.1 Configuration

```js
import { google } from 'emdash/auth/providers/google'
emdash({ authProviders: [google()] })
```

### 3.2 Environment Variables

1. Go to https://console.cloud.google.com/apis/credentials
2. Create OAuth client ID -> Web application
3. Add Authorized redirect URI: `http://localhost:3100/_emdash/api/auth/callback/google`
4. Set env vars: `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`

---

## 4. Key Architecture Notes

### 4.1 Publish Flow

When a collection supports `"revisions"`:
- **Save** -> creates a **draft revision** in the `revisions` table
- **Publish** -> calls `syncDataColumns()` to copy draft data into the `ec_*` table columns
- Frontend reads from `ec_*` table, NOT revisions table
- You must click **Publish** for edits to appear on frontend

### 4.2 Field Storage

- Fields become **SQL columns** on `ec_{slug}` table
- Type mapping: `string`->`TEXT`, `text`->`TEXT`, `portableText`->`JSON`, `image`->`TEXT`
- Object values are JSON.stringify'd for storage, JSON.parsed on read

### 4.3 Media URL Pattern

Admin-uploaded images: `/_emdash/api/media/file/{storageKey}` where `storageKey = {ulid}.{ext}`

---

## 5. Quick Reference

| Task | Command |
|------|---------|
| Start dev server | `npm run dev` |
| Reset database | `rm -f data.db data.db-shm data.db-wal && npm run dev` |
| Admin UI | `/_emdash/admin` |
