Astro.js Content Collection 처음 세팅 — 마크다운 블로그 구조 만들기

Astro.js에서 마크다운 파일을 블로그 포스트로 만드는 방법. content collection 설정과 frontmatter 스키마 정의.

Astro.js Content Collection 처음 세팅 — 마크다운 블로그 구조 만들기

Content Collection이 뭔가

Astro.js에서 마크다운(.md) 파일들을 타입 안전하게 관리하는 방법입니다.

src/content/picks/ 폴더에 .md 파일을 넣으면 getCollection('picks')로 전체 목록을 가져올 수 있습니다.


폴더 구조

src/
├── content/
│   ├── config.ts       ← 스키마 정의 (필수)
│   ├── picks/          ← picks 컬렉션
│   │   ├── 2026-05-27-post-1.md
│   │   └── 2026-05-27-post-2.md
│   └── news/           ← news 컬렉션 (별도)
│       └── 2026-05-27-news-1.md
└── pages/
    └── picks/
        ├── index.astro ← 목록 페이지
        └── [...slug].astro ← 개별 포스트 페이지

config.ts — 스키마 정의

src/content/config.ts:

import { defineCollection, z } from 'astro:content';

const picksCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    pubDate: z.date().or(z.string()).optional(),
    description: z.string().optional(),
    tags: z.array(z.string()).optional(),
    target_keyword: z.string().optional(),
    post_type: z.string().optional(),
    hidden: z.boolean().optional(),
  }),
});

export const collections = {
  'picks': picksCollection,
};

z는 Zod 스키마 라이브러리입니다. Astro가 포함해서 줍니다.


마크다운 파일 형식

---
title: "포스트 제목"
pubDate: 2026-05-27
description: "설명"
tags: ["태그1", "태그2"]
post_type: "review"
---

## 본문 시작

내용...

--- 사이의 YAML이 frontmatter입니다. config.ts 스키마와 일치해야 합니다.


목록 페이지 — index.astro

src/pages/picks/index.astro:

---
import { getCollection } from 'astro:content';

const allPicks = await getCollection('picks');

// 날짜 기준 정렬 (최신순)
const sortedPicks = allPicks.sort((a, b) => {
  const dateA = a.data.pubDate ? new Date(a.data.pubDate).getTime() : 0;
  const dateB = b.data.pubDate ? new Date(b.data.pubDate).getTime() : 0;
  return dateB - dateA;
});
---

<ul>
  {sortedPicks.map(pick => (
    <li>
      <a href={`/picks/${pick.slug}/`}>{pick.data.title}</a>
    </li>
  ))}
</ul>

pick.data로 frontmatter에 접근합니다. pick.slug이 파일명 기반 URL 슬러그입니다.


개별 포스트 페이지 — […slug].astro

src/pages/picks/[...slug].astro:

---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const allPicks = await getCollection('picks');
  return allPicks.map(pick => ({
    params: { slug: pick.slug },
    props: { pick },
  }));
}

const { pick } = Astro.props;
const { Content } = await pick.render();
---

<article>
  <h1>{pick.data.title}</h1>
  <Content />
</article>

getStaticPaths()가 모든 포스트의 URL 경로를 미리 생성합니다. Astro가 정적 사이트를 빌드할 때 이 경로들을 전부 HTML로 만듭니다.


파일명 규칙

파일명이 곧 URL 슬러그가 됩니다.

2026-05-27-my-post.md
→ /picks/2026-05-27-my-post/

한글 파일명도 됩니다:

2026-05-27-파이썬-자동화.md
→ /picks/2026-05-27-파이썬-자동화/

단, 한글이 포함된 파일명은 Git에서 URL 인코딩됩니다. 터미널에서 다루기 번거롭습니다. 영어 슬러그로 만들고 제목만 한글로 하는 방법도 있습니다.


겪었던 문제

pubDate 타입 불일치

frontmatter에 pubDate: 2026-05-27으로 쓰면 YAML이 날짜 타입으로 파싱합니다. TypeScript에서는 Date 객체.

정렬 코드에서 new Date(a.data.pubDate) 형변환이 필요합니다.

스키마에서 z.date().or(z.string()) 으로 둘 다 허용하면 편합니다.

스키마에 없는 필드

frontmatter에 스키마에 없는 필드가 있으면 빌드 에러가 납니다.

Error: Frontmatter validation error: Unrecognized key(s) in object: 'my_custom_field'

config.ts 스키마에 해당 필드를 추가하거나 .optional()로 선언합니다.

BOM 인코딩 이슈

PowerShell로 마크다운 파일을 생성할 때 BOM(UTF-8 BOM)이 붙으면 YAML 파서가 ---를 인식 못합니다.

Error: Expected '---' frontmatter delimiter

해결: [System.IO.File]::WriteAllBytes(path, $utf8NoBom.GetBytes($content))로 BOM 없이 저장.

자세한 내용은 Astro frontmatter BOM 이슈 포스트 참조.


필터링

특정 포스트만 보이게 하려면 filter()를 씁니다:

// hidden 필드로 필터
const visiblePicks = allPicks.filter(p => !p.data.hidden);

// 또는 화이트리스트 방식
const VISIBLE = ["2026-05-27-post-1", "2026-05-27-post-2"];
const visiblePicks = allPicks.filter(p =>
  VISIBLE.includes(p.slug)
);

64개 AI 자동생성 포스트를 숨길 때 화이트리스트 방식을 썼습니다.

#Astro.js#Content Collection#마크다운#블로그#TypeScript