이 페이지는 EmDash 를 개발하는 사람들을 위한 것이며, EmDash로 사이트를 구축하는 사람들을 위한 것이 아닙니다. 내부 메커니즘을 문서화합니다 — 테이블 레이아웃, Astro 통합, 요청 경로, 코드 생성. 이 중 어느 것도 EmDash를 사용하는 데 필요하지 않습니다. 사이트를 구축하는 경우 대신 Architecture와 Content Model을 읽으세요.
Astro 통합
EmDash는 emdash 패키지의 Astro 통합으로 실행됩니다. 빌드 시:
-
Astro의
injectRouteAPI를 사용하여 관리자 SPA 및 REST API 라우트를 주입합니다. 사용자의 프로젝트에는 아무것도 복사되지 않습니다. 주입된 경로는 다음과 같습니다:경로 패턴 목적 /_emdash/admin/[...path]관리자 패널 SPA /_emdash/api/manifest관리자 매니페스트 (컬렉션, 플러그인) /_emdash/api/content/[collection]콘텐츠 항목 CRUD /_emdash/api/media/*미디어 라이브러리 작업 /_emdash/api/schema/*스키마 관리 /_emdash/api/settings사이트 설정 /_emdash/api/menus/*내비게이션 메뉴 /_emdash/api/taxonomies/*카테고리, 태그, 커스텀 분류체계 -
번들러가 구성 및 플러그인 코드를 해석하고 트리 셰이크할 수 있도록 가상 모듈을 생성합니다:
모듈 목적 virtual:emdash/config데이터베이스 및 스토리지 구성 virtual:emdash/dialect데이터베이스 방언 팩토리 virtual:emdash/plugin-admins플러그인 관리자 UI용 정적 임포트 -
Live Collections 로더를 제공하고, 마이그레이션을 관리하며, 스토리지 연결을 엽니다.
데이터베이스 우선 스키마
스키마 정의는 코드가 아닌 데이터베이스에 존재합니다. 두 개의 시스템 테이블이 구조를 추적합니다.
_emdash_collections는 컬렉션당 한 행을 보유합니다:
CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "posts", "products"
label TEXT NOT NULL, -- "Blog Posts"
label_singular TEXT, -- "Post"
description TEXT,
icon TEXT,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- how it was created
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
source 열은 출처를 기록합니다: manual (관리자 UI), template:<name> (시드 파일), import:wordpress (가져오기), 또는 discovered (기존 테이블에서 자동 감지됨).
_emdash_fields는 필드당 한 행을 보유하며, 해당 컬렉션에 연결됩니다:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- column name
label TEXT NOT NULL,
type TEXT NOT NULL, -- field type
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
컬렉션별 콘텐츠 테이블
각 컬렉션은 ec_ 접두사가 붙은 자체 테이블을 가집니다. title 및 price 필드가 있는 products 컬렉션은 다음을 생성합니다:
CREATE TABLE ec_products (
-- System columns, always present
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft',
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT, -- soft delete
version INTEGER DEFAULT 1, -- optimistic locking
-- Content columns, from field definitions
title TEXT NOT NULL,
price REAL
);
실제 열 (JSON blob이 있는 하나의 테이블이 아닌)은 적절한 인덱싱, 작동하는 외래 키, 데이터베이스 도구가 검사할 수 있는 스키마, 필드별 JSON 파싱이 없음을 제공합니다.
관심사는 분리된 상태로 유지됩니다:
| 관심사 | 위치 | 테이블 |
|---|---|---|
| 스키마 | 시스템 테이블 | _emdash_collections, _emdash_fields |
| 콘텐츠 | 컬렉션별 테이블 | ec_posts, ec_products, … |
| 미디어 | 별도 테이블 + 스토리지 | media 테이블 + R2/S3 |
| 설정 | 옵션 테이블 | site: 접두사가 있는 options |
런타임 스키마 변경
관리자 UI를 통해 필드를 추가하면 세 단계가 실행됩니다:
_emdash_fields에 레코드를 삽입합니다.ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>을 실행합니다.- 유효성 검사에 사용되는 Zod 스키마를 재생성합니다.
SQLite는 런타임에 열 추가, 이름 바꾸기 및 삭제 (삭제는 SQLite 3.35+ 필요)를 지원합니다. 열 유형 변경은 제자리에서 지원되지 않으므로 EmDash는 테이블을 투명하게 재구성합니다: 새 테이블 생성, 행 복사, 이전 테이블 삭제, 새 테이블 이름 바꾸기.
런타임 유효성 검사
EmDash는 시작 시 필드 정의에서 Zod 스키마를 구축하고 모든 생성 및 업데이트를 이에 대해 유효성 검사합니다:
function buildSchema(fields: Field[]): ZodSchema {
const shape: Record<string, ZodType> = {};
for (const field of fields) {
let zodType = fieldTypeToZod(field.type);
if (field.required) zodType = zodType.required();
if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
shape[field.slug] = zodType;
}
return z.object(shape);
}
데이터 레이어
EmDash는 지원되는 모든 데이터베이스 (SQLite, libSQL, Cloudflare D1 및 PostgreSQL)에서 타입 안전 SQL을 위해 Kysely를 사용합니다. 방언은 사이트가 통합에 전달하는 구성에서 virtual:emdash/dialect에 의해 선택됩니다.
Live Collections 로더
콘텐츠는 Astro의 Live Collections를 통해 런타임에 제공됩니다. emdashLoader()는 Astro의 LiveLoader 인터페이스를 구현하고 단일 _emdash 컬렉션으로 등록됩니다:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
단일 _emdash 컬렉션은 모든 콘텐츠 유형을 래핑합니다. getEmDashCollection("posts")가 호출되면 로더는 유형별로 필터링합니다.
요청 경로
페이지로부터의 콘텐츠 요청:
- Astro가 요청을 수신하고 페이지 컴포넌트를 실행합니다.
getEmDashCollection()이 Astro의getLiveCollection()을 호출합니다.emdashLoader가 Kysely를 통해 관련ec_*테이블을 쿼리합니다.- 행이 Astro의 항목 형식 (
id,slug,data)으로 매핑됩니다. - 컴포넌트가 렌더링됩니다.
관리자 요청:
- 미들웨어가 세션 토큰을 검증합니다.
- API 라우트가 저장소를 통해 CRUD를 실행합니다.
- 라이프사이클 훅이 실행됩니다 (예:
content:beforeSave). - Kysely가 SQL을 실행합니다.
- 라우트가 관리자 SPA에 JSON을 반환합니다.
관리자 패널 내부 구조
관리자는 하나의 React 아일랜드입니다. Astro는 셸을 제공하고 미들웨어에서 인증을 적용합니다. 내부의 모든 것은 클라이언트 사이드이며 TanStack Router, TanStack Query, TanStack Table, React Hook Form + Zod, TipTap 및 Kumo (Cloudflare의 Base UI + Tailwind 디자인 시스템)를 기반으로 구축되었습니다.
셸 라우트는 미들웨어에서 액세스를 제어합니다:
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) return redirect("/_emdash/admin/login");
locals.user = session.user;
}
return next();
}
매니페스트 기반 UI
관리자는 컬렉션이나 플러그인에 대해 하드코딩하지 않습니다. GET /_emdash/api/manifest를 가져오며, 요청 사용자가 액세스할 수 있는 컬렉션, 플러그인 및 분류체계를 역할별로 필터링하여 반환합니다:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
내비게이션, 폼 및 필드 편집기는 이 매니페스트에서 생성되므로 스키마 및 플러그인 변경 사항이 관리자 재구성 없이 나타나고 Zod 스키마는 서버 사이드에 남습니다.
플러그인 관리자 UI
플러그인 관리자 진입점은 번들러가 해석하고 트리 셰이크할 수 있도록 정적 임포트의 생성된 가상 모듈에 수집됩니다:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
리치 텍스트 변환
Portable Text 필드는 TipTap (ProseMirror)에서 편집됩니다. 콘텐츠는 portableTextToProsemirror() 및 prosemirrorToPortableText()에 의해 로드 및 저장 경계에서 변환됩니다. 플러그인이나 가져오기의 알 수 없는 블록은 읽기 전용 자리 표시자로 보존됩니다.
서명된 업로드
미디어 업로드는 스토리지로의 직접 서명된 URL을 사용하여 Worker 본문 크기 제한을 우회합니다:
- 클라이언트가 업로드 URL을 요청합니다 (
POST /api/media/upload-url). - 클라이언트가 서명된 URL에 직접 업로드합니다 (R2 또는 S3).
- 클라이언트가 확인합니다 (
POST /api/media/:id/confirm). - 서버가 메타데이터 (크기, MIME 유형)를 추출합니다.
콘텐츠 가져오기 확장
WordPress 가져오기는 플러그 가능한 ImportSource 인터페이스를 기반으로 구축되었습니다. 사용자 지정 소스는 probe, analyze 및 fetch를 구현합니다:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe는 입력을 검증하고 발견한 내용을 보고하며, analyze는 소스 게시물 유형을 EmDash 컬렉션에 매핑하고 스키마 격차를 표시하며, fetchContent는 관리자가 사용하는 것과 동일한 저장소를 통해 가져오기 파이프라인이 작성하는 정규화된 항목을 스트리밍합니다. 기본 제공 소스는 WordPress WXR, WordPress.com 및 WordPress REST API를 다룹니다. 다른 시스템에서 가져오려면 사용자 지정 소스를 등록하십시오.