入门
基于 Astro 5.x(最新稳定 5.13+)+ Vite 6 + Node 18.20+ / 20.3+ / 22+ 编写
速查
- 系统要求:Node.js 18.20.8+ / 20.3.0+ / 22.0.0+(Astro 5),奇数版本(19/21/23)不支持;TypeScript 5+;Vite 6+
- 创建:
npm create astro@latest(官方 CLI,交互选模板 / TS / 集成) - 启动:
npm run dev(默认http://localhost:4321,Vite HMR) - 入口:
src/pages/index.astro(首页)+astro.config.mjs(配置)+tsconfig.json(继承astro/tsconfigs/base) - 文件路由:
src/pages/about.astro→/about;src/pages/blog/[slug].astro→/blog/:slug .astro文件结构:- frontmatter(
---包裹)—— 服务端 JS / TS,仅在构建/请求时执行 - template(HTML +
{ expr })—— 输出 HTML <style>—— 默认 scoped<script>—— 默认 ES module,自动 bundle,仅在浏览器执行
- frontmatter(
- 渲染指令:
client:load/client:idle/client:visible/client:media/client:only="<framework>"/server:defer - 集成:
npx astro add react vue svelte solid mdx tailwind sitemap partytown ... - Adapter:
@astrojs/node/@astrojs/vercel/@astrojs/netlify/@astrojs/cloudflare
安装与首次启动
最快路径(官方脚手架):
# 推荐方式
npm create astro@latest
# pnpm / yarn / bun
pnpm create astro@latest
yarn create astro
bun create astro@latest交互式提问:
dir Where should we create your new project?
./my-astro-site
tmpl How would you like to start your new project?
◉ Use blog template
◯ Include sample files
◯ Empty
◯ Use template from GitHub
ts Do you plan to write TypeScript?
Yes
use How strict should TypeScript be?
Strict
deps Install dependencies?
Yes
git Initialize a new git repository?
Yes接着:
cd my-astro-site
npm run dev打开 http://localhost:4321 即看默认页。Vite HMR 默认开启。
Astro 5 改名
create-astro→ 仍是npm create astro@latest,不再有create-astro单独 npm 包。
安装时直接加集成(--add)
npm create astro@latest -- --add react --add tailwind --add sitemap用 GitHub 模板
# 官方 example(在 withastro/astro 的 examples 目录)
npm create astro@latest -- --template blog
# 第三方模板(任意 GitHub 仓库)
npm create astro@latest -- --template username/repo官方 examples 列表:github.com/withastro/astro/tree/main/examples
Node 版本
Astro 5 最低支持:
- Node 18.20.8+(18 LTS 最后一档)
- Node 20.3.0+
- Node 22.0.0+(推荐)
# 用 nvm 安装 22 LTS
nvm install 22
nvm use 22
node -v # v22.x.xAstro 6(2025.10)已升 Node 22.12+;如果用 Astro 6,请用
nvm install --lts。
手动安装
mkdir my-astro-site && cd my-astro-site
npm init --yes
npm install astro补 package.json scripts:
{
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
}
}建最小 astro.config.mjs / tsconfig.json / src/pages/index.astro:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({});// tsconfig.json
{
"extends": "astro/tsconfigs/strict"
}---
// src/pages/index.astro
---
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>Astro</title>
</head>
<body>
<h1>Hello Astro</h1>
</body>
</html>跑 npm run dev 即可。
项目结构
npm create astro@latest 选 minimal 模板生成:
my-astro-site/
├── public/ # 静态资源(按原样拷贝到 dist/)
│ └── favicon.svg
├── src/
│ ├── components/ # 公共组件(.astro / .tsx / .vue / .svelte 都可)
│ ├── content/ # Markdown / MDX 内容(配合 Content Collections)
│ ├── content.config.ts # Content Layer 配置(Astro 5+)
│ ├── layouts/ # 布局组件
│ ├── pages/ # 文件路由(核心)
│ │ ├── index.astro # /
│ │ └── about.astro # /about
│ ├── styles/ # 全局样式(可选)
│ ├── env.d.ts # TS 类型补丁(含 astro/client 引用)
│ └── middleware.ts # 中间件(可选)
├── astro.config.mjs # 配置文件
├── tsconfig.json
├── package.json # 必须 "type": "module"
└── node_modules/
src/pages/和astro.config.mjs是仅有的两个必需位置。其余目录都是约定俗成,可任意改名 / 取消(src/components/src/layouts等)。
关键文件
src/pages/index.astro —— / 路由的页面组件:
---
const greeting = 'Hello';
const name = 'Astro';
---
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>{name}</title>
</head>
<body>
<h1>{greeting}, {name}!</h1>
</body>
</html>astro.config.mjs —— 核心配置:
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
site: 'https://example.com',
integrations: [react()],
output: 'static', // 默认;也可 'server'
// adapter: node({ mode: 'standalone' }), // SSR 必填
});tsconfig.json —— 继承官方推荐:
{
"extends": "astro/tsconfigs/strict",
"include": ["src/**/*", ".astro/types.d.ts", "astro.config.mjs"],
"exclude": ["dist"]
}三档:base / strict / strictest,可按团队偏好选。
src/env.d.ts —— TS 全局类型补丁:
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />Astro 4.14+ 起,运行
astro sync会自动生成.astro/types.d.ts;dev/build启动时也会自动同步。
CLI 命令
astro dev # 启动 dev server(默认 :4321)
astro build # 构建生产产物到 dist/
astro preview # 本地预览 dist/(仅静态)
astro check # TS / Astro 类型检查(含 astro-check)
astro sync # 重新生成 .astro/types.d.ts、Content Collection 类型
astro add <integration># 安装并自动配置集成(react / tailwind / mdx 等)
astro telemetry disable# 关闭遥测第一个页面
src/pages/index.astro 对应 /:
---
// frontmatter(仅服务端执行)
const title = 'Hello Astro';
---
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<p>这是一个最小的 Astro 页面。</p>
</body>
</html>加 /about:
---
// src/pages/about.astro
---
<html lang="zh">
<head><title>About</title></head>
<body>
<h1>About</h1>
<a href="/">Home</a>
</body>
</html>跳转用原生 <a>:
<a href="/about">About</a>Astro 不提供
<Link>组件。装了<ClientRouter />后,原生<a>自动走客户端路由(无 reload);未装则就是普通 MPA 跳转。
.astro 文件语法详解
.astro 文件三段式:frontmatter + 模板 + 样式 / 脚本。
---
// === Frontmatter(服务端执行)===
import Layout from '../layouts/Base.astro';
import Card from '../components/Card.astro';
// 可以 fetch、读文件、调数据库
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
// Props 接收(容后说)
const { title } = Astro.props;
---
<!-- === 模板(HTML + 表达式)=== -->
<Layout title={title}>
<h1>{title}</h1>
<ul>
{posts.map((p) => <li><Card post={p} /></li>)}
</ul>
</Layout>
<!-- === Scoped 样式(默认)=== -->
<style>
h1 {
color: rebeccapurple;
}
</style>
<!-- === 客户端脚本(默认 ESM bundle)=== -->
<script>
console.log('Runs in browser');
</script>要点:
- frontmatter 只在 服务端(构建期或请求期)执行,不进浏览器
- frontmatter 可以用
await(顶层 await) - 模板里
{ expr }是 JS 表达式 <style>默认 scoped(Astro 编译器自动加data-astro-cid-xxx属性)<script>默认 ES module + bundle,仅浏览器执行(不能访问 frontmatter 变量,要传值用define:vars指令)
表达式与控制流
---
const items = ['Apple', 'Banana', 'Cherry'];
const show = true;
const user = { name: 'Alice', age: 30 };
---
<!-- 简单插值 -->
<p>Hello, {user.name}</p>
<!-- 三元 / 短路 -->
<p>{user.age > 18 ? '成年' : '未成年'}</p>
{show && <p>Visible</p>}
<!-- 循环(用 .map 而不是 for)-->
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
<!-- 条件渲染 -->
{user.age >= 18 ? <strong>成年</strong> : <em>未成年</em>}
<!-- 多行 / Fragment -->
{items.length > 0 ? (
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
) : (
<p>暂无数据</p>
)}Astro 模板基于 JSX-like 语法(但更接近原生 HTML:
class而不是className,for而不是htmlFor)。
Props(组件参数)
---
// src/components/Greet.astro
interface Props {
name: string;
age?: number;
}
const { name, age = 18 } = Astro.props;
---
<p>Hello, {name} ({age})</p>使用:
---
import Greet from './Greet.astro';
---
<Greet name="Alice" age={30} />
<Greet name="Bob" /> <!-- age 用默认值 18 -->Slots(插槽)
默认 slot + 命名 slot:
---
// src/components/Card.astro
---
<div class="card">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer">© 默认页脚</slot></footer>
</div>使用:
<Card>
<h2 slot="header">标题</h2>
<p>内容</p>
<p slot="footer">自定义页脚</p>
</Card>slot fallback(默认内容):
<slot>这里是默认内容(无传入时显示)</slot>第一个 Layout
布局是普通 .astro 组件,用 slot 承接子内容:
---
// src/layouts/Base.astro
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<slot />
</main>
<footer>© 2026</footer>
</body>
</html>使用:
---
// src/pages/index.astro
import Base from '../layouts/Base.astro';
---
<Base title="首页">
<h1>Hello Astro</h1>
<p>用 Layout 包页面。</p>
</Base>动态路由
文件名用方括号 [param] 即动态段:
src/pages/
├── blog/
│ ├── index.astro # /blog
│ └── [slug].astro # /blog/:slug
└── shop/
└── [...path].astro # /shop/* catch-all静态构建(output: 'static')需要 getStaticPaths() 告诉 Astro 要生成哪些 URL:
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
return posts.map((post) => ({
params: { slug: post.slug },
props: { post }, // 可选:传 props 给页面
}));
}
const { post } = Astro.props;
---
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>服务端模式(output: 'server' 或单页 prerender = false)不需要 getStaticPaths,Astro.params 直接拿运行时参数:
---
// src/pages/blog/[slug].astro
export const prerender = false;
const { slug } = Astro.params;
const post = await fetch(`/api/posts/${slug}`).then((r) => r.json());
---
<article>
<h1>{post.title}</h1>
</article>第一个 Island(互动组件)
.astro 文件默认不进 JS。要做"按钮点击改文本"这类交互,要么写 client <script>,要么用 framework component + client:*。
用原生 <script>
---
// src/pages/counter.astro
---
<button id="btn">Count: <span id="n">0</span></button>
<script>
const btn = document.getElementById('btn')!;
const n = document.getElementById('n')!;
let count = 0;
btn.addEventListener('click', () => {
n.textContent = String(++count);
});
</script>用 React Island
先装 React 集成:
npx astro add reactastro.config.mjs 自动加:
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
});写 React 组件:
// src/components/Counter.tsx
import { useState } from 'react';
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
}在 .astro 里用,必须加 client:*:
---
// src/pages/counter.astro
import Counter from '../components/Counter.tsx';
---
<h1>Counter</h1>
<Counter client:load /> <!-- 立即 hydrate -->
<Counter client:idle /> <!-- 浏览器空闲后 hydrate -->
<Counter client:visible /> <!-- 进入视口后 hydrate -->不加
client:*的 framework component 会被服务端渲染成 HTML,但没有 JS(变成静态卡片)。
多框架混用
同一页可以同时用 React + Svelte + Vue:
npx astro add react svelte vue---
import ReactCounter from '../components/Counter.tsx';
import SvelteToggle from '../components/Toggle.svelte';
import VueSearch from '../components/Search.vue';
---
<ReactCounter client:load />
<SvelteToggle client:idle />
<VueSearch client:visible />每个 island 完全独立(自己的 React/Svelte/Vue 实例);想跨 island 共享状态用 Nano Stores。
Content Collections(内容集合)
Astro 5 引入 Content Layer API,统一处理 Markdown / MDX / JSON / YAML / 远程 API 内容。
配置 src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
// Content Layer:loader 决定数据从哪来
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
// Zod schema 校验 frontmatter
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
description: z.string().optional(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };写 Markdown
---
title: 我的第一篇博客
pubDate: 2026-05-18
description: 用 Astro 写博客
tags: [astro, web]
---
# 我的第一篇博客
Markdown 内容……文件路径:src/content/blog/my-first-post.md
列出 + 渲染
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
---
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<small>{post.data.pubDate.toLocaleDateString()}</small>
</li>
))}
</ul>---
// src/pages/blog/[id].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<Content />
</article>Astro 5 的
render(entry)是独立函数(从astro:content导入),不再是 entry 上的方法(entry.render()在 5.x deprecated、6.x 移除)。
集成(Integrations)
astro add 一键安装并改 config:
# UI 框架
npx astro add react preact vue svelte solid alpinejs
# 内容 / 渲染
npx astro add mdx markdoc
# 工具
npx astro add tailwind sitemap partytown
# DB
npx astro add db
# Adapter(SSR)
npx astro add node vercel netlify cloudflare也可以一行装多个:
npx astro add react tailwind mdx sitemap完整列表见 Astro Integrations Directory(官方 + 社区,800+ 个)。
第一个 API 端点
Astro 把 src/pages/ 下的 .ts / .js 文件视为端点(不是页面):
// src/pages/api/hello.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = () => {
return new Response(
JSON.stringify({ message: 'Hello' }),
{ headers: { 'Content-Type': 'application/json' } },
);
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
return new Response(JSON.stringify({ received: body }), { status: 201 });
};访问 /api/hello 即得 JSON。
默认(
output: 'static')下,端点只在构建期跑一次(生成静态 JSON 文件)。要按请求跑,加export const prerender = false或全局output: 'server'。
带动态段:
// src/pages/api/users/[id].ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = async ({ params }) => {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) return new Response('Not Found', { status: 404 });
return Response.json(user);
};部署
Static 站(默认)
npm run build # 输出 dist/dist/ 是纯静态文件,丢任意 CDN / S3 / Cloudflare Pages / Netlify / Vercel 都行。
SSR 部署
装 adapter:
npx astro add node # Node.js 自托管
npx astro add vercel # Vercel
npx astro add netlify # Netlify
npx astro add cloudflare # Cloudflare Workers / Pagesastro.config.mjs 自动补:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});npm run build
node ./dist/server/entry.mjs # Node standalone 模式详见 指南 - 高级 的部署章节。
一份能跑的最小示例
my-astro-site/
├── src/
│ ├── components/
│ │ └── Counter.tsx
│ ├── content/
│ │ └── blog/
│ │ ├── first-post.md
│ │ └── second-post.md
│ ├── content.config.ts
│ ├── layouts/
│ │ └── Base.astro
│ └── pages/
│ ├── index.astro
│ ├── about.astro
│ ├── blog/
│ │ ├── index.astro
│ │ └── [id].astro
│ └── api/
│ └── hello.ts
├── astro.config.mjs
├── tsconfig.json
└── package.json// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
});// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
export const collections = {
blog: defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
}),
}),
};---
// src/layouts/Base.astro
const { title } = Astro.props;
---
<html lang="zh">
<head><title>{title}</title></head>
<body>
<nav>
<a href="/">Home</a> | <a href="/about">About</a> | <a href="/blog">Blog</a>
</nav>
<slot />
</body>
</html>---
// src/pages/index.astro
import Base from '../layouts/Base.astro';
import Counter from '../components/Counter.tsx';
---
<Base title="首页">
<h1>Hello Astro</h1>
<Counter client:load />
</Base>---
// src/pages/blog/index.astro
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<Base title="Blog">
<h1>Blog</h1>
<ul>
{posts.map((p) => (
<li><a href={`/blog/${p.id}`}>{p.data.title}</a></li>
))}
</ul>
</Base>---
// src/pages/blog/[id].astro
import Base from '../../layouts/Base.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<Base title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
</Base>// src/components/Counter.tsx
import { useState } from 'react';
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
}npm run dev 打开 http://localhost:4321 即看完整 App。
下一步
.astro完整语法 / Islands / 客户端指令 / 文件路由 / Layouts / 端点 见 指南 - 基础- SSR / On-demand / Server Islands / Actions / View Transitions / Image / Adapter 见 指南 - 进阶
- Content Layer 深度 / DB / i18n / Streaming / Middleware / 多框架混用 / 部署模式 见 指南 - 高级
- vs Next.js / Nuxt / SvelteKit / Astro 5 → 6 升级 / 常见踩坑 见 指南 - 其他
- 全部文件约定 /
Astro全局对象 / 配置 / 指令速查 见 参考