Astro 개발자를 위한 EmDash

이 페이지

EmDash는 Astro를 위해 특별히 구축된 CMS입니다. 데이터베이스 기반 콘텐츠, 세련된 관리 UI 및 WordPress 스타일 기능(메뉴, 위젯, 분류법)으로 Astro 사이트를 확장하면서 기대하는 개발자 경험을 유지합니다.

Astro에 대해 알고 있는 모든 것이 여전히 적용됩니다. EmDash는 기존 Astro 워크플로우 위에 콘텐츠 관리를 추가합니다.

EmDash가 추가하는 것

EmDash는 파일 기반 Astro 사이트에 부족한 콘텐츠 관리 기능을 제공합니다:

기능설명
관리 UI/_emdash/admin의 전체 WYSIWYG 편집 인터페이스
데이터베이스 저장소SQLite, libSQL, Cloudflare D1 또는 PostgreSQL에 저장된 콘텐츠
미디어 라이브러리이미지 및 파일 업로드, 정리 및 제공
탐색 메뉴중첩 기능이 있는 드래그 앤 드롭 메뉴 관리
위젯 영역동적 사이드바 및 푸터 영역
사이트 설정전역 구성(제목, 로고, 소셜 링크)
분류법카테고리, 태그 및 사용자 정의 분류법
미리보기 시스템초안 콘텐츠용 서명된 미리보기 URL
수정본콘텐츠 버전 기록

Astro 컬렉션 vs EmDash

Astro의 astro:content 컬렉션은 파일 기반이며 빌드 시간에 확인됩니다. EmDash 컬렉션은 데이터베이스 기반이며 런타임에 확인됩니다.

Astro 컬렉션EmDash 컬렉션
저장소src/content/의 Markdown/MDX 파일SQL 데이터베이스(SQLite, libSQL, D1 또는 Postgres)
편집코드 에디터관리 UI
콘텐츠 형식프론트매터가 있는 MarkdownPortable Text(구조화된 JSON)
업데이트재빌드 필요즉시(SSR)
스키마content.config.ts의 Zod관리자에서 정의되고 데이터베이스에 저장됨
최적 용도개발자 관리 콘텐츠편집자 관리 콘텐츠

함께 사용하기

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 통합

다음 구성은 서버 출력 모드에서 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",
			}),
		}),
	],
});

라이브 컬렉션 로더

다음 파일은 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";

// 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: ["content:read"],

	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 });
}
---

다음 단계