Astro로 기술 블로그 만들기 | 콘텐츠 컬렉션·MDX·SEO·배포까지
이 글의 핵심
Astro로 기술 블로그를 만드는 전 과정을 다룹니다. Content Collections, MDX, 태그·검색·시리즈, RSS·Sitemap, OG 이미지, 다국어, SSR/SSG 선택, Cloudflare Pages 배포까지 실전 예제로 정리합니다.
들어가며
Astro는 콘텐츠 중심 사이트(블로그, 문서, 포트폴리오)에 최적화된 정적 사이트 생성기입니다. 기본이 Zero JavaScript라 빌드 시 HTML만 남고, 필요한 곳에만 React·Vue·Svelte 컴포넌트를 아일랜드(Islands)로 추가할 수 있습니다.
이 글에서는 Astro로 기술 블로그를 만드는 전 과정을 다룹니다: Content Collections, MDX, 태그·검색·시리즈, RSS·Sitemap, OG 이미지, 다국어, SSR/SSG 선택, 그리고 Cloudflare Pages 배포까지.
배포 후 검색 유입 구조는 기술 블로그 방문자 늘리기에서, Cloudflare Pages 설정은 Cloudflare Pages 완벽 가이드를 참고하세요.
목차
- Astro 프로젝트 시작
- Content Collections로 블로그 글 관리
- MDX로 컴포넌트 넣기
- 태그·카테고리·시리즈
- 검색 기능
- RSS·Sitemap·OG 이미지
- 다국어(i18n)
- SSR vs SSG 선택
- 배포 (Cloudflare Pages)
- 정리
1. Astro 프로젝트 시작
1-1. 프로젝트 생성
npm create astro@latest my-blog
cd my-blog
npm install
npm run dev
템플릿 선택: Blog 템플릿 선택 시 기본 구조 자동 생성.
1-2. 프로젝트 구조
my-blog/
├── src/
│ ├── content/
│ │ ├── blog/
│ │ │ ├── post-1.md
│ │ │ └── post-2.mdx
│ │ └── config.ts # Content Collections 스키마
│ ├── pages/
│ │ ├── index.astro
│ │ ├── blog/
│ │ │ ├── [slug].astro
│ │ │ └── tag/[tag].astro
│ │ └── rss.xml.ts
│ ├── components/
│ └── layouts/
├── public/
├── astro.config.mjs
└── package.json
2. Content Collections로 블로그 글 관리
Content Collections는 마크다운 파일을 타입 안전하게 관리하는 Astro의 핵심 기능입니다.
2-1. 스키마 정의
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
author: z.string().default('pkglog'),
readingMinutes: z.number().optional(),
relatedPosts: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
2-2. 마크다운 작성
---
title: 'Astro로 블로그 만들기'
description: 'Astro 블로그 시작 가이드'
pubDate: 2026-04-01
tags: ['Astro', '블로그', 'JAMstack']
draft: false
---
## 들어가며
Astro는 콘텐츠 중심 사이트에 최적화되어 있습니다.
2-3. 글 목록 가져오기
// src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog', ({ data }) => {
// draft 제외, 날짜 필터
return !data.draft && data.pubDate <= new Date();
});
// 최신순 정렬
const posts = allPosts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
</li>
))}
</ul>
2-4. 개별 글 페이지
// src/pages/blog/[slug].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString('ko-KR')}</time>
<Content />
</article>
타입 안전: post.data.title은 자동 완성되고, 스키마 위반 시 빌드 실패.
3. MDX로 컴포넌트 넣기
MDX는 마크다운 안에 JSX를 쓸 수 있게 해 줍니다.
3-1. MDX 설치
npm install @astrojs/mdx
// astro.config.mjs
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [mdx()],
});
3-2. MDX 파일 작성
---
title: '인터랙티브 예제'
pubDate: 2026-04-01
---
import Counter from '../../components/Counter.jsx';
## 카운터 예제
<Counter client:load />
일반 마크다운과 컴포넌트를 섞을 수 있습니다.
3-3. 컴포넌트 예제
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
클라이언트 디렉티브:
client:load: 페이지 로드 시 즉시client:idle: 브라우저 idle 시client:visible: 뷰포트 진입 시
4. 태그·카테고리·시리즈
4-1. 태그 페이지
// src/pages/blog/tag/[tag].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
const tags = [...new Set(posts.flatMap(p => p.data.tags))];
return tags.map(tag => ({
params: { tag },
props: {
posts: posts.filter(p => p.data.tags.includes(tag))
},
}));
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<h1>태그: {tag}</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.slug}/`}>{post.data.title}</a></li>
))}
</ul>
4-2. 시리즈 관리
// src/content/config.ts
const blog = defineCollection({
schema: z.object({
// ...
seriesId: z.string().optional(),
seriesOrder: z.number().optional(),
}),
});
// 시리즈 글 가져오기
const seriesPosts = allPosts
.filter(p => p.data.seriesId === 'algorithm')
.sort((a, b) => (a.data.seriesOrder || 0) - (b.data.seriesOrder || 0));
5. 검색 기능
5-1. 클라이언트 검색 (Fuse.js)
npm install fuse.js
// src/pages/search.astro
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
const searchData = posts.map(p => ({
slug: p.slug,
title: p.data.title,
description: p.data.description,
tags: p.data.tags,
}));
---
<script define:vars={{ searchData }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(searchData, {
keys: ['title', 'description', 'tags'],
threshold: 0.3,
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
// 결과 렌더링
});
</script>
<input id="search" type="text" placeholder="검색..." />
<div id="results"></div>
5-2. 서버 검색 (Pagefind)
npm install -D pagefind
// package.json
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}
빌드 후 자동으로 검색 인덱스 생성.
6. RSS·Sitemap·OG 이미지
6-1. RSS 피드
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: '내 블로그',
description: '기술 블로그',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDate,
link: `/blog/${post.slug}/`,
})),
});
}
6-2. Sitemap
// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [sitemap()],
});
빌드 시 dist/sitemap-index.xml 자동 생성.
6-3. OG 이미지 (Satori)
npm install satori sharp
// src/pages/og/[slug].png.ts
import { getCollection } from 'astro:content';
import satori from 'satori';
import sharp from 'sharp';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
export async function GET({ props }) {
const { post } = props;
const svg = await satori(
<div style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '80px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
}}>
<h1 style={{ fontSize: '64px', margin: 0 }}>{post.data.title}</h1>
<p style={{ fontSize: '32px', marginTop: '20px' }}>{post.data.description}</p>
</div>,
{
width: 1200,
height: 630,
fonts: [/* 폰트 로드 */],
}
);
const png = await sharp(Buffer.from(svg)).png().toBuffer();
return new Response(png, {
headers: { 'Content-Type': 'image/png' },
});
}
메타 태그:
<meta property="og:image" content={`https://example.com/og/${slug}.png`} />
7. 다국어(i18n)
7-1. 설정
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'ko',
locales: ['ko', 'en'],
routing: {
prefixDefaultLocale: false, // /ko/ 없이 /blog/
},
},
});
7-2. 언어별 폴더
src/content/blog/
├── my-post.md # 한국어
└── en/
└── my-post.md # 영어
7-3. 언어 전환
// src/utils/i18n.ts
export function getAlternateSlug(slug: string, locale: string) {
if (locale === 'en') return `en/${slug}`;
return slug.replace(/^en\//, '');
}
<link rel="alternate" hreflang="en" href={`/blog/${getAlternateSlug(slug, 'en')}/`} />
<link rel="alternate" hreflang="ko" href={`/blog/${slug}/`} />
8. SSR vs SSG 선택
Astro는 기본이 SSG(Static Site Generation)지만, 필요한 페이지만 SSR로 전환할 수 있습니다.
8-1. 전체 SSG (기본)
// astro.config.mjs
export default defineConfig({
output: 'static', // 기본값
});
빌드 시 모든 페이지가 HTML로 생성.
8-2. Hybrid (일부 SSR)
export default defineConfig({
output: 'hybrid',
adapter: cloudflare(), // 또는 node(), vercel()
});
// src/pages/api/views.ts
export const prerender = false; // 이 페이지만 SSR
export async function GET() {
const views = await getViewCount();
return new Response(JSON.stringify({ views }));
}
8-3. 전체 SSR
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});
모든 페이지가 요청마다 렌더링.
선택 기준:
- 블로그 글: SSG (빌드 시 HTML)
- 조회수·댓글: SSR API 또는 클라이언트 fetch
- 검색: 클라이언트 검색 또는 SSR 엔드포인트
9. 배포 (Cloudflare Pages)
9-1. GitHub 연동
- Cloudflare 대시보드 → Pages → Create a project
- GitHub 저장소 연결
- 빌드 설정:
- Framework preset: Astro
- Build command:
npm run build - Build output directory:
dist
9-2. Wrangler CLI
npm install -D wrangler
npm run build
wrangler pages deploy dist --project-name=my-blog
9-3. GitHub Actions
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install
run: npm ci
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-blog
자세한 배포 설정은 Cloudflare Pages 완벽 가이드를 참고하세요.
10. 실전 팁
10-1. 빌드 성능
대량 페이지 (1,000개+):
// astro.config.mjs
export default defineConfig({
build: {
concurrency: 16, // 병렬 렌더링
},
});
OG 이미지 캐시:
// scripts/generate-og-images.mjs
import { existsSync } from 'fs';
for (const post of posts) {
const ogPath = `public/og/${post.slug}.png`;
if (existsSync(ogPath)) {
console.log(`캐시 사용: ${post.slug}`);
continue;
}
// 생성 로직
}
10-2. 읽기 시간 계산
// src/utils/reading-time.ts
export function calculateReadingTime(content: string): number {
const wordsPerMinute = 200; // 한국어는 더 낮게
const words = content.split(/\s+/).length;
return Math.ceil(words / wordsPerMinute);
}
// src/pages/blog/[slug].astro
const readingTime = calculateReadingTime(post.body);
10-3. 관련 글 추천
// src/utils/related-posts.ts
export function getRelatedPosts(currentPost, allPosts) {
return allPosts
.filter(p => p.slug !== currentPost.slug)
.map(p => ({
post: p,
score: countCommonTags(currentPost.data.tags, p.data.tags),
}))
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(item => item.post);
}
function countCommonTags(tags1, tags2) {
return tags1.filter(t => tags2.includes(t)).length;
}
10-4. 코드 하이라이팅
Astro는 기본으로 Shiki를 사용합니다.
// astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
theme: 'github-dark',
langs: ['javascript', 'typescript', 'python', 'cpp'],
},
},
});
10-5. 댓글 (Giscus)
<!-- src/components/Comments.astro -->
<script
src="https://giscus.app/client.js"
data-repo="username/repo"
data-repo-id="..."
data-category="Comments"
data-category-id="..."
data-mapping="pathname"
data-reactions-enabled="1"
data-theme="light"
async
></script>
11. 고급 기능
11-1. View Transitions (페이지 전환 애니메이션)
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
페이지 이동 시 부드러운 전환 효과.
11-2. Middleware
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const start = Date.now();
const response = await next();
console.log(`${context.url.pathname} - ${Date.now() - start}ms`);
return response;
});
11-3. 환경 변수
// .env
PUBLIC_SITE_URL=https://example.com
PRIVATE_API_KEY=secret123
// PUBLIC_으로 시작하면 클라이언트에서도 접근 가능
const siteUrl = import.meta.env.PUBLIC_SITE_URL;
// PRIVATE_는 서버에서만
const apiKey = import.meta.env.PRIVATE_API_KEY;
12. 정리
핵심 요약
Astro 블로그 장점:
- 빠른 속도: Zero JS, 정적 HTML
- 타입 안전: Content Collections
- 유연성: MDX, React·Vue 아일랜드
- SEO: RSS, Sitemap, OG 이미지 자동화
추천 스택:
- 프레임워크: Astro 5+
- 스타일: Tailwind CSS
- 검색: Fuse.js 또는 Pagefind
- 댓글: Giscus (GitHub Discussions)
- 배포: Cloudflare Pages
- CI/CD: GitHub Actions
체크리스트
프로젝트 설정:
- Content Collections 스키마 정의
- 태그·시리즈 구조 설계
- RSS·Sitemap 설정
- OG 이미지 생성 스크립트
콘텐츠:
- 마크다운 템플릿 (frontmatter)
- 코드 블록 스타일
- 목차 자동 생성
- 관련 글 로직
배포:
- 환경 변수 설정
- 빌드 캐시 최적화
- 커스텀 도메인
- Analytics 연동
다음 단계
Astro 블로그와 함께 보면 좋은 글:
- Astro + Cloudflare Pages 스택 분석 (Vercel·WordPress 비교)
- Cloudflare Pages 완벽 가이드
- 기술 블로그 방문자 늘리기·내부 링크
- Node.js + GitHub Actions CI/CD
- Technical SEO with Next.js App Router — SSR/SSG 비교 (영문)
참고 자료: