Astro.js Content Collection 처음 세팅 — 마크다운 블로그 구조 만들기
Astro.js에서 마크다운 파일을 블로그 포스트로 만드는 방법. content collection 설정과 frontmatter 스키마 정의.
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 자동생성 포스트를 숨길 때 화이트리스트 방식을 썼습니다.