Files
2026-05-25 16:41:08 +07:00

223 lines
7.1 KiB
Markdown

---
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` |