시드 파일은 EmDash 사이트를 부트스트랩하는 JSON 문서입니다. 컬렉션, 필드, 택소노미, 메뉴, 리디렉션, 위젯 영역, 사이트 설정, 선택적 샘플 콘텐츠를 정의합니다.
루트 구조
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {},
"settings": {},
"collections": [],
"taxonomies": [],
"bylines": [],
"menus": [],
"redirects": [],
"widgetAreas": [],
"sections": [],
"content": {}
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
$schema | string | 아니오 | 에디터 유효성 검사용 JSON 스키마 URL |
version | "1" | 예 | 시드 형식 버전 |
meta | object | 아니오 | 시드에 대한 메타데이터 |
settings | object | 아니오 | 사이트 설정 |
collections | array | 아니오 | 컬렉션 정의 |
taxonomies | array | 아니오 | 택소노미 정의 |
bylines | array | 아니오 | 바이라인 프로필 정의 |
menus | array | 아니오 | 내비게이션 메뉴 |
redirects | array | 아니오 | 리디렉션 규칙 |
widgetAreas | array | 아니오 | 위젯 영역 정의 |
sections | array | 아니오 | 재사용 가능한 콘텐츠 블록 |
content | object | 아니오 | 샘플 콘텐츠 항목 |
메타
시드에 대한 선택적 메타데이터:
{
"meta": {
"name": "Blog Starter",
"description": "A simple blog with posts, pages, and categories",
"author": "EmDash"
}
}
설정
사이트 전체 구성 값:
{
"settings": {
"title": "My Site",
"tagline": "A modern CMS",
"postsPerPage": 10,
"dateFormat": "MMMM d, yyyy"
}
}
설정은 site: 접두사와 함께 options 테이블에 적용됩니다. 설정 마법사에서 사용자가 title과 tagline을 재정의할 수 있습니다.
컬렉션
컬렉션 정의는 데이터베이스에 콘텐츠 타입을 생성합니다:
{
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"description": "Blog posts",
"icon": "file-text",
"supports": ["drafts", "revisions"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true
},
{
"slug": "content",
"label": "Content",
"type": "portableText"
},
{
"slug": "featured_image",
"label": "Featured Image",
"type": "image"
}
]
}
]
}
컬렉션 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
slug | string | 예 | URL에 안전한 식별자 (소문자, 밑줄) |
label | string | 예 | 복수형 표시 이름 |
labelSingular | string | 아니오 | 단수형 표시 이름 |
description | string | 아니오 | 관리 UI 설명 |
icon | string | 아니오 | Lucide 아이콘 이름 |
supports | array | 아니오 | 기능: "drafts", "revisions" |
fields | array | 예 | 필드 정의 |
필드 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
slug | string | 예 | 열 이름 (소문자, 밑줄) |
label | string | 예 | 표시 이름 |
type | string | 예 | 필드 타입 |
required | boolean | 아니오 | 유효성 검사: 필드에 값이 있어야 함 |
unique | boolean | 아니오 | 유효성 검사: 값이 고유해야 함 |
defaultValue | any | 아니오 | 새 항목의 기본값 |
validation | object | 아니오 | 추가 유효성 검사 규칙 |
widget | string | 아니오 | 관리 UI 위젯 재정의 |
options | object | 아니오 | 위젯별 구성 |
필드 타입
| 타입 | 설명 | 저장 형태 |
|---|---|---|
string | 짧은 텍스트 | TEXT |
text | 긴 텍스트 (textarea) | TEXT |
number | 숫자 값 | REAL |
integer | 정수 | INTEGER |
boolean | 참/거짓 | INTEGER |
date | 날짜 값 | TEXT (ISO 8601) |
datetime | 날짜와 시간 | TEXT (ISO 8601) |
email | 이메일 주소 | TEXT |
url | URL | TEXT |
slug | URL에 안전한 문자열 | TEXT |
portableText | 리치 텍스트 콘텐츠 | JSON |
image | 이미지 참조 | JSON |
file | 파일 참조 | JSON |
json | 임의 JSON | JSON |
reference | 다른 항목에 대한 참조 | TEXT |
택소노미
콘텐츠 분류 시스템:
{
"taxonomies": [
{
"name": "category",
"label": "Categories",
"labelSingular": "Category",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "News" },
{ "slug": "tutorials", "label": "Tutorials" },
{
"slug": "advanced",
"label": "Advanced Tutorials",
"parent": "tutorials"
}
]
},
{
"name": "tag",
"label": "Tags",
"labelSingular": "Tag",
"hierarchical": false,
"collections": ["posts"]
}
]
}
택소노미 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
name | string | 예 | 고유 식별자 |
label | string | 예 | 복수형 표시 이름 |
labelSingular | string | 아니오 | 단수형 표시 이름 |
hierarchical | boolean | 예 | 중첩된 용어 허용 (카테고리) 또는 플랫 (태그) |
collections | array | 예 | 이 택소노미가 적용되는 컬렉션 |
terms | array | 아니오 | 미리 정의된 용어 |
용어 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
slug | string | 예 | URL에 안전한 식별자 |
label | string | 예 | 표시 이름 |
description | string | 아니오 | 용어 설명 |
parent | string | 아니오 | 부모 용어 슬러그 (계층적인 경우만) |
메뉴
관리에서 편집 가능한 내비게이션 메뉴:
{
"menus": [
{
"name": "primary",
"label": "Primary Navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "page", "ref": "about" },
{ "type": "custom", "label": "Blog", "url": "/posts" },
{
"type": "custom",
"label": "External",
"url": "https://example.com",
"target": "_blank"
}
]
}
]
}
메뉴 항목 타입
| 타입 | 설명 | 필수 필드 |
|---|---|---|
custom | 사용자 정의 URL | url |
page | 페이지 항목 링크 | ref |
post | 포스트 항목 링크 | ref |
taxonomy | 택소노미 아카이브 링크 | ref, collection |
collection | 컬렉션 아카이브 링크 | collection |
메뉴 항목 속성
| 속성 | 타입 | 설명 |
|---|---|---|
type | string | 항목 타입 (위 참조) |
label | string | 표시 텍스트 (page/post 참조의 경우 자동 생성) |
url | string | 사용자 정의 URL (custom 타입용) |
ref | string | 시드의 콘텐츠 ID (page/post 타입용) |
collection | string | 컬렉션 슬러그 |
target | string | 새 창을 위한 "_blank" |
titleAttr | string | HTML title 속성 |
cssClasses | string | 사용자 정의 CSS 클래스 |
children | array | 중첩된 메뉴 항목 |
바이라인
바이라인 프로필은 소유권(author_id)과 별개입니다. 재사용 가능한 바이라인 아이덴티티를 한 번 정의하고 콘텐츠 항목에서 참조합니다.
{
"bylines": [
{
"id": "editorial",
"slug": "emdash-editorial",
"displayName": "EmDash Editorial"
},
{
"id": "guest",
"slug": "guest-contributor",
"displayName": "Guest Contributor",
"isGuest": true
}
]
}
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | string | 예 | content[].bylines에서 사용하는 시드 로컬 ID |
slug | string | 예 | URL에 안전한 바이라인 슬러그 |
displayName | string | 예 | 템플릿과 API에 표시되는 이름 |
bio | string | 아니오 | 선택적 프로필 소개 |
websiteUrl | string | 아니오 | 선택적 웹사이트 URL |
isGuest | boolean | 아니오 | 게스트 프로필로 표시 |
리디렉션
마이그레이션 후 레거시 URL을 보존하기 위한 리디렉션 규칙:
{
"redirects": [
{ "source": "/old-about", "destination": "/about" },
{ "source": "/legacy-feed", "destination": "/rss.xml", "type": 308 },
{
"source": "/category/news",
"destination": "/categories/news",
"groupName": "migration"
}
]
}
리디렉션 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
source | string | 예 | 소스 경로 (/로 시작해야 함) |
destination | string | 예 | 대상 경로 (/로 시작해야 함) |
type | number | 아니오 | HTTP 상태: 301, 302, 307 또는 308 |
enabled | boolean | 아니오 | 리디렉션 활성 여부 (기본값: true) |
groupName | string | 아니오 | 관리 필터링/검색을 위한 선택적 그룹 라벨 |
위젯 영역
구성 가능한 콘텐츠 영역:
{
"widgetAreas": [
{
"name": "sidebar",
"label": "Main Sidebar",
"description": "Appears on blog posts and pages",
"widgets": [
{
"type": "component",
"title": "Recent Posts",
"componentId": "core:recent-posts",
"props": { "count": 5 }
},
{
"type": "menu",
"title": "Quick Links",
"menuName": "footer"
},
{
"type": "content",
"title": "About",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Welcome to our site!" }]
}
]
}
]
}
]
}
위젯 타입
| 타입 | 설명 | 필수 필드 |
|---|---|---|
content | 리치 텍스트 콘텐츠 | content (Portable Text) |
menu | 메뉴 렌더링 | menuName |
component | 등록된 컴포넌트 | componentId |
내장 컴포넌트
| 컴포넌트 ID | 설명 |
|---|---|
core:recent-posts | 최근 포스트 목록 |
core:categories | 카테고리 목록 |
core:tags | 태그 클라우드 |
core:search | 검색 폼 |
core:archives | 월별 아카이브 |
섹션
편집자가 /section 슬래시 명령을 통해 Portable Text 필드에 삽입할 수 있는 재사용 가능한 콘텐츠 블록:
{
"sections": [
{
"slug": "hero-centered",
"title": "Centered Hero",
"description": "Full-width hero with centered heading and CTA button",
"keywords": ["hero", "banner", "header", "landing"],
"content": [
{
"_type": "block",
"style": "h1",
"children": [{ "_type": "span", "text": "Welcome to Our Site" }]
},
{
"_type": "block",
"children": [
{ "_type": "span", "text": "Your compelling tagline goes here." }
]
}
]
}
]
}
섹션 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
slug | string | 예 | URL에 안전한 식별자 |
title | string | 예 | 섹션 선택기에 표시되는 이름 |
description | string | 아니오 | 이 섹션을 사용할 시기 설명 |
keywords | array | 아니오 | 섹션 검색 용어 |
content | array | 예 | Portable Text 블록 |
source | string | 아니오 | "theme" (시드 기본값) 또는 "import" |
시드 파일의 섹션은 source: "theme"으로 표시되며 관리 UI에서 삭제할 수 없습니다. 편집자는 자체 섹션(source: "user")을 만들 수 있으며 콘텐츠 편집 시 모든 섹션 타입을 삽입할 수 있습니다.
콘텐츠
컬렉션별로 정리된 샘플 콘텐츠:
{
"content": {
"posts": [
{
"id": "hello-world",
"slug": "hello-world",
"status": "published",
"bylines": [
{ "byline": "editorial" },
{ "byline": "guest", "roleLabel": "Guest essay" }
],
"data": {
"title": "Hello World",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Welcome!" }]
}
],
"excerpt": "Your first post."
},
"taxonomies": {
"category": ["news"],
"tag": ["welcome", "first-post"]
}
}
],
"pages": [
{
"id": "about",
"slug": "about",
"status": "published",
"data": {
"title": "About Us",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "About page content." }]
}
]
}
}
]
}
}
콘텐츠 항목 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | string | 예 | 참조를 위한 시드 로컬 ID |
slug | string | 예 | URL 슬러그 |
status | string | 아니오 | "published" 또는 "draft" (기본값: "published") |
data | object | 예 | 필드 값 |
bylines | array | 아니오 | 정렬된 바이라인 크레딧 (byline, 선택적 roleLabel) |
taxonomies | object | 아니오 | 택소노미 이름별 용어 할당 |
콘텐츠 참조
$ref: 접두사를 사용하여 다른 콘텐츠 항목을 참조합니다:
{
"data": {
"related_posts": ["$ref:another-post", "$ref:third-post"]
}
}
$ref: 접두사는 시딩 중에 시드 ID를 데이터베이스 ID로 해석합니다.
미디어 참조
URL에서 이미지 포함:
{
"data": {
"featured_image": {
"$media": {
"url": "https://images.unsplash.com/photo-xxx",
"alt": "Description of the image",
"filename": "hero.jpg",
"caption": "Photo by Someone"
}
}
}
}
.emdash/media/에서 로컬 이미지 포함:
{
"data": {
"featured_image": {
"$media": {
"file": "hero.jpg",
"alt": "Description of the image"
}
}
}
}
미디어 속성
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
url | string | 예* | 다운로드할 원격 URL |
file | string | 예* | .emdash/media/의 로컬 파일명 |
alt | string | 아니오 | 접근성을 위한 대체 텍스트 |
filename | string | 아니오 | 파일명 재정의 |
caption | string | 아니오 | 미디어 캡션 |
*url 또는 file 중 하나가 필수이며, 둘 다는 안 됩니다.
프로그래밍 방식 시드 적용
CLI 도구나 스크립트를 위한 시드 API를 사용합니다:
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";
// 먼저 유효성 검사
const validation = validateSeed(seedData);
if (!validation.valid) {
console.error(validation.errors);
process.exit(1);
}
// 시드 적용
const result = await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip",
storage: myStorage,
baseUrl: "http://localhost:4321",
});
console.log(result);
// {
// collections: { created: 2, skipped: 0 },
// fields: { created: 8, skipped: 0 },
// taxonomies: { created: 2, terms: 5 },
// bylines: { created: 2, skipped: 0 },
// menus: { created: 1, items: 4 },
// redirects: { created: 3, skipped: 0 },
// widgetAreas: { created: 1, widgets: 3 },
// settings: { applied: 3 },
// content: { created: 3, skipped: 0 },
// media: { created: 2, skipped: 0 }
// }
적용 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
includeContent | boolean | false | 샘플 콘텐츠 항목 생성 |
onConflict | string | "skip" | "skip", "update" 또는 "error" |
mediaBasePath | string | — | 로컬 미디어 파일 기본 경로 |
storage | Storage | — | 미디어 업로드용 스토리지 어댑터 |
baseUrl | string | — | 미디어 URL의 기본 URL |
멱등성
시딩은 여러 번 실행해도 안전합니다. 엔티티 타입별 충돌 동작:
| 엔티티 | 동작 |
|---|---|
| 컬렉션 | 슬러그가 존재하면 건너뛰기 |
| 필드 | 컬렉션 + 슬러그가 존재하면 건너뛰기 |
| 택소노미 정의 | 이름이 존재하면 건너뛰기 |
| 택소노미 용어 | 이름 + 슬러그가 존재하면 건너뛰기 |
| 바이라인 프로필 | 슬러그가 존재하면 건너뛰기 |
| 메뉴 | 이름이 존재하면 건너뛰기 |
| 메뉴 항목 | 전체 교체 (메뉴가 재생성됨) |
| 리디렉션 | 소스가 존재하면 건너뛰기 |
| 위젯 영역 | 이름이 존재하면 건너뛰기 |
| 위젯 | 전체 교체 (영역이 재생성됨) |
| 섹션 | 슬러그가 존재하면 건너뛰기 |
| 설정 | 업데이트 (설정은 변경되도록 되어 있음) |
| 콘텐츠 | 컬렉션에 슬러그가 존재하면 건너뛰기 |
유효성 검사
시드 파일은 적용 전에 유효성이 검사됩니다:
import { validateSeed } from "emdash/seed";
const { valid, errors, warnings } = validateSeed(seedData);
if (!valid) {
errors.forEach((e) => console.error(e));
}
warnings.forEach((w) => console.warn(w));
유효성 검사 항목:
- 필수 필드 존재
- 슬러그가 명명 규칙을 따름 (소문자, 밑줄)
- 필드 타입이 유효
- 참조가 기존 콘텐츠를 가리킴
- 계층적 용어 부모가 존재
- 리디렉션 경로가 안전한 로컬 URL
- 리디렉션 소스가 고유
- 컬렉션 내 중복 슬러그 없음
CLI 명령
# 시드 파일 적용
npx emdash seed .emdash/seed.json
# 샘플 콘텐츠 없이 적용
npx emdash seed .emdash/seed.json --no-content
# 유효성 검사만
npx emdash seed .emdash/seed.json --validate
# 현재 스키마를 시드로 내보내기
npx emdash export-seed > seed.json
# 콘텐츠와 함께 내보내기
npx emdash export-seed --with-content > seed.json