블로그 만들기

이 페이지

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에서 두 개의 컬렉션을 정의합니다:

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

각 게시물에는 다음과 같은 커스텀 필드가 있습니다:

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

EmDash는 안정적인 콘텐츠 ID, 슬러그, 상태, 생성 및 업데이트 시간, 게시 시간 등의 시스템 필드를 추가합니다. 템플릿은 게시물용 categorytag 택소노미도 정의하며, 한 명 이상의 저자를 표기할 수 있는 저자 정보도 포함합니다.

개발 서버는 이 스키마에서 emdash-env.d.ts를 생성합니다. 그 결과, getEmDashCollection("posts")data 속성이 Post로 타입이 지정된 항목을 반환합니다.

첫 번째 게시물 게시하기

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

  2. 제목을 입력합니다. EmDash가 제목에서 슬러그를 제안합니다; 공개 URL에 다른 값이 필요하면 편집하세요.

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

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

  5. 설정 패널에서 저자 정보, 카테고리, 관련 태그를 할당합니다.

  6. Save를 선택합니다. 항목이 초안이 되고 편집기가 영구적인 항목 URL을 엽니다.

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

로컬 사이트에서 /posts/your-post-slug를 엽니다. 게시물은 홈 페이지와 게시물 아카이브에도 표시됩니다. 표시되지 않으면 편집기에서 DraftScheduled가 아닌 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()로 태그 조회를 일괄 처리합니다. 저자 정보는 컬렉션 쿼리에 의해 post.data.bylines에 이미 포함되어 있습니다.

게시물 쿼리 추적하기

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

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

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/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.redirect("/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.redirect("/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와 같은 번호가 매겨진 라우트에는 오프셋 페이지네이션을, 이전 게시물 링크에는 커서 페이지네이션을 사용하세요. 페이지 간에 orderBy: { published_at: "desc" }를 변경하지 마세요. 항목의 순서가 예기치 않게 변경되지 않도록 하기 위함입니다.

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

RSS 피드 확인하기

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

테스트 게시물을 게시한 후 /rss.xml을 열고 해당 제목을 검색하세요. 사이트가 피드에서 절대 프로덕션 URL을 사용할 경우, 배포 전에 Astro의 site 옵션을 설정하세요; 엔드포인트는 로컬 개발 중에는 현재 요청 오리진으로 폴백합니다.

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