EmDash는 Astro 전용으로 만든 CMS이며, Astro 어댑터가 붙은 일반 헤드리스 CMS가 아닙니다. 데이터베이스-backed 콘텐츠, 다듬어진 관리 UI, WordPress 스타일 기능(메뉴, 위젯, 분류체계)으로 Astro 사이트를 확장하면서도 기대하는 개발 경험을 유지합니다.
Astro에 대해 아는 내용은 그대로 적용됩니다. EmDash는 사이트를 보강할 뿐, 워크플로를 대체하지 않습니다.
EmDash가 더하는 것
파일 기반 Astro 사이트에 부족한 콘텐츠 관리 기능을 EmDash가 제공합니다.
| 기능 | 설명 |
|---|---|
| Admin UI | /_emdash/admin의 전체 WYSIWYG 편집 UI |
| Database storage | SQLite, libSQL 또는 Cloudflare D1에 콘텐츠 저장 |
| Media library | 이미지와 파일 업로드·정리·제공 |
| Navigation menus | 드래그 앤 드롭과 중첩이 있는 메뉴 관리 |
| Widget areas | 동적 사이드바와 푸터 영역 |
| Site settings | 전역 설정(제목, 로고, 소셜 링크) |
| Taxonomies | 카테고리, 태그, 사용자 정의 분류체계 |
| Preview system | 초안용 서명된 미리보기 URL |
| Revisions | 콘텐츠 버전 기록 |
Astro Collections vs EmDash
Astro의 astro:content 컬렉션은 파일 기반이며 빌드 시점에 해석됩니다. EmDash 컬렉션은 데이터베이스-backed이며 런타임에 해석됩니다.
| Astro Collections | EmDash Collections | |
|---|---|---|
| Storage | src/content/의 Markdown/MDX 파일 | SQLite/D1 데이터베이스 |
| Editing | 코드 편집기 | Admin UI |
| Content format | 프론트매터가 있는 Markdown | Portable Text(구조화된 JSON) |
| Updates | 재빌드 필요 | 즉시(SSR) |
| Schema | content.config.ts의 Zod | 관리 UI에서 정의, DB에 저장 |
| Best for | 개발자가 관리하는 콘텐츠 | 편집자가 관리하는 콘텐츠 |
함께 사용하기
Astro 컬렉션과 EmDash는 공존할 수 있습니다. 개발자 콘텐츠(문서, 변경 로그)는 Astro, 편집 콘텐츠(블로그 글, 페이지)는 EmDash에 둡니다.
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
// Developer-managed docs from files
const docs = await getCollection("docs");
// Editor-managed posts from database
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---
구성
EmDash에는 구성 파일이 두 개 필요합니다.
Astro 통합
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server", // Required for EmDash
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
Live Collections Loader
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
이렇게 EmDash를 라이브 콘텐츠 소스로 등록합니다. _emdash 컬렉션은 내부적으로 콘텐츠 타입(posts, pages, products)으로 라우팅합니다.
콘텐츠 조회
EmDash는 Astro의 live content collections 패턴을 따르는 쿼리 함수를 제공하며 { entries, error } 또는 { entry, error }를 반환합니다.
EmDash
import { getEmDashCollection, getEmDashEntry } from "emdash";
// Get all published posts - returns { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Get a single post by slug - returns { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");
Astro
import { getCollection, getEntry } from "astro:content";
// Get all blog entries
const posts = await getCollection("blog");
// Get a single entry by slug
const post = await getEntry("blog", "my-post"); 필터 옵션
getEmDashCollection은 Astro의 getCollection에 없는 필터를 지원합니다.
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // max results
where: { category: "news" }, // taxonomy filter
});
콘텐츠 렌더링
EmDash는 리치 텍스트를 구조화된 JSON 형식인 Portable Text로 저장합니다. PortableText 컴포넌트로 렌더링합니다.
EmDash
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
if (!post) {
return Astro.redirect("/404");
}
---
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article> Astro
---
import { getEntry, render } from "astro:content";
const { slug } = Astro.params;
const post = await getEntry("blog", slug);
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article> 동적 기능
EmDash는 Astro 콘텐츠 레이어에 없는 WordPress 스타일 기능을 위한 API를 제공합니다.
내비게이션 메뉴
---
import { getMenu } from "emdash";
const primaryMenu = await getMenu("primary");
---
{primaryMenu && (
<nav>
<ul>
{primaryMenu.items.map(item => (
<li>
<a href={item.url}>{item.label}</a>
{item.children.length > 0 && (
<ul>
{item.children.map(child => (
<li><a href={child.url}>{child.label}</a></li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}
위젯 영역
---
import { getWidgetArea } from "emdash";
import { PortableText } from "emdash/ui";
const sidebar = await getWidgetArea("sidebar");
---
{sidebar && sidebar.widgets.length > 0 && (
<aside>
{sidebar.widgets.map(widget => (
<div class="widget">
{widget.title && <h3>{widget.title}</h3>}
{widget.type === "content" && widget.content && (
<PortableText value={widget.content} />
)}
</div>
))}
</aside>
)}
사이트 설정
---
import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();
// Or fetch individual values:
const title = await getSiteSetting("title");
---
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
{settings.tagline && <p>{settings.tagline}</p>}
</header>
플러그인
훅, 스토리지, 설정, 관리 UI를 추가하는 플러그인으로 EmDash를 확장합니다.
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
export default defineConfig({
integrations: [
emdash({
// ...
plugins: [seoPlugin({ generateSitemap: true })],
}),
],
});
definePlugin으로 사용자 정의 플러그인을 만듭니다.
import { definePlugin } from "emdash";
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["read:content"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
},
},
});
서버 렌더링
EmDash 사이트는 SSR 모드로 실행됩니다. 콘텐츠 변경은 리빌드 없이 바로 반영됩니다.
getStaticPaths가 있는 정적 페이지는 빌드 시점에 콘텐츠를 가져옵니다.
---
import { getEmDashCollection, getEmDashEntry } from "emdash";
export async function getStaticPaths() {
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
return posts.map((post) => ({
params: { slug: post.data.slug },
}));
}
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
---
동적 페이지는 prerender = false로 요청마다 콘텐츠를 가져옵니다.
---
export const prerender = false;
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Server error", { status: 500 });
}
if (!post) {
return new Response(null, { status: 404 });
}
---
다음 단계
Getting Started
5분 안에 첫 EmDash 사이트 만들기.
Querying Content
Create a Blog
카테고리와 태그가 있는 완전한 블로그 구축.
Deploy to Cloudflare
Workers에서 프로덕션 배포.