[2026] Astro 4 Complete Guide — Build Performance, Content Collections v2, View Transitions
이 글의 핵심
Astro 4 keeps a static-first architecture while boosting real-world productivity and perceived performance with View Transitions, the Content Layer (collections v2), the Dev Toolbar, and more. This post walks through build pipeline optimization, type-safe content, page transition animations, dark mode, and deployment settings on major hosting platforms in one flow.
Key points
Astro 4 is a framework for shipping content-centric websites quickly. It defaults to Island Architecture to minimize JavaScript. This guide ties together build and dev performance, Content Collections v2 (content layer), View Transitions, dark mode, and deployment on Vercel, Netlify, and Cloudflare Pages from a practical engineering perspective.
Practical note: For marketing sites, technical blogs, and documentation, both perceived loading and build stability when editing content matter. Astro 4 can combine a type-safe content pipeline with page transition UX, which helps team operations.
Table of contents
- What changed in Astro 4
- Project setup and layout
- Inspecting islands with the Dev Toolbar
- Build and dev performance
- Content Collections v2 and content.config.ts
- MDX and interactive content
- View Transitions in practice
- Dark mode (classes and transitions)
- Hybrid rendering and SSR overview
- Deployment: Vercel, Netlify, Cloudflare Pages
- SEO and structured data
- Checklist and troubleshooting
What changed in Astro 4
The Astro 4.x line keeps static site generation (SSG) as the default while improving productivity in these areas:
| Area | Description |
|---|---|
| Developer experience | Dev Toolbar to visually inspect components, events, and island dependencies |
| Content | Content Layer — declaratively unify Markdown/MDX and remote sources in content.config.ts with loaders (e.g. glob) |
| UX | View Transitions — smoother page transitions via the browser View Transitions API |
| Internationalization | Experimental i18n routing and routing-layer improvements (enabled per project) |
These pillars (content layer, View Transitions, adapters) carry forward into Astro 5, so patterns you learn in Astro 4 map naturally to newer releases.
Project setup
Create with the CLI
npm create astro@latest
To pin Astro 4.x explicitly, after creation set "astro": "^4.16.0" (or similar) in package.json so the major version stays 4. Document that as a team standard for reproducible builds.
In the interactive prompts, pick a template (Blog, Docs, …) and TypeScript to scaffold content and pages together.
package.json scripts
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
}
}
astro build produces the production bundle; astro preview serves the build locally for verification. On hosting platforms, publishing the npm run build output is the usual flow.
Dev Toolbar
The Dev Toolbar is an overlay shown while the dev server runs. Use it to quickly review island boundaries, client directives, and performance hints. It is not included in production builds, so it does not affect bundle size or SEO.
In practice it helps with:
- Spotting unnecessary
client:load: Check whether off-screen components hydrate too eagerly - Tracing layout shift: Investigate CLS tied to images and fonts
- Debugging View Transitions: See which DOM should persist vs. swap during transitions
Add a single line to onboarding docs — “Lighthouse on staging, Dev Toolbar locally” — to reduce performance regressions.
Build performance
1) Output mode and adapters
- Static (
output: 'static'): HTML and assets are generated at build time by default. CDN caching works very well. - Server (
output: 'server') or hybrid: For APIs and SSR, attach an adapter (@astrojs/node,@astrojs/vercel,@astrojs/netlify,@astrojs/cloudflare, etc.).
// astro.config.mjs 예시 — 정적 사이트
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
vite: {
build: {
cssMinify: true,
},
},
});
2) Vite-level tuning
Astro uses Vite internally. You can adjust bundle splitting and dependency pre-bundling via vite.build, vite.ssr, vite.optimizeDeps, and related options. Heavy customization can invalidate build caches easily, so prefer minimal changes after measuring.
3) Image optimization
Use the Image component from astro:assets to optimize local images at build time. That directly improves CLS (Cumulative Layout Shift) and LCP (Largest Contentful Paint).
---
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image src={hero} alt="히어로 이미지" width={1200} height={630} loading="eager" />
4) Keeping client JS small (islands)
Framework components should be wrapped only with client:* directives. Sprinkling unnecessary client:load everywhere worsens TTI (Time to Interactive).
---
import Counter from '../components/Counter.tsx';
---
<!-- 뷰포트에 들어올 때만 수화 -->
<Counter client:visible />
Content Collections v2
The recommended pattern since Astro 4 is to define collections in content.config.ts at the project root and collect files under src/content with glob or other loaders. Zod schemas validate frontmatter so typos and missing fields fail at build time.
content.config.ts example
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
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),
level: z.enum(['초급', '중급', '고급']).optional(),
}),
});
export const collections = { blog };
Querying from a page
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sorted = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<ul>
{sorted.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
post.id depends on the glob loader’s base and file paths. For a Korean blog in this repo (pkglog.com), if [...slug].astro passes post.id into params.slug, list href values must follow the same rules or links will break.
For dynamic routes, call render(post) to render MDX/Markdown bodies. Use getEntry('blog', slug) when you need a single entry.
Why use content v2
- Schema validation: Catch bad frontmatter early instead of broken builds in production
- Loader extensibility: Room to unify local MDX and remote CMS in one pipeline
- Type generation: Better editor autocomplete and safer
datafield access
MDX content
If the collection pattern allows **/*.{md,mdx}, posts can import UI components and place them alongside prose. MDX shines when combined with islands. Prefer small chunks of interactivity with client:visible or client:idle for safer delayed hydration.
---
title: '샘플 MDX'
description: 'MDX에서 컴포넌트 사용. [2026] Astro 4 Complete Guide — Build Performance, Content Collections v2, View Transitions에 대한 완전한 가이드입니다. 실전 예제와 함께 핵심 개념부터 고급 활용까지 다룹니다.'
pubDate: '2024-07-03'
---
import Chart from '../../components/Chart.tsx';
## 실시간 예제
아래 차트는 스크롤 후에만 수화됩니다.
<Chart client:visible />
To avoid bloated client bundles in MDX, wrap shared charts or code editors in one or two abstractions and reuse them across posts.
View Transitions
View Transitions keep global layout and add smooth transitions between navigations. Astro exposes this via astro:transitions.
Enable globally in the layout
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="ko">
<head>
<meta charset="utf-8" />
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
Per-link transition hints
<a href="/blog/hello" transition:animate="slide">글로 이동</a>
Keep sidebar and header with transition:persist
Headers, sidebars, and audio players on the same layout often should persist across navigations. Add transition:persist on the root element so the DOM snapshot stays without flicker.
<aside transition:persist="sidebar">
<nav>...</nav>
</aside>
Scripts after transitions: astro:page-load
View Transitions swap HTML, so scripts that only listen for DOMContentLoaded may not run again. Astro provides the astro:page-load custom event as a hook after each navigation.
<script>
document.addEventListener('astro:page-load', () => {
// 페이지마다 초기화해야 하는 분석·짧은 UI 스크립트
});
</script>
Scroll and state quirks
- On long posts, scroll restoration may differ from expectations; use
transition:persistto keep specific DOM when needed. - Avoid duplicate event listeners by scoping client components clearly with
client:*and lifecycle boundaries.
Dark mode
Dark mode usually combines a class strategy (e.g. html.dark) with prefers-color-scheme. With View Transitions, an inline bootstrap script that applies preference first is common to prevent flash of unstyled content (FOUC).
Layout example: localStorage + class
---
const title = 'Astro 4 가이드';
---
<!doctype html>
<html lang="ko" class="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<script is:inline>
(function () {
const k = 'theme';
const stored = localStorage.getItem(k);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body>
<button
type="button"
id="theme-toggle"
class="rounded border px-3 py-1 text-sm dark:border-gray-600"
>
테마 전환
</button>
<slot />
<script>
const btn = document.getElementById('theme-toggle');
btn?.addEventListener('click', () => {
const root = document.documentElement;
const next = root.classList.toggle('dark') ? 'dark' : 'light';
localStorage.setItem('theme', next === 'dark' ? 'dark' : 'light');
});
</script>
</body>
</html>
With Tailwind CSS, pair darkMode: 'class' with this pattern. CSS variables for color tokens let a single class toggle update the whole theme consistently.
If the theme misapplies for a frame with View Transitions enabled, you can re-sync classes on document.documentElement in an astro:after-swap hook—but that adds complexity, so inline initialization to stop FOUC should come first.
Hybrid rendering
To server-render only some routes and keep the rest static, consider output: 'hybrid' (or, in later versions, the documented output: 'static' + adapter combinations). In Astro 4, teams often add SSR only on a few routes where static generation is a poor fit—auth-gated pages or data that must be fresh per request.
Hybrid and SSR complicate caching (CDN, Cache-Control, edge keys) compared with a static site. Prefer static by default and dynamism only where truly needed.
Deployment
Common: astro build output
In static mode, dist/ is the default output. Each platform points its build command at that directory.
Vercel
- Framework preset: Astro
- Build Command:
npm run build - Output Directory:
dist - For SSR, use the
@astrojs/verceladapter withoutput: 'server'orhybrid
// astro.config.mjs — SSR 예시 (필요 시)
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
Netlify
- Build command:
npm run build - Publish directory:
dist - For SSR, use the
@astrojs/netlifyadapter
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
Cloudflare Pages
Static deploys still publish dist. For SSR or the edge, use @astrojs/cloudflare. This repo (pkglog.com) often assumes Cloudflare Pages; align environment variables, cache invalidation, and Node compatibility with the runtime docs.
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});
SEO and structured data
Meta and structured data
title/description: Core to search snippets—make them unique per page.- Open Graph / Twitter cards: Affect social click-through.
- Semantic HTML: One
h1, cleararticle,header,nav,main.
Patterns in Astro
Centralize canonical URLs, OG images, and JSON-LD in a shared component such as BaseHead.astro, and inject summary and dates from frontmatter in posts. For this blog, designing schemas so fields like summaryTop and faq can feed rich results is a good goal.
Article JSON-LD example
---
const { title, description, pubDate, canonicalURL } = Astro.props;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: title,
description,
datePublished: pubDate?.toISOString?.() ?? pubDate,
mainEntityOfPage: canonicalURL,
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
Search engines consolidate duplicates with consistent date formats and canonical URLs. When migrating blog platforms, plan redirects and canonical together.
Troubleshooting
| Symptom | What to check |
|---|---|
| Content schema errors at build | Zod fields in content.config.ts match Markdown frontmatter keys |
| Duplicate scripts after View Transitions | Scope of client:only / client:load and use of transition:persist |
| Broken image paths | astro:assets often expects relative import paths |
| Cloudflare SSR build failures | Node-only packages not leaking into the edge bundle; compatibility in adapter docs |
Summary
Astro 4 keeps fast first loads with minimal JavaScript, uses Content Collections v2 to enforce content quality at build time, and can lift perceived UX with View Transitions. Add dark mode, images, and island loading, and you can balance performance, operations, and search for technical blogs, docs, and marketing sites.
Deploy on Vercel, Netlify, or Cloudflare Pages with astro build as the standard step, choosing an adapter only when SSR is required. After changes, follow git push then npm run deploy, and verify Lighthouse and real route transitions in production.