null

Astro.js: современный фреймворк для контент-ориентированных сайтов

Astro.js: современный фреймворк для контент-ориентированных сайтов

Что такое Astro и зачем он нужен

Astro — это фреймворк для сайтов и веб-приложений, который по умолчанию рендерит большую часть страницы в статический HTML на сервере, а JavaScript подключает только там, где он действительно нужен. Такой подход называется islands architecture: основная часть страницы остается «легкой», а интерактивность добавляется небольшими изолированными фрагментами.

Astro поддерживает несколько UI-фреймворков одновременно: React, Preact, Vue, Svelte, Solid и веб-компоненты. Это делает его удобным, когда нужно сочетать контентные страницы, маркетинговые лендинги, документацию и отдельные интерактивные блоки в одном проекте.

Ключевые преимущества

Производительность «из коробки»

Astro проектировался как server-first решение, поэтому большинство страниц может отдаваться без необходимости тащить на клиент целый фреймворк runtime. В документации прямо отмечено, что это снижает сложность по сравнению с другими UI-фреймворками, потому что на сервере нет необходимости управлять реактивностью, хуками, stale closures и похожими механизмами.

UI-агностичность

Astro имеет систему интеграций и renderer-ов, через которую подключаются UI-фреймворки, адаптеры для серверного рендеринга и дополнительные возможности вроде MDX или sitemap. Для обычного проекта подключение официальной интеграции делается в несколько строк.

  • React — для компонентов с богатым состоянием,
  • Preact — там, где нужен React-подобный синтаксис с минимальным весом,
  • Svelte — для компактных и производительных виджетов,
  • Vue — если команда привыкла к его экосистеме,
  • Lit — для веб-компонентов.

На практике это особенно ценно при миграции старых проектов: вы можете постепенно переписывать компоненты, не ломая всё сразу.

Гибкая модель интерактивности

Astro использует компонентный подход, но интерактивность включается точечно через hydration directives: client:load, client:idle, client:visible, client:media и client:only. Это позволяет запускать JavaScript только для конкретных элементов интерфейса и выбирать момент загрузки в зависимости от приоритета UI.

Content Collections

Начиная с версии 2.0, Astro предлагает типизированные коллекции контента — встроенную систему для работы с Markdown, MDX и другими форматами с полной поддержкой TypeScript.

Технические основы: как это работает

Этапы сборки

Когда вы запускаете astro build, происходит следующее:

  1. Парсинг компонентов — Astro читает все .astro-файлы и компоненты UI-фреймворков.
  2. Статическая генерация — выполняется JavaScript в frontmatter (часть между ---), которая может делать запросы к API, читать файлы и т.д.
  3. Рендеринг в HTML — вся разметка превращается в чистый HTML.
  4. Выделение «островов» — компоненты с клиентскими директивами (client:load, client:idle и т.д.) упаковываются отдельно.
  5. Гидратация — при загрузке страницы браузер загружает только нужные острова, причём строго по заданной стратегии.

Клиентские директивы

Директивы определяют, когда браузер должен оживить компонент:

Директива Поведение
client:load Немедленно при загрузке страницы
client:idle Когда браузер освободился (через requestIdleCallback)
client:visible Когда компонент попадает в область видимости (Intersection Observer)
client:media="(max-width: 768px)" При совпадении медиазапроса
client:only="preact" Только на клиенте, без серверного рендеринга

 

Синтаксис .astro-файлов

.astro-файл делится на две части: frontmatter (серверный JavaScript) и шаблон (HTML-подобная разметка).

---
// Всё здесь выполняется ТОЛЬКО на сервере/в момент сборки.
// Этот код никогда не попадёт в браузер.

import Header from '../components/Header.astro';
import ProductCard from '../components/ProductCard.jsx'; // React-компонент
import { getCollection } from 'astro:content';

const products = await fetch('https://api.example.com/products')
  .then(res => res.json());

const { title = 'Каталог товаров' } = Astro.props;
---

<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>{title}</title>
  </head>
  <body>
    <!-- Статический Astro-компонент -->
    <Header />

    <main>
      <h1>{title}</h1>

      <ul class="product-grid">
        {products.map(product => (
          <!-- ProductCard — React-компонент -->
          <ProductCard client:visible name={product.name}/>
        ))}
      </ul>
    </main>
  </body>
</html>

<style>...</style>

Astro автоматически изолирует CSS-стили — каждый .astro-файл получает уникальный хэш-атрибут.

Layouts — переиспользуемые макеты

В Astro макеты — это просто компоненты со слотом <slot />:

---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
}

const { title, description = 'Мой Astro-сайт' } = Astro.props;
---

<!doctype html>
<html lang="ru">
  <head>
    ...
  </head>
  <body>
    <nav class="site-nav"> ... </nav>

    <!-- Содержимое страницы подставляется сюда -->
    <slot />

    <footer>© 2024 Мой сайт</footer>
  </body>
</html>

Маршрутизация и работа с данными

Файловая маршрутизация

Astro использует файловую систему для маршрутизации — принцип, знакомый по Next.js:

src/pages/
├── index.astro          → /
├── about.astro          → /about
├── blog/
│   ├── index.astro      → /blog
│   └── [slug].astro     → /blog/любой-slug
└── api/
    └── products.ts      → /api/products  (API-эндпоинт)

Динамические маршруты и getStaticPaths

Для генерации динамических страниц используется функция getStaticPaths:

---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

// Эта функция говорит Astro: "вот все возможные URL, которые нужно сгенерировать"
export async function getStaticPaths() {
  const posts = await getCollection('blog'); // читаем коллекцию из src/content/blog/

  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },              // передаём данные в компонент
  }));
}

const { post } = Astro.props;
const { Content } = await post.render(); // превращаем Markdown в компонент
---

<BlogLayout title={post.data.title} description={post.data.description}>
  <article>
    <h1>{post.data.title}</h1>
    <time>{post.data.publishedAt.toLocaleDateString('ru-RU')}</time>
    <!-- Content — это готовый к рендерингу HTML из Markdown-файла -->
    <Content />
  </article>
</BlogLayout>

Content Collections с типизацией

Система коллекций позволяет описать схему frontmatter и получить полную автодополнение в редакторе:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content', // 'content' для Markdown, 'data' для JSON/YAML
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishedAt: z.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    // Если поля нет — TypeScript немедленно об этом сообщит
    coverImage: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};

После этого getCollection('blog') вернёт типизированный массив, и вы не сможете случайно обратиться к несуществующему полю.

API-эндпоинты

Astro позволяет создавать серверные API прямо внутри проекта:

// src/pages/api/newsletter.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const { email } = body;

  if (!email || !email.includes('@')) {
    return new Response(
      JSON.stringify({ error: 'Некорректный email' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Здесь можно вызвать любой сервис: Mailchimp, Resend, собственную БД...
  await subscribeToNewsletter(email);

  return new Response(
    JSON.stringify({ success: true }),
    { status: 200, headers: { 'Content-Type': 'application/json' } }
  );
};

Сравнение с другими фреймворками

Понять место Astro в экосистеме проще всего через сравнение с конкурентами. Но важно понимать: это не «Astro против Next.js», а «разные инструменты для разных задач».

Сводная таблица

Критерий Astro Next.js SvelteKit Nuxt
JS по умолчанию ✅ Нет ❌ Да ⚠️ Минимум ❌ Да
Смешение фреймворков ✅ Да ❌ Нет ❌ Нет ❌ Нет
SSR ✅ Да ✅ Да ✅ Да ✅ Да
Встроенный роутер ✅ Файловый ✅ Файловый ✅ Файловый ✅ Файловый
Лучший случай Контент-сайты Rich SPA Svelte-приложения Vue-приложения
Порог входа Низкий Средний Средний Средний

 

Интеграция с UI-фреймворками на практике: Preact

Preact — это компактная библиотека компонентов, совместимая с React-экосистемой через preact/compat. В официальных материалах Preact подчеркивается, что compatibility layer позволяет использовать React-компоненты как drop-in replacement в ряде случаев, а сам Preact отличается более близким к DOM поведением и меньшим runtime-overhead, в том числе без synthetic event system.

В Astro интеграция с Preact подключается официальным пакетом @astrojs/preact. Для обычного использования достаточно команды astro add preact; после этого Astro умеет рендерить Preact-компоненты и гидрировать их через client:* директивы.

Установка и конфигурация

# Добавляем интеграцию одной командой
npx astro add preact

# Или вручную через npm
npm install @astrojs/preact preact

После этого обновляем конфиг:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';

export default defineConfig({
  integrations: [
    preact({
      // compat: true — включает слой совместимости с React,
      // что позволяет использовать некоторые React-библиотеки с Preact
      compat: true,
    }),
  ],
});

Особенности и тонкости при работе с Preact в Astro

Атрибуты class vs className. В Astro-компонентах (.astro) всегда используется class. В Preact-компонентах (.jsx) тоже рекомендуется class, а не className — это одно из отличий Preact от React.

Серверный рендеринг Preact-компонентов. Даже без клиентской директивы Preact-компоненты рендерятся на сервере в HTML. Директива лишь добавляет гидратацию. Это значит, что поисковик увидит контент компонента, даже если JavaScript в браузере отключён.

client:only="preact" нужен в редких случаях — когда компонент использует браузерные API (localStorage, window) и не может быть отрендерен на сервере. В таком случае Astro пропускает серверный рендеринг полностью.

// Компонент, который читает из localStorage — не может работать на сервере
export default function ThemeToggle() {
  const [theme, setTheme] = useState(
    // localStorage доступен только в браузере
    () => localStorage.getItem('theme') ?? 'light'
  );

  // ...
}
<!-- Правильно: client:only пропускает SSR -->
<ThemeToggle client:only="preact" />

Итог

Astro полезен там, где важны скорость первой отрисовки, небольшой клиентский бандл и гибкая интеграция с несколькими UI-фреймворками. Preact в этой экосистеме хорошо подходит для компактных интерактивных компонентов и для проектов, где нужна совместимость с частью React-экосистемы, но без полного веса React runtime.

Next