Astro
Экспресс-тур по Astro: островная архитектура, zero-JS, frontmatter, компоненты, гидрация client:*, маршрутизация и контент-коллекции на примерах.
Astro — фреймворк для контентных сайтов (блоги, документация, лендинги, маркетинг), который по умолчанию отдаёт страницы как чистый HTML без JavaScript. Его ключевая идея — островная архитектура: интерактив включается точечно, островками, а остальная страница остаётся статикой. Ниже — весь Astro за 14 минут в плотно закомментированном коде.
Что такое Astro
Astro собирает страницы на сервере (во время билда) и отправляет браузеру минимум JavaScript. Это делает контентные сайты быстрыми по умолчанию.
---
// Это .astro-компонент. Часть между --- называется frontmatter.
// Здесь обычный JavaScript/TypeScript, который выполняется НА СЕРВЕРЕ
// во время сборки (или при запросе в SSR), но НЕ попадает в браузер.
// ФИЛОСОФИЯ ASTRO:
// 1) Контентные сайты — блоги, доки, лендинги (не SPA-дашборды).
// 2) Островная архитектура — страница это статичный HTML («море»),
// а интерактивные виджеты — отдельные «острова», которые
// оживают (гидрируются) независимо друг от друга.
// 3) Zero-JS по умолчанию — если на странице нет островов,
// в браузер уедет 0 КБ JavaScript. Только HTML и CSS.
const framework = "Astro";
---
<!-- Ниже идёт разметка. Она отрендерится в чистый HTML. -->
<h1>Привет, {framework}!</h1>
Структура .astro файла
Файл состоит из двух частей: frontmatter между линиями --- (серверный JS) и разметки ниже (почти обычный HTML).
---
// === ЧАСТЬ 1: FRONTMATTER (между тремя дефисами) ===
// Импорты, переменные, запросы к API, любая логика.
// Выполняется один раз на сервере при рендере страницы.
const title = "Моя страница";
const items = ["Astro", "быстрый", "простой"];
// Можно делать асинхронные вызовы прямо здесь (top-level await):
// const data = await fetch("https://api.example.com").then(r => r.json());
---
<!-- === ЧАСТЬ 2: ШАБЛОН/РАЗМЕТКА (всё после второго ---) === -->
<!-- Здесь HTML + вставки {выражений} из frontmatter. -->
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
</body>
</html>
Переменные и выражения
Значения из frontmatter подставляются в разметку через фигурные скобки { } — как в JSX.
---
const name = "Мир";
const price = 1990;
const user = { isAdmin: true, login: "chick" };
---
<!-- Простая подстановка переменной -->
<p>Привет, {name}!</p>
<!-- Любое JS-выражение внутри скобок -->
<p>Цена со скидкой: {price * 0.9} руб.</p>
<p>Сегодня: {new Date().getFullYear()} год</p>
<!-- Доступ к полям объекта -->
<p>Логин: {user.login}</p>
<!-- Выражение можно подставлять и в значение атрибута -->
<a href={"/users/" + user.login}>Профиль</a>
<!-- Динамические атрибуты булевого типа -->
<input disabled={user.isAdmin} />
Компоненты
Любой .astro-файл — переиспользуемый компонент. Его импортируют во frontmatter и используют как тег. Данные передаются через props и читаются из Astro.props.
---
// Файл: src/components/Card.astro
// Достаём переданные props из глобального объекта Astro.
const { title, href } = Astro.props;
---
<article class="card">
<a href={href}>{title}</a>
</article>
---
// Файл: src/pages/index.astro
// Импортируем компонент (расширение .astro обязательно).
import Card from "../components/Card.astro";
---
<!-- Используем как обычный тег. Props передаём как атрибуты. -->
<Card title="Главная" href="/" />
<Card title="Блог" href="/blog" />
Разметка и HTML
Шаблон Astro — это почти стандартный HTML. Атрибут класса пишется как class (а не className), а комментарии — обычные HTML-комментарии.
---
const active = true;
---
<!-- class, а НЕ className (Astro ближе к HTML, чем React) -->
<div class="box highlighted">
<p>Обычный абзац</p>
</div>
<!-- class:list — удобный синтаксис для условных классов -->
<button class:list={["btn", { active: active }]}>
Кнопка
</button>
<!-- Стили внутри компонента по умолчанию scoped -->
<style>
.box { padding: 16px; }
.highlighted { border: 2px solid hotpink; }
</style>
Условия и циклы
Отдельных директив нет — используются обычные JS-выражения прямо в разметке: && для условий и map для списков.
---
const isLoggedIn = true;
const role = "admin";
const tags = ["js", "astro", "web"];
---
<!-- Условный рендер через логическое И -->
{isLoggedIn && <p>Вы вошли в систему</p>}
<!-- Тернарный оператор для выбора из двух вариантов -->
{role === "admin"
? <strong>Администратор</strong>
: <span>Пользователь</span>}
<!-- Цикл по массиву через map (как в JSX) -->
<ul>
{tags.map((tag) => <li>{tag}</li>)}
</ul>
Слоты
Слоты позволяют компоненту-обёртке принимать вложенное содержимое. <slot /> — место для контента, можно делать именованные слоты.
---
// Файл: src/components/Box.astro
---
<div class="box">
<header>
<!-- Именованный слот: сюда попадёт содержимое с slot="head" -->
<slot name="head" />
</header>
<!-- Безымянный слот — всё остальное вложенное содержимое -->
<slot />
</div>
---
import Box from "../components/Box.astro";
---
<Box>
<!-- Уйдёт в slot name="head" -->
<h2 slot="head">Заголовок</h2>
<!-- А это попадёт в безымянный <slot /> -->
<p>Основной текст карточки.</p>
</Box>
Островная гидрация
Это главная фишка Astro. По умолчанию компоненты фреймворков рендерятся в статичный HTML без JS. Директива client:* превращает компонент в «остров» — оживляет его в браузере.
---
// Импортируем интерактивный React-компонент
import Counter from "../components/Counter.jsx";
---
<!-- Без директивы: только HTML, кнопки НЕ работают (zero-JS) -->
<Counter />
<!-- client:load — гидрировать сразу при загрузке страницы -->
<Counter client:load />
<!-- client:idle — когда браузер освободится (requestIdleCallback) -->
<Counter client:idle />
<!-- client:visible — только когда остров появится во вьюпорте.
Идеально для виджетов внизу длинной страницы. -->
<Counter client:visible />
<!-- client:media — гидрировать по медиа-запросу (напр. только на десктопе) -->
<Counter client:media="(min-width: 768px)" />
<!-- client:only — рендерить ТОЛЬКО на клиенте, без серверного HTML -->
<Counter client:only="react" />
Интеграция фреймворков
В одном проекте (и даже на одной странице) можно смешивать React, Vue, Svelte, Solid и другие. Нужна соответствующая интеграция в конфиге.
---
// Подключаем компоненты из разных фреймворков
import ReactWidget from "../components/Widget.jsx";
import VueChart from "../components/Chart.vue";
import SvelteForm from "../components/Form.svelte";
---
<!-- React, Vue и Svelte мирно соседствуют на одной странице -->
<ReactWidget client:visible />
<VueChart client:idle />
<SvelteForm client:load />
<!--
Интеграции добавляются командой, например:
npx astro add react vue svelte
Она пропишет их в astro.config.mjs:
import react from "@astrojs/react";
import vue from "@astrojs/vue";
export default defineConfig({ integrations: [react(), vue()] });
-->
Маршрутизация
Роутинг файловый: каждый файл в src/pages/ становится маршрутом. Квадратные скобки в имени файла создают динамический параметр.
---
// === ФАЙЛОВАЯ МАРШРУТИЗАЦИЯ ===
// src/pages/index.astro -> /
// src/pages/about.astro -> /about
// src/pages/blog/index.astro -> /blog
// src/pages/blog/[slug].astro -> /blog/любой-slug (динамический)
// Для динамических страниц в статичном режиме нужен getStaticPaths:
export function getStaticPaths() {
const posts = ["hello", "astro-rules", "zero-js"];
// Возвращаем массив путей, которые Astro сгенерирует при билде.
return posts.map((slug) => ({ params: { slug } }));
}
// Текущее значение параметра из URL:
const { slug } = Astro.params;
---
<h1>Пост: {slug}</h1>
Layouts (макеты)
Layout — обычный компонент-обёртка с общей разметкой (head, шапка, подвал). Страница оборачивается в него и передаёт контент через слот.
---
// Файл: src/layouts/BaseLayout.astro
const { title } = Astro.props;
---
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<header>Шапка сайта</header>
<!-- Сюда подставится контент страницы -->
<slot />
<footer>Подвал</footer>
</body>
</html>
---
// Файл: src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
---
<!-- Оборачиваем страницу в макет, передаём props -->
<BaseLayout title="Главная">
<h1>Контент уйдёт в <slot /> макета</h1>
</BaseLayout>
Markdown и контент-коллекции
Astro умеет рендерить .md и .mdx файлы как страницы. Для типизированного контента (блог, доки) есть контент-коллекции с проверкой схемы.
---
// === КОНТЕНТ-КОЛЛЕКЦИИ ===
// Файл: src/content/config.ts описывает схему коллекции:
//
// import { defineCollection, z } from "astro:content";
// const blog = defineCollection({
// schema: z.object({
// title: z.string(),
// date: z.date(),
// draft: z.boolean().default(false),
// }),
// });
// export const collections = { blog };
//
// Сами посты лежат в src/content/blog/*.md с frontmatter:
// ---
// title: "Привет"
// date: 2026-01-01
// ---
// Текст поста в Markdown.
import { getCollection } from "astro:content";
// Получаем все записи коллекции (с проверкой по схеме):
const posts = await getCollection("blog");
---
<ul>
{posts.map((post) => (
<li>
<a href={"/blog/" + post.slug}>{post.data.title}</a>
</li>
))}
</ul>
Типичная страница
Соберём всё вместе: импорт макета и островов, данные во frontmatter, цикл по списку и точечная гидрация интерактивного виджета.
---
// Файл: src/pages/index.astro — собираем изученное воедино.
import BaseLayout from "../layouts/BaseLayout.astro";
import Card from "../components/Card.astro";
import SearchBox from "../components/SearchBox.jsx"; // React-остров
const pageTitle = "Курсы по веб-разработке";
const courses = [
{ title: "Astro с нуля", href: "/c/astro" },
{ title: "React для всех", href: "/c/react" },
];
---
<BaseLayout title={pageTitle}>
<h1>{pageTitle}</h1>
<!-- Интерактивный остров: оживёт, когда попадёт во вьюпорт -->
<SearchBox client:visible />
<!-- Статичные карточки: zero-JS, просто HTML -->
{courses.map((course) => (
<Card title={course.title} href={course.href} />
))}
<!-- Условный блок -->
{courses.length === 0 && <p>Курсов пока нет</p>}
</BaseLayout>