Справочник21 августа 2026 г.·обновлено 21 августа 2026 г.

Astro: статический сайт, который выдерживает тысячи страниц

Astro собирает каждую страницу в готовый HTML на этапе сборки и по умолчанию не грузит в браузер JavaScript. Разбор механики: маршруты из файлов, коллекции со схемой, острова, сборка в dist/ — и где этот подход перестаёт работать.

Что это решает

Сайт на тысячу и больше страниц упирается в три вещи: скорость первой отрисовки, полноту индексации и стоимость живого бэкенда. Astro собирает каждую страницу в готовый HTML на этапе сборки и по умолчанию не отправляет в браузер ни строчки JavaScript. В продакшене остаётся папка dist/ и любой хостинг, умеющий отдавать файлы. Каталог товаров, база знаний, документация, блог с архивом за пять лет — типовая нагрузка, под которую такой генератор и сделан.

Вторая боль, которую он снимает, — разнобой в контенте. Когда статьи пишут пять человек, поля во frontmatter расползаются: у кого-то date, у кого-то published, у третьего дата строкой в формате «12.03». Astro позволяет описать схему контента и уронить сборку на первом же несоответствии, а не выкатить страницу с пустой датой.

Как устроено

Маршруты лежат в файловой системе

Путь файла в src/pages/ — это URL. src/pages/index.astro отдаёт /, src/pages/docs/install.astro отдаёт /docs/install. Квадратные скобки в имени дают параметр: src/pages/blog/[slug].astro.

Динамический маршрут обязан объяснить сборщику, какие конкретно страницы порождать. За это отвечает экспорт getStaticPaths() — он выполняется один раз при сборке и возвращает список параметров:

---
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<article><Content /></article>

Верхний блок между --- выполняется на сервере во время сборки. Всё, что в нём происходит — запросы к файлам, к API, к базе, — в браузер не попадает: туда уезжает только результат.

В проектах, начатых на ранних версиях, встречается старый вариант того же кода: post.slug вместо post.id и await post.render() вместо await render(post). Обе формы делают одно и то же, менять их имеет смысл только при апгрейде.

Контент описывается схемой

Коллекция — это набор однотипных файлов плюс правила их разбора. Конфиг лежит в src/content.config.ts (в старых проектах — src/content/config.ts):

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()).max(5),
  }),
});

export const collections = { blog };

Дальше getCollection('blog') отдаёт типизированные записи, а редактор подсказывает поля. Файл с опечаткой в date не соберётся вовсе — ошибка вылезет в консоли сборки с указанием пути. На девятистах статьях это единственный способ держать порядок без ручной вычитки.

Загрузчик не обязан читать диск. glob() берёт markdown из репозитория, file() — один JSON или CSV со списком записей, свой загрузчик может ходить в CMS или в базу и отдавать те же записи со схемой. Для каталога это значит: товары тянутся из внешнего источника один раз при сборке, а на выходе всё равно получаются обычные HTML-файлы.

Острова

Компонент React, Vue или Svelte, вставленный в .astro-страницу без директив, отрисуется в HTML при сборке и никакого JavaScript в браузер не пошлёт. Интерактивность включается явно:

<Search client:load />        <!-- гидратировать сразу -->
<Comments client:visible />   <!-- когда доскроллят -->
<Chart client:idle />         <!-- когда браузер освободится -->
<Map client:only="react" />   <!-- не рендерить на сервере вовсе -->

Каждый такой компонент собирается в отдельный бандл со своими зависимостями. Страница с одним client:visible внизу не тянет фреймворк до тех пор, пока пользователь не доскроллит.

Сборка

astro build проходит по всем маршрутам, выполняет getStaticPaths(), рендерит страницы и складывает результат в dist/ — по одному index.html на каждый URL плюс общие ассеты с хешем в имени. astro preview поднимает локальный сервер поверх готового dist/, чтобы посмотреть ровно то, что уедет на хостинг.

Сборка обходит маршруты последовательно, поэтому время растёт примерно пропорционально числу страниц. Это главный расход, который придётся планировать.

Адаптеры и точечный SSR

По умолчанию проект статический. Если части сайта нужен рендер по запросу, ставится адаптер (@astrojs/node, @astrojs/cloudflare, @astrojs/vercel, @astrojs/netlify), после чего отдельная страница помечается export const prerender = false и начинает выполняться на сервере. Остальные страницы остаются файлами. Так живут гибриды: каталог статикой, корзина и личный кабинет — динамикой.

Изображения

astro:assets обрабатывает картинки при сборке: компонент <Image /> генерирует производные нужных размеров и форматов, проставляет width/height и убирает скачок вёрстки. Внешние картинки надо явно разрешить в конфиге через image.domains или image.remotePatterns — иначе Astro их не тронет. Обработка тысяч изображений заметно удлиняет сборку, поэтому большие каталоги обычно готовят производные заранее и кладут в CDN.

Полезные сценарии

  • База знаний или документация: markdown в репозитории, схема на frontmatter, поиск через предсобранный индекс, правки приходят пул-реквестом.
  • Каталог, который меняется раз в сутки: ночная сборка тянет товары из API и раскладывает по статическим карточкам.
  • Блог и медиа-раздел с архивом: старые страницы не требуют ни бэкенда, ни базы, ни продлений.
  • Многоязычный сайт: язык становится сегментом маршрута (src/pages/[lang]/...), переводы лежат коллекцией с той же схемой.
  • Лендинги под кампании: общие компоненты, разный контент, отдельная сборка на каждый запуск.
  • Витрина поверх тяжёлого legacy-бэкенда: публичные страницы отдаются статикой, а обращения к старой системе остаются только в островах.

Ограничения

Время сборки растёт вместе с числом страниц. Опечатка в одной статье означает полный прогон и новый деплой, а не правку в админке. На больших объёмах между «поправил» и «увидел на проде» проходят минуты, и это надо закладывать в редакционный процесс.

Контент, меняющийся ежеминутно, сюда не ложится. Остатки на складе, цены, статусы заказов через сборку не проведёшь — либо остров, который ходит в API, либо страница с prerender = false, то есть бэкенд, который снова надо держать живым.

Поиск и фильтры по всей базе из коробки не работают. Нужен либо предсобранный индекс (Pagefind и подобные инструменты умеют строить его по готовому dist/), либо внешний поисковый сервис. Фильтрация каталога по пяти параметрам на клиенте упирается в размер индекса.

Персонализация ломает основную выгоду. Корзина, кабинет, «вы смотрели» — всё это острова с запросами к API, и чем их больше, тем ближе результат к обычному SPA с лишним слоем сверху.

Строгая схема стоит денег при изменениях. Добавили обязательное поле — девятьсот файлов надо мигрировать, иначе сборка не пройдёт. Скрипт миграции frontmatter приходится писать самому.

Переходы между страницами по умолчанию — полная перезагрузка, состояние на клиенте теряется. ClientRouter даёт мягкие переходы, но возвращает в бандл JavaScript, от которого уходили.

Экосистема мельче, чем у больших фреймворков: часть библиотек рассчитывает на клиентский рендер и в статику не собирается, а плагины иногда отстают от свежих версий самого Astro.

Как проверить результат

Запустить astro build и прочитать вывод: сколько страниц собрано, сколько времени заняло, нет ли предупреждений о схеме. Число страниц должно совпадать с ожидаемым — расхождение обычно означает, что getStaticPaths() вернул не весь список (частая причина — фильтр по draft).

Открыть конкретный файл результата и найти в нём текст:

grep -c "ключевая фраза" dist/blog/kakaya-to-statya/index.html

Если фраза в файле есть, её видит и поисковый робот. Если её там нет, а на экране она появляется — контент рисуется островом и в индекс не попадёт.

Поднять astro preview, открыть страницу и посмотреть вкладку Network: у страницы без островов запросов к .js быть не должно вовсе. Каждый лишний бандл ищется по имени файла — оно указывает на компонент.

Прогнать astro check — проверка типов и схем без сборки, быстрее ловит ошибки в шаблонах.

Проверить dist/sitemap-index.xml и связанные с ним файлы: число <url> должно сойтись с числом собранных страниц. Пропавшие URL — признак маршрута, который не попал в getStaticPaths().

Отдельно проверить хостинг: несуществующий адрес должен отдавать код 404, а не 200 с текстом «страница не найдена». Статические хостинги настраиваются на это вручную, и ошибка живёт годами.

Правило одной строкой: если страница нужна поиску, она обязана существовать в dist/ отдельным HTML-файлом с полным текстом внутри, а всё остальное выносится в острова.

  • astro
  • статика
  • ssg
  • frontend