EmDash는 Astro를 위해 특별히 구축된 CMS입니다. 데이터베이스 기반 콘텐츠, 세련된 관리 UI, WordPress 스타일 기능(메뉴, 위젯, 택소노미)으로 Astro 사이트를 확장하면서 기대하는 개발자 경험을 유지합니다.
Astro에 대해 알고 있는 모든 것이 그대로 적용됩니다. EmDash는 기존 Astro 워크플로우 위에 콘텐츠 관리를 추가합니다.
EmDash가 추가하는 것
EmDash는 파일 기반 Astro 사이트에 부족한 콘텐츠 관리 기능을 제공합니다:
| 기능 | 설명 |
|---|---|
| 관리 UI | /_emdash/admin에서 완전한 WYSIWYG 편집 인터페이스 |
| 데이터베이스 저장 | SQLite, libSQL, Cloudflare D1 또는 PostgreSQL에 콘텐츠 저장 |
| 미디어 라이브러리 | 이미지와 파일 업로드, 정리, 제공 |
| 내비게이션 메뉴 | 중첩 지원 드래그 앤 드롭 메뉴 관리 |
| 위젯 영역 | 동적 사이드바 및 푸터 영역 |
| 사이트 설정 | 글로벌 설정 (제목, 로고, 소셜 링크) |
| 택소노미 | 카테고리, 태그, 커스텀 택소노미 |
| 미리보기 시스템 | 초안 콘텐츠용 서명된 미리보기 URL |
| 리비전 | 콘텐츠 버전 이력 |
Astro Collections vs EmDash
Astro의 astro:content 컬렉션은 파일 기반이며 빌드 시 해석됩니다. EmDash 컬렉션은 데이터베이스 기반이며 런타임에 해석됩니다.
| Astro 컬렉션 | EmDash 컬렉션 | |
|---|---|---|
| 저장소 | src/content/의 Markdown/MDX 파일 | SQL 데이터베이스 (SQLite, libSQL, D1 또는 Postgres) |
| 편집 | 코드 에디터 | 관리 UI |
| 콘텐츠 형식 | 프론트매터가 있는 Markdown | Portable Text (구조화된 JSON) |
| 업데이트 | 리빌드 필요 | 즉시 (SSR) |
| 스키마 | content.config.ts의 Zod | 관리 화면에서 정의, 데이터베이스에 저장 |
| 적합한 용도 | 개발자가 관리하는 콘텐츠 | 편집자가 관리하는 콘텐츠 |
함께 사용하기
Astro 컬렉션과 EmDash는 공존할 수 있습니다. 개발자 콘텐츠(문서, 변경 로그)에는 Astro 컬렉션을, 편집자 콘텐츠(블로그 게시물, 페이지)에는 EmDash를 사용하세요:
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
// 파일에서 개발자 관리 문서
const docs = await getCollection("docs");
// 데이터베이스에서 편집자 관리 게시물
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---
설정
EmDash는 두 개의 설정 파일이 필요합니다.
Astro 통합
다음 설정은 서버 출력 모드에서 EmDash를 Astro 통합으로 등록합니다:
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server", // EmDash에 필수
integrations: [
react(), // 필수 — 관리 UI는 React 앱
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
Live Collections Loader
다음 파일은 EmDash를 라이브 콘텐츠 소스로 등록합니다:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
_emdash 컬렉션은 내부적으로 콘텐츠 유형(게시물, 페이지, 상품)으로 라우팅합니다.
콘텐츠 쿼리
EmDash는 Astro의 라이브 콘텐츠 컬렉션 패턴을 따르는 쿼리 함수를 제공하며, { entries, error } 또는 { entry, error }를 반환합니다:
EmDash
import { getEmDashCollection, getEmDashEntry } from "emdash";
// 게시된 모든 게시물 가져오기 - { entries, error } 반환
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// 슬러그로 단일 게시물 가져오기 - { entry, error, isPreview } 반환
const { entry: post } = await getEmDashEntry("posts", "my-post");
Astro
import { getCollection, getEntry } from "astro:content";
// 모든 블로그 항목 가져오기
const posts = await getCollection("blog");
// 슬러그로 단일 항목 가져오기
const post = await getEntry("blog", "my-post"); 필터링 옵션
getEmDashCollection은 Astro의 getCollection이 지원하지 않는 필터링을 지원합니다:
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // 최대 결과 수
where: { category: "news" }, // 택소노미 필터
});
콘텐츠 렌더링
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();
// 또는 개별 값 가져오기:
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 react from "@astrojs/react";
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
export default defineConfig({
integrations: [
react(),
emdash({
// ...
plugins: [seoPlugin({ generateSitemap: true })],
}),
],
});
definePlugin으로 커스텀 플러그인을 만드세요:
import { definePlugin } from "emdash";
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["content:read"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "추적 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 });
}
---
다음 단계
시작하기
5분 이내에 첫 번째 EmDash 사이트를 만드세요.
콘텐츠 쿼리
블로그 만들기
카테고리와 태그가 있는 완전한 블로그를 구축하세요.
Cloudflare에 배포
Workers에서 사이트를 프로덕션에 배포하세요.