
Astro 블로그에서 카테고리를 URL에 넣지 않고 관리하기
이 사이트는 Astro 공식 blog 템플릿으로 시작했다. 템플릿에는 분류 기능이 없고, 모든 글이 /blog/ 아래에 있다.
글을 HR, 게임, 노트 세 주제로 나누기로 하면서 주소를 어떻게 할지부터 정해야 했다. 여기에는 그때 정한 방식과, 지금 이 사이트에서 돌아가는 코드를 적어 둔다. 사이트를 처음 만든 과정은 구축기에 따로 있다.
주소에 주제를 넣는 방식은 버렸다
/hr/글이름/, /game/글이름/ 처럼 주제를 주소에 넣는 방식도 있다. 주소만 봐도 무슨 글인지 알 수 있다.
걸린 건 두 가지였다.
- 한 글이 두 주제에 걸칠 수 있다. 게임 회사의 근무 제도 같은 글은 어느 경로에 넣을지부터 애매하다.
- 분류는 나중에 바뀐다. 주제를 쪼개거나 합치면 주소가 바뀌고, 이미 검색엔진에 잡힌 옛 주소마다 리디렉션(옛 주소로 들어온 사람을 새 주소로 보내는 설정)을 걸어야 한다.
그래서 글 주소는 /blog/글이름/ 하나로 평평하게 두고, 분류는 글 안에 적는 값으로만 나눴다. 분류를 바꿔도 주소는 그대로다.
hr.heonote.com 같은 서브도메인(주소 앞에 붙는 별도 이름)으로 나누는 방법도 버렸다. 검색엔진은 서브도메인을 사실상 다른 사이트로 본다. 글이 적은 사이트에서 검색 유입이 세 곳으로 흩어지면 어느 쪽도 쌓이지 않는다.
분류는 글 머리에 한 줄로 적는다
마크다운 글 맨 위 --- 사이에는 제목, 날짜 같은 설정값을 적는다. 이 부분을 frontmatter(글 머리 설정)라고 한다. 여기에 category 한 줄을 추가했다.
---
title: '글 제목'
pubDate: 'Sep 15 2026'
category: 'hr'
---
어떤 값을 쓸 수 있는지는 src/content.config.ts 에서 정한다.
category: z.enum(['hr', 'game', 'notes']).default('notes'),
z.enum은 정해진 값 중 하나만 허용한다는 뜻이다. 적지 않으면 notes로 들어간다.
목록에 없는 값을 적으면 빌드가 멈춘다. hr을 hrr로 잘못 적으면 이런 에러가 나온다.
[InvalidContentEntryDataError] blog → zz-enum-test data does not match collection schema.
category: Invalid option: expected one of "hr"|"game"|"notes"
오타가 난 채로 배포되는 일은 없다. 글이 엉뚱한 분류로 들어가거나 어느 목록에도 안 보이는 상태를 빌드 단계에서 막아 준다.
이름과 주소를 따로 둔다
메뉴에 보이는 이름은 src/consts.ts 한 곳에서 관리한다.
export const CATEGORIES = [
{ slug: 'hr', name: 'HR', description: '인사·노무 실무에서 정리한 것들' },
{ slug: 'game', name: '게임', description: '게임 관련 정보와 기록' },
{ slug: 'notes', name: '노트', description: '읽고 정리하고 남겨두는 것들' },
] as const;
slug는 주소에 쓰이는 값이고 name은 화면에 보이는 이름이다. name은 언제 바꿔도 되지만 slug는 한번 정하면 바꾸지 않는다.
실제로 한 번 바꿨다. 노트 카테고리의 처음 이름은 ’잡다’였다. name만 ’노트’로 고쳤고, slug는 notes 그대로라 주소는 하나도 바뀌지 않았다. 메뉴, 목록 페이지 제목, 설명이 한꺼번에 따라 바뀌었다.
목록 페이지 세 개를 파일 하나로 만든다
/hr/, /game/, /notes/ 페이지는 src/pages/[category]/index.astro 파일 하나가 만든다. 파일 이름의 대괄호는 그 자리에 여러 값이 들어간다는 표시다.
어떤 값으로 페이지를 만들지는 getStaticPaths(빌드할 때 만들 페이지 목록을 돌려주는 함수)에서 정한다.
export async function getStaticPaths() {
const posts = await getCollection('blog');
return CATEGORIES
// 글이 실제로 있는 카테고리만 페이지를 만든다
.filter((cat) => posts.some((post) => post.data.category === cat.slug))
.map((cat) => ({
params: { category: cat.slug },
props: {
category: cat,
posts: posts
.filter((post) => post.data.category === cat.slug)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()),
},
}));
}
params는 주소에 들어갈 값, props는 그 페이지에 넘길 데이터다. 카테고리마다 해당 글만 골라 최신순으로 넘긴다.
글이 없는 카테고리는 숨긴다
위 코드의 .filter 줄이 이 역할을 한다. 글이 한 편도 없는 카테고리는 페이지를 아예 만들지 않는다.
메뉴에도 같은 조건을 걸었다. src/components/Header.astro 에서 글이 있는 카테고리만 골라 링크를 만든다.
const posts = await getCollection('blog');
const activeCategories = CATEGORIES.filter((cat) =>
posts.some((post) => post.data.category === cat.slug),
);
메뉴에 “게임”이 있는데 눌렀더니 빈 페이지면 만들다 만 사이트로 보인다. 이 글을 쓰는 시점에는 게임 글이 없어서 메뉴에 게임이 없다. 첫 글을 올리면 그 빌드에서 메뉴와 페이지가 같이 생긴다.
페이지와 메뉴 두 곳 모두에 조건을 걸어야 한다. 한쪽만 걸면 메뉴에는 있는데 누르면 404가 나거나, 페이지는 있는데 메뉴에서 갈 수 없는 상태가 된다.
아직 남은 문제
카테고리 목록이 두 곳에 따로 있다. content.config.ts 의 z.enum 과 consts.ts 의 CATEGORIES 다.
새 카테고리를 z.enum 에만 추가하면 빌드는 통과한다. 그 카테고리로 글도 쓸 수 있다. 그런데 getStaticPaths 와 메뉴는 CATEGORIES 만 보기 때문에, 목록 페이지도 메뉴 링크도 생기지 않는다. 에러는 없는데 그 글은 전체 글 목록에서만 보이고 카테고리로는 찾아갈 수 없다.
지금은 두 파일에 “함께 고칠 것”이라는 주석을 달아 두는 것으로 버티고 있다. z.enum 이 CATEGORIES 에서 값을 읽어 오게 바꾸면 한 곳만 고치면 되는데, 아직 손대지 않았다.
2026년 9월, Astro 7.3 기준.