223 lines
7.1 KiB
Markdown
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` |
|