- Add EmDash CMS integration with SQLite and local storage - Add blog collection (seed/seed.json) with 3 sample posts - Add /บทความ list page and /บทความ/[slug] detail page - Add blog section to homepage - Fix reserved field slugs (slug, published_at removed from fields) - Fix date field mapping (publishedAt camelCase) - Fix featured image URL for admin-uploaded images (meta.storageKey) - Standardize all product page hero sections - Update navigation with 'บทความ' link - Configure Google OAuth provider
6.8 KiB
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_idcreated_at,updated_at,published_at,scheduled_at,deleted_atversion,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).
// ❌ 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:
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.
// ✅ 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:
const INCLUDE_IN_DATA = {
published_at: "publishedAt",
created_at: "createdAt",
updated_at: "updatedAt",
scheduled_at: "scheduledAt",
};
Fix: Use camelCase property names in templates:
{/* ❌ 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:
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:
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:
{
"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 |
---
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
import { google } from 'emdash/auth/providers/google'
emdash({ authProviders: [google()] })
3.2 Environment Variables
- Go to https://console.cloud.google.com/apis/credentials
- Create OAuth client ID -> Web application
- Add Authorized redirect URI:
http://localhost:3100/_emdash/api/auth/callback/google - Set env vars:
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET
4. Key Architecture Notes
4.1 Publish Flow
When a collection supports "revisions":
- Save -> creates a draft revision in the
revisionstable - Publish -> calls
syncDataColumns()to copy draft data into theec_*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
5.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 |