블로그 만들기

이 페이지

EmDash 블로그 템플릿은 게시물, 페이지, 작성자, 카테고리, 태그, 검색, 댓글, 위젯, RSS 피드를 갖춘 동작하는 Astro 사이트를 제공합니다. 이 튜토리얼에서는 Cloudflare 버전을 만들고, 게시물 하나를 게시한 다음, 그 게시물이 템플릿 코드를 거치는 과정을 따라가 봅니다.

사전 요구 사항

시작하기 전에 Node.js 22.12 이상과 pnpm을 설치하세요.

Cloudflare 계정은 사이트를 배포할 때만 필요합니다. 로컬 개발 중에는 템플릿이 데이터베이스와 파일 스토리지의 로컬 버전을 컴퓨터에서 실행합니다.

블로그 스캐폴딩

다음 명령은 Cloudflare 블로그 템플릿으로 my-blog를 만들고 pnpm으로 의존성을 설치합니다.

npm create emdash@latest my-blog -- --template cloudflare:blog --pm pnpm --yes

스캐폴더는 EMDASH_ENCRYPTION_KEY가 들어 있는 로컬 .env 파일도 만듭니다. 생성된 .gitignore는 .env를 버전 관리에서 제외합니다. 의존성 설치에 실패했다고 명령이 알려 주면, 프로젝트 디렉터리로 이동해 pnpm install을 실행한 뒤 계속하세요.

로컬 개발 서버를 시작합니다.

cd my-blog
pnpm dev

터미널에 출력된 로컬 URL을 열고 /_emdash/admin을 엽니다. 처음 실행하는 경우라면 설정 화면을 완료하세요. 템플릿의 시드 데이터가 설정 중에 콘텐츠 모델과 샘플 콘텐츠를 만듭니다.

콘텐츠 모델 이해하기

템플릿은 seed/seed.json에 두 개의 컬렉션을 정의합니다.

  • posts는 supports를 통해 초안, 리비전, 검색, SEO를 활성화하고, 댓글은 commentsEnabled: true로 별도로 활성화합니다.
  • pages는 초안, 리비전, 검색을 지원합니다.

각 게시물에는 다음 커스텀 필드가 있습니다.

필드용도
title필수 게시물 제목
featured_image게시물과 함께 표시되는 선택적 이미지
contentPortable Text 본문
excerpt게시물 목록과 메타데이터 대체 값에 사용하는 짧은 텍스트

EmDash는 안정적인 콘텐츠 ID, 슬러그, 상태, 생성 및 수정 시각, 게시 시각 같은 시스템 필드를 추가합니다. 템플릿은 게시물용 category와 tag 택소노미와, 한 명 이상의 작성자를 표기할 수 있는 Byline도 정의합니다.

개발 서버는 이 스키마에서 emdash-env.d.ts를 생성합니다. 그 결과 getEmDashCollection("posts")가 반환하는 항목의 data 속성은 Post 타입이 됩니다.

첫 번째 게시물 게시하기

  1. 관리자 사이드바에서 Posts를 선택한 다음 Add New를 선택합니다.

  2. 제목을 입력합니다. EmDash가 제목을 바탕으로 슬러그를 제안합니다. 공개 URL에 다른 값이 필요하면 수정하세요.

  3. 요약을 추가하고 Content 편집기에서 본문을 작성합니다.

  4. 미디어 라이브러리에서 대표 이미지를 선택하거나 업로드합니다. 게시물에서 그 이미지가 하는 역할을 설명하는 대체 텍스트를 추가하세요.

  5. 설정 패널에서 Byline, 카테고리, 관련 태그를 지정합니다.

  6. Save를 선택합니다. 항목이 초안이 되고, 편집기에서 그 항목의 고정 URL이 열립니다.

  7. Preview를 선택하여 게시물 페이지를 확인합니다. 초안이 준비되면 편집기로 돌아가 Publish를 선택합니다.

로컬 사이트에서 /posts/your-post-slug를 엽니다. 게시물은 홈페이지와 게시물 아카이브에도 나타납니다. 나타나지 않으면 편집기에 Draft나 Scheduled가 아니라 Published로 표시되는지 확인하세요.

게시한 후에는 현재 게시물이 라이브 상태로 유지되는 동안 편집 내용이 새 초안으로 자동 저장됩니다. 수정한 초안이 현재 게시물을 대체해야 할 때 Publish changes를 선택하세요. 미리보기, 예약, 리비전, 편집 잠금은 콘텐츠 작성 가이드에서 설명합니다.

컬렉션 쿼리 추적하기

홈페이지와 게시물 아카이브는 서버 렌더링 중에 getEmDashCollection()을 호출합니다. 템플릿은 저장된 published_at 필드를 기준으로 데이터베이스에서 게시물을 정렬합니다.

---
import { getEmDashCollection, getTermsForEntries } from "emdash";

const { entries: posts, cacheHint } = await getEmDashCollection("posts", {
  orderBy: { published_at: "desc" },
});

if (Astro.cache?.enabled) Astro.cache.set(cacheHint);

const tagsByEntry = await getTermsForEntries(
  "posts",
  posts.map((post) => post.data.id),
  "tag",
);
---

컬렉션 쿼리는 기본적으로 게시된 항목을 반환합니다. 정렬에는 데이터베이스 필드 이름인 published_at을 사용합니다. 반환되는 publishedAt 속성은 렌더링에 쓰는 JavaScript Date입니다.

택소노미 헬퍼가 post.data.id를 받는 이유는 택소노미 지정이 안정적인 콘텐츠 ID에 속하기 때문입니다. 링크는 대신 post.id를 사용하는데, 이는 콘텐츠 로더가 만든 URL용 슬러그이기 때문입니다.

<a href={`/posts/${post.id}`}>
  <h2>{post.data.title}</h2>
  {post.data.excerpt && <p>{post.data.excerpt}</p>}
</a>

현재 템플릿은 게시물마다 한 번씩 쿼리하는 대신 getTermsForEntries()로 태그 조회를 일괄 처리합니다. Byline은 컬렉션 쿼리에 의해 이미 post.data.bylines에 포함되어 있습니다.

게시물 쿼리 추적하기

동적 게시물 라우트는 URL에서 슬러그를 읽어 getEmDashEntry()를 호출합니다. 다음 발췌는 핵심 쿼리와 렌더링 경로를 보여 주며, 전체 템플릿은 SEO, Byline, 댓글, 관련 게시물, 위젯도 처리합니다.

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");

const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);

if (error) return new Response("Unable to load post", { status: 500 });
if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<article>
  {post.data.featured_image && <Image image={post.data.featured_image} priority />}
  <h1>{post.data.title}</h1>
  <PortableText value={post.data.content} />
</article>

Image는 편집자가 선택한 미디어 값을 읽어 반응형 출력을 생성합니다. PortableText는 저장된 블록 데이터를 제목, 문단, 링크, 이미지, 코드 블록 및 그 밖의 지원되는 블록 타입으로 변환합니다.

두 블로그 템플릿 모두 astro.config.mjs에서 output: "server"를 설정합니다. 이 쿼리들은 요청이 렌더링될 때 실행되므로, 게시된 콘텐츠는 빌드 중에 만든 정적 라우트 목록에 의존하지 않습니다.

카테고리와 태그 사용하기

템플릿에는 각 카테고리와 태그용 아카이브 라우트가 포함되어 있습니다. 카테고리 라우트는 먼저 용어 슬러그를 해석한 다음 해당 택소노미로 게시물을 필터링합니다.

---
import { decodeSlug, getEmDashCollection, getTerm } from "emdash";

const slug = decodeSlug(Astro.params.slug);
const category = slug
  ? await getTerm("category", slug, { includeCounts: false })
  : null;

if (!category) return Astro.rewrite("/404");

const { entries: posts, error } = await getEmDashCollection("posts", {
  where: { category: category.slug },
  orderBy: { published_at: "desc" },
});

if (error) return new Response("Unable to load posts", { status: 500 });
---

tag 라우트는 getTerm("tag", slug)와 where: { tag: term.slug }를 사용하는 동일한 패턴입니다. 편집자는 관리자에서 용어와 지정을 관리하며, 계층형 카테고리, 평면 태그, 커스텀 택소노미는 택소노미 가이드에서 다룹니다.

getEmDashEntry()는 게시물에 지정된 용어를 포함하므로, 상세 라우트는 추가 쿼리 없이 이를 렌더링할 수 있습니다.

---
const categories = post.data.terms?.category ?? [];
const tags = post.data.terms?.tag ?? [];
---

{categories.map((category) => (
  <a href={`/category/${category.slug}`}>{category.label}</a>
))}
{tags.map((tag) => (
  <a href={`/tag/${tag.slug}`}>{tag.label}</a>
))}

아카이브 페이지네이션 추가하기

템플릿의 게시물 아카이브는 게시된 모든 게시물을 렌더링합니다. 아카이브가 커지면 limit을 추가하고, /posts/page/2 같은 번호 라우트에는 오프셋 페이지네이션을, Older posts 링크에는 커서 페이지네이션을 사용하세요. 항목 순서가 예기치 않게 바뀌지 않도록 페이지 간에 orderBy: { published_at: "desc" }를 그대로 유지하세요.

페이지네이션 예제는 두 방식을 모두 보여 주고 각각 언제 선택해야 하는지 설명합니다.

RSS 피드 확인하기

템플릿은 이미 /rss.xml을 제공합니다. 해당 엔드포인트는 getEmDashCollection()으로 최신 게시물 20개를 읽고, 각 게시 날짜를 포맷하며, 제목과 요약을 XML에 넣기 전에 이스케이프합니다. 사이트 제목과 태그라인도 EmDash 설정에서 읽습니다.

테스트 게시물을 게시한 후 /rss.xml을 열어 그 제목을 검색하세요. 사이트가 피드에서 프로덕션 절대 URL을 사용한다면 배포 전에 Astro의 site 옵션을 설정하세요. 로컬 개발 중에는 엔드포인트가 현재 요청의 오리진으로 대체됩니다.

이제 블로그에는 작성 워크플로, 런타임 게시물 페이지, 택소노미 아카이브, 미디어 렌더링, 피드가 갖춰졌습니다. 필터와 페이지네이션은 콘텐츠 쿼리에서, 에셋 편집과 사용 추적은 미디어 라이브러리에서 이어서 확인하세요. AI 어시스턴트로 게시물을 초안 작성하거나 편집하려면 AI 도구를 따르세요.