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
This commit is contained in:
217
EMDASH-SETUP-GUIDE.md
Normal file
217
EMDASH-SETUP-GUIDE.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# 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
|
||||
|
||||
### 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` |
|
||||
Reference in New Issue
Block a user