Files
dealplustech-astroreal/EMDASH-SETUP-GUIDE.md
Kunthawat Greethong c8cf03a725 Add EmDash CMS blog + hero standardization + seed fix
- 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
2026-06-01 15:44:02 +07:00

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_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).

// ❌ 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

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

  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

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