LEARN X · ЗА 14 МИН

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>
Поддержать проект