Seed 파일은 EmDash 사이트를 초기화하는 JSON 문서입니다. 컬렉션, 필드, 분류체계, 메뉴, 리디렉션, 위젯 영역, 사이트 설정 및 선택적 샘플 컨텐츠를 정의합니다.
루트 구조
Seed 파일은 다음과 같은 최상위 구조를 갖습니다:
{
"$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" | 예 | Seed 형식 버전 |
meta | object | 아니오 | Seed에 대한 메타데이터 |
settings | object | 아니오 | 사이트 설정 |
collections | array | 아니오 | 컬렉션 정의 |
taxonomies | array | 아니오 | 분류체계 정의 |
bylines | array | 아니오 | 바이라인 프로필 정의 |
menus | array | 아니오 | 네비게이션 메뉴 |
redirects | array | 아니오 | 리디렉션 규칙 |
widgetAreas | array | 아니오 | 위젯 영역 정의 |
sections | array | 아니오 | 재사용 가능한 컨텐츠 블록 |
content | object | 아니오 | 샘플 컨텐츠 항목 |
Meta
meta 객체는 seed에 대한 선택적 설명 메타데이터를 포함합니다:
{
"meta": {
"name": "Blog Starter",
"description": "A simple blog with posts, pages, and categories",
"author": "EmDash"
}
}
Settings
settings 객체는 사이트 전체 구성 값을 포함합니다:
{
"settings": {
"title": "My Site",
"tagline": "A modern CMS",
"postsPerPage": 10,
"dateFormat": "MMMM d, yyyy"
}
}
설정은 site: 접두사와 함께 options 테이블에 적용됩니다. 설정 마법사는 seed 파일에서 title과 tagline을 미리 채우며(제공된 경우), 사용자가 초기 설정 중에 이를 재정의할 수 있도록 합니다.
Collections
각 컬렉션 정의는 데이터베이스에 컨텐츠 유형을 생성합니다:
{
"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
분류체계는 컨텐츠를 위한 분류 시스템입니다:
{
"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
menus 배열은 관리자에서 편집 가능한 네비게이션 메뉴를 정의합니다:
{
"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 | 표시 텍스트 (페이지/게시물 참조의 경우 자동 생성) |
url | string | 사용자 정의 URL (custom 타입의 경우) |
ref | string | Seed의 컨텐츠 ID (page/post 타입의 경우) |
collection | string | 컬렉션 슬러그 |
target | string | 새 창의 경우 "_blank" |
titleAttr | string | HTML title 속성 |
cssClasses | string | 사용자 정의 CSS 클래스 |
children | array | 중첩된 메뉴 항목 |
Bylines
바이라인 프로필은 소유권(author_id)과 분리되어 있습니다. 재사용 가능한 바이라인 ID를 한 번 정의한 다음 컨텐츠 항목에서 참조합니다.
{
"bylines": [
{
"id": "editorial",
"slug": "emdash-editorial",
"displayName": "EmDash Editorial"
},
{
"id": "guest",
"slug": "guest-contributor",
"displayName": "Guest Contributor",
"isGuest": true
}
]
}
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | string | 예 | content[].bylines에서 사용하는 Seed 로컬 ID |
slug | string | 예 | URL 안전 바이라인 슬러그 |
displayName | string | 예 | 템플릿 및 API에 표시되는 이름 |
bio | string | 아니오 | 선택적 프로필 약력 |
websiteUrl | string | 아니오 | 선택적 웹사이트 URL |
isGuest | boolean | 아니오 | 바이라인을 게스트 프로필로 표시 |
Redirects
redirects 배열은 마이그레이션 후 레거시 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 | 아니오 | 관리자 필터링/검색을 위한 선택적 그룹화 레이블 |
Widget Areas
widgetAreas 배열은 구성 가능한 컨텐츠 영역을 정의합니다:
{
"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 | 월별 아카이브 |
Sections
섹션은 편집자가 /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" (seed의 기본값) 또는 "import" |
Seed 파일의 섹션은 source: "theme"으로 표시되며 관리 UI에서 삭제할 수 없습니다. 편집자는 자신만의 섹션(source: "user")을 만들 수 있으며 컨텐츠를 편집할 때 모든 섹션 타입을 삽입할 수 있습니다.
Content
content 객체는 컬렉션별로 구성된 샘플 컨텐츠 항목을 포함합니다:
{
"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 | 예 | 참조를 위한 Seed 로컬 ID |
slug | string | 예 | URL 슬러그 |
status | string | 아니오 | "published" 또는 "draft" (기본값: "published") |
data | object | 예 | 필드 값 |
bylines | array | 아니오 | 순서가 지정된 바이라인 크레딧 (byline, 선택적 roleLabel) |
taxonomies | object | 아니오 | 분류체계 이름별 용어 할당 |
Content References
$ref: 접두사를 사용하여 다른 컨텐츠 항목을 참조합니다:
{
"data": {
"related_posts": ["$ref:another-post", "$ref:third-post"]
}
}
$ref: 접두사는 시드 중에 Seed ID를 데이터베이스 ID로 해석합니다.
Media References
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 중 하나가 필요하며, 둘 다는 아닙니다.
Applying Seeds Programmatically
CLI 도구 또는 스크립트에 seed 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);
}
// Seed 적용
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 }
// }
Apply 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
includeContent | boolean | false | 샘플 컨텐츠 항목 생성 |
onConflict | string | "skip" | "skip", "update" 또는 "error" |
mediaBasePath | string | — | 로컬 미디어 파일의 기본 경로 |
storage | Storage | — | 미디어 업로드를 위한 스토리지 어댑터 |
baseUrl | string | — | 미디어 URL의 기본 URL |
Idempotency
시드는 여러 번 안전하게 실행할 수 있습니다. 엔티티 타입별 충돌 동작:
| 엔티티 | 동작 |
|---|---|
| 컬렉션 | 슬러그가 존재하면 건너뛰기 |
| 필드 | collection + 슬러그가 존재하면 건너뛰기 |
| 분류체계 정의 | 이름이 존재하면 건너뛰기 |
| 분류체계 용어 | 이름 + 슬러그가 존재하면 건너뛰기 |
| 바이라인 프로필 | 슬러그가 존재하면 건너뛰기 |
| 메뉴 | 이름이 존재하면 건너뛰기 |
| 메뉴 항목 | 모두 교체 (메뉴가 다시 생성됨) |
| 리디렉션 | 소스가 존재하면 건너뛰기 |
| 위젯 영역 | 이름이 존재하면 건너뛰기 |
| 위젯 | 모두 교체 (영역이 다시 생성됨) |
| 섹션 | 슬러그가 존재하면 건너뛰기 |
| 설정 | 업데이트 (설정은 변경되도록 의도됨) |
| 컨텐츠 | 컬렉션에 슬러그가 존재하면 건너뛰기 |
Validation
Seed 파일은 적용 전에 유효성이 검사됩니다:
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 Commands
.emdash/seed.json, package.json#emdash.seed 또는 seed/seed.json의 seed 파일은 빌드에 인라인되며 데이터베이스가 비어 있을 때 첫 번째 요청에 적용됩니다. 기존 사이트의 스키마(및 선택적으로 해당 컨텐츠)를 seed 파일로 내보내려면:
# 새 프로젝트에서는 .emdash/가 아직 존재하지 않을 수 있으므로 `mkdir -p`
mkdir -p .emdash
# 현재 스키마를 seed 파일로 내보내기
npx emdash export-seed > .emdash/seed.json
# 컨텐츠와 함께 내보내기
npx emdash export-seed --with-content > .emdash/seed.json
Next Steps
- Creating Themes — 완전한 테마 만들기
- Themes Overview — 테마 작동 방식