Astro — статический генератор сайтов, ориентированный на производительность. Фреймворк стремительно набирает популярность и конкурирует с популярным NextJS.

Чем же так хорош Astro? Рассказываем об основных концепциях фреймворка, его архитектурных паттернах, подходах и фишках, которые позволяют достигать высокого уровня оптимизации.

Погружаться в тему будем в процессе сборки блога по фронтенд-разработке. Кстати, весь контент для сайта от имени разработчика для нас сгенерирует ChatGPT.

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

Немного теории

Что такое Astro?

Astro — это веб-фреймворк, статический генератор сайтов, который собирается без JavaScript. С Astro можно использовать любую библиотеку: React, Vue, Angular и другие.

Как правило, современная фронтенд-разработка выглядит примерно так: мы грузим огромный JS-файл, он парсится и только потом уже всё отрисовывает. Да, есть оптимизации, разделение на чанки, отключение JavaScript — но сама проблема производительности никуда не девается.

Astro предлагает нам совершенно другой подход: по умолчанию собирать чистые HTML и CSS, а все динамические элементы помечать вручную. Это называется «архитектурой островов», где вода — HTML и CSS, а островки — JS.

Почему Astro?

Astro любят по нескольким причинам:

  1. Удобство разработки. Под капотом крутится Vite, всё собирается быстро, можно добавлять библиотеки, менять конфиг — и не пересобирать build.

  2. Работа с библиотеками. Мало того что в Astro можно интегрировать любую библиотеку, так их ещё и можно без проблем комбинировать между собой.

  3. Поддержание веб-стандартов. Оно даётся очень легко, когда работаешь с чистыми HTML и CSS.

  4. Автономность продукта. HTML + CSS + CDN — фундамент интернета. Вы строите продукт, заливаете — и он работает без поддержки.

  5. Простота. В отличие от React, изначально созданного для сложного Facebook, Astro идеален для простых сайтов без миллиона зависимостей.

Можно сказать, что Astro позволяет нам строить базовый веб с современными удобствами. Сама по себе задумка интересная — посмотрим, как она реализована на практике.

Практика: фундамент

Создание проекта

Начинаем в консоли — с создания репозитория с пустым Astro-проектом. Если вы запускаете Astro в первый раз, нужно разрешить установку пакета create-astro. Устанавливаем зависимости и выбираем TypeScript.

Изображение
Изображение

В правилах TypeScript выбираем strict (про отличия можно почитать тут) и инициализируем репозиторий.

Изображение
Изображение

Теперь можно переходить в редактор. Мы будем использовать WebStorm. Если у вас он не поддерживает Astro, обновитесь до последней версии. В документации есть подсказки по VS Code и другим инструментам.

В нашем проекте, помимо всего прочего, видим некий файл env.d.ts. Это декларируемый файл TypeScript с типами. Дело в том, что под капотом у нас крутится Vite, а его типы по умолчанию предназначены для API Node.js. Чтобы изменить окружение кода на стороне клиента, как раз и добавляют env.d.ts — в нашем случае Astro это сделал сам.

Переходим в package.json и запускаем сборку проекта:

npm run start

По адресу localhost:3000 видим наш сайт.

Изображение
Изображение

Попробуем изменить один из файлов проекта, чтобы посмотреть, как изменится конфиг. Например, добавим в .gitignore .idea/.

Изображение
Изображение

Мы видим, что после сохранения всё автоматически пересобирается. То есть не нужно перезапускать сборку при каждом изменении файлов. Это мелочь — но суперприятная, потому что очень ускоряет работу на длинной дистанции.

Напоследок настроим prettier, чтобы всё было оформлено красиво:

npm install -DE prettier

echo {}> .prettierrc.json

touch .prettierignore

# build output

dist/

# generated types

.astro/

npx prettier --write .

И всё, наш проект готов к работе — можно переходить непосредственно к освоению Astro.

Создание страниц и роутинг

В первую очередь  научимся создавать новые страницы и переключаться между ними. 

Заходим в SRC и видим папку pages — можно сказать, что она является единственной обязательной, потому что выступает в качестве роутера.

Здесь лежит файл index.astro. Поместим в него контент для главной страницы, в нашем случае — сгенерированный ChatGPT (см. scratches/base-index.html в репозитории).

Изображение
Изображение

Дальше создадим в папке pages Astro-компонент blog.astro — это будущая страница нашего блога. С непонятными чёрточками мы разберёмся чуть позже, а пока просто добавим после них заголовок:

<h1>Blog<h1>

Посмотреть результат можно по адресу localhost:3000/blog. Помните, что роутер чувствителен к регистру!

Изображение
Изображение

Интересно, что по сути мы написали невалидный HTML с одним только «голым» заголовком <h1> — но в браузере видим астровские скрипты <head> и <body>. Подробнее о том, почему так происходит, можно почитать здесь.

Помимо Astro-компонентов, мы можем создавать MD и MDX (тот же маркдаун, но с возможностью импортировать компоненты), HTML и TypeScript/JavaScript-файлы.

Чтобы посмотреть, как это работает на практике, создадим в папке pages три дополнительных файла:

  1. about.md, например, содержащий тот же текст, который нейросеть сгенерировала для главной страницы (см. base-about.md).

  2. account.html с любым рандомным кодом (мы не будем использовать его в этом проекте).

  3. posts.ts с эндпоинтом для API:

export async function get() {

  return {

    body: JSON.stringify([{ id: 1, title: "abc" }]),

  };

}

Посмотрим, как отрабатывает каждый из них:

  1. По адресу localhost:3000/about видим маркдаун, который Astro автоматически распарсил: расставил теги, проставил id заголовкам.

  2. По адресу localhost:3000/account лежит тот HTML-файл, который мы туда положили.

  3. По адресу localhost:3000/posts мы получаем JSON-ответ.

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

npm run build

Появилась директория dist — в ней лежат наши файлы: каждый в своей отдельной папке. Видим, что по факту все они сгенерированы статическими, в том числе ответ от API.

Изображение
Изображение

Создание шаблонов

Наверняка ещё при создании страниц вы подумали, что хорошо бы иметь шаблоны для каждого типа документа. Естественно, Astro предлагает инструменты для их создания.

Для хранения шаблонов создаём папку layouts. Она не является обязательной, однако по соглашению Astro называть её принято именно так, в первую очередь — чтобы проект оставался читабельным для других разработчиков.

В папке layouts создаём файл Layout.astro, копируем туда код из index.astro и очищаем его от контента:

<html lang="ru">

<head>

    <meta charset="utf-8" />

    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />

    <meta name="viewport" content="width=device-width" />

    <meta name="generator" content={Astro.generator} />

    <title>Сайт ChatGPT — фронтендера со стажем</title>

</head>

<body>

	<slot />

</body>

</html>

Обратите внимание на <slot />. По сути своей это то же самое, что и children в React — он нужен, чтобы внутри компонента отображалось содержимое. Правда, от children есть несколько отличий: например, в slot можно вставлять несколько компонентов сразу без родительского.

Шаблон готов — его можно интегрировать в файлы. Так что возвращаемся в index.astro, импортируем Layout (внутри тех самых загадочных чёрточек) и оборачиваем в него  весь контент:

---

import Layout from '../layouts/Layout.astro';

---

<Layout>

	<h1>Hello, Astro</h1>

</Layout>

По аналогии добавляем шаблон в blog.astro и чуть иначе в about.md:

---

layout: ../layouts/Layout.astro

---

# About

Text

А вот к HTML-файлу в Astro шаблон применить нельзя, так что лучше отказаться от этого расширения. Теперь, когда мы об этом знаем, документ account.html можно смело удалять.

Ниже мы подробнее разберём структуру файлов форматов .astro и .md.

Файлы форматов .astro и .md

Что вообще представляет из себя формат .astro?

К файлам этого формата нужно относиться именно как к серверным, а не как к фронтенду. Если вы когда-нибудь работали с PHP, Astro очень на него похож: мы получаем данные на сервере, формируем страницу и отдаём её на клиент. Разве что синтаксис тут более современный, JS-овский.

Когда мы запускаем npm run start, начинает крутиться сервер. Он обрабатывает Astro-файлы и в итоге при запуске npm run build собирает их в обычный HTML.

Что за чёрточки/дефисы мы видим в начале каждого Astro-файла?

По-английски эта штука называется code fence — дословно «забор из кода». Всё, что находится в её границах, выполняется только на сервере и никак не проникает в браузер. Поэтому внутри code fence можно писать пароли от сервисов, токены и другие небезопасные вещи, которые нельзя отдавать на фронтенд.

Обычно «за забором» размещают:

  • импорт компонентов Astro,

  • импорт других компонентов фреймворка, например, React,

  • пропы компонентов,

  • импорт данных, например, JSON-файлов или изображений,

  • получение содержимого из API или базы данных,

  • объявление переменных, на которые собираются ссылаться на клиенте.

Вот как мы можем создать переменную внутри code fence в index.astro:

---

import Layout from '../layouts/Layout.astro';

const title = 'Hello, Astro';

const list = ['a', 'b', 'c'];

---

<Layout>

	<h1>{title}</h1>

	{list.map(item => <p>{item}</p>)}

</Layout>

Вот так — там же прокинуть заголовок страницы:

<Layout title="Главная">

...

</Layout>

И вытащить проп в code fence шаблона Layout.astro:

---

const { title = 'Astro' } = Astro.props;

---

...

<title>{title}</title>

...

Что насчёт маркдаун-файлов?

В маркдаун-файл нельзя прокинуть пропы так же, как в .astro — это нужно делать в другом формате с другим API. Мы можем добавить title в about.md:

---

layout: ../layouts/Layout.astro

title: Обо мне

---

А затем модифицировать Layout.astro, чтобы обрабатывать и обычные пропы, и маркдаун-пропы (frontmatter):

---

const { title = 'Astro', frontmatter = {} } = Astro.props;

const pageTitle = frontmatter.title || title;

---

Другой вариант —  создать  для разделения новый шаблон MdLayout.astro, который будет наследовать основной:

---

import Layout from './Layout.astro';

const { frontmatter } = Astro.props;

---

<Layout title={frontmatter.title}>

    <slot />

</Layout>

Стилизация контента

Мы разобрались со структурой проектов и шаблонами, а значит — пора понять, как в Astro писать CSS. В очередной раз открываем index.astro и прописываем стили:

<style>

	p {

		font-size: 18px;

		font-family: sans-serif;

	}

</style>

Теперь открываем DevTools и видим один класс для всех элементов. Astro создаёт по классу на компонент (в нашем случае компонент — это файл index.astro) и прописывает стили через селектор :where. Довольно креативный способ нативно обеспечить их закрытость.

Изображение
Изображение

Возникает вопрос: «Зачем нужны новые непонятные селекторы, если можно использовать p.astro-dlkjdas — и результат будет тот же?» Всё дело в нулевой специфичности селектора (пример).

Если мы хотим сделать глобальные стили безо всяких :where, нужно добавить is:global:

<style is:global>

	p {

		font-size: 18px;

		font-family: sans-serif;

	}

</style>
Изображение
Изображение

Соберём build и посмотрим на результат. Появилась папка .astro — в ней лежит файл CSS. В index.html Astro специально для этой страницы подключает стили.

Изображение
Изображение

Сборки с is:global и без него выглядят одинаково. Посмотрим вариант с is:inline:

<style is:inline>

	p {

		font-size: 18px;

		font-family: sans-serif;

	}

</style>

Делаем build и видим, что файл CSS исчез, а стили теперь прописаны внутри index.html.

Изображение
Изображение

Также мы можем разместить глобальные стили в отдельном файле и подключить их к шаблону. Для этого создадим папку styles и файл global.css внутри неё:

p {

    font-size: 18px;

    font-family: sans-serif;

}

Подключим стили в Layout.astro:

---

...

import '../styles/content.css';

...

Теперь, чтобы ещё немного попрактиковаться в стилизации, добавим в шаблон Layout.astro шапку для удобного переключения между страницами (см. scratches/base-layout.css). Заодно причешем стили в файле global.css (см. scratches/base-global.css).

Изображение
Изображение

На этом мы не останавливаемся. Поскольку мы находимся на сервере, то можем легко получить доступ к запросу, понять, какую именно страницу запросили, и подсветить соответствующую  ссылку.

Пропишем в шаблоне Layout.astro, что ссылка в шапке должна быть активной, если совпадает с первой частью запрошенного пути (/, /about, /blog):

---

const pathname = new URL(Astro.request.url).pathname;

const currentPath = pathname.split(“/”).filter(Boolean)[0];

---

<header>

  <nav>

    <a href="/" class:list={{ active: currentPath === undefined }}

      >Главная</a

    >

    <a href="/about" class:list={{ active: currentPath === "about" }}

      >Обо мне</a

    >

    <a href="/blog" class:list={{ active: currentPath === "blog" }}>Блог</a>

  </nav>

  <a href="/posts" class:list={{ active: currentPath === "posts" }}>АПИ</a>

</header>

Обратим внимание на два интересных момента:

  1. На то, что в файлах .astro мы добавляем к элементам именно атрибут class, а не className, к которому привыкли в React. 

  1. На список классов class:list, который позволяет в том числе добавлять классы в зависимости от выполнения условия.

Прежде чем переходить к следующему разделу, приведём, наконец, в порядок страницу блога. Для этого наполним файл blog.astro заготовленным контентом: это список будущих статей по фронтенду от ChatGPT (см. base-blog.html).

Изображение
Изображение

Интерактивность

Переходим к интерактивности, то есть к JavaScript и UI-библиотекам.

Скрипты

Начнём с того, что тег <script> добавляется ровно так же, как тег <style> — вне «забора» code fence. Так что смело открываем файл index.astro и вписываем в конец тестовый скрипт:

<script>

  console.log('hello');

</script>
Изображение
Изображение

Делаем build, чтобы посмотреть, как наш скрипт генерится на фронтенде — и видим, что он инлайново встроился в index.html.

Изображение
Изображение

Интересно то, что в скриптах мы можем использовать импорты. Создадим, например, утилиту utils.ts, которая форматирует даты:

export const formatDate = (date = new Date()) => date

    .toLocaleDateString(

        'ru-RU',

        {

            weekday: 'long',

            year: 'numeric',

            month: 'long',

            day: 'numeric',

        });

И импортируем её в index.astro:

<script>

	import { formatDate } from "../utils/formatDate";

	console.log(formatDate());

</script>
Изображение
Изображение

Окей, а что будет, если подключить внешнюю ссылку? Например, React:

<script src="https://unpkg.com/react@18/umd/react.development.js"></script>

Смотрим build и видим, что Astro создал отдельный JS-файл специально под наш импорт.

Изображение
Изображение

Если вы не хотите, чтобы создавался отдельный JS-файл, добавьте is:inline — и скрипт будет импортироваться внутри HTML:

<script is:inline src="https://unpkg.com/react@18/umd/react.development.js"></script>

В целом в Astro всё очень похоже на обычный HTML: интерактивность делается вручную, это неудобно и сложно — поэтому приходится интегрировать библиотеки.

UI-библиотеки

Чтобы научиться работать с библиотеками, реализуем довольно глупую с функциональной точки зрения штуку: добавим на страницу блога кнопку «Я прочитал» и сделаем так, чтобы при нажатии на неё на всю страницу вылетали конфетти.

Представим, мы хотим использовать именно библиотеку react-confetti — аналоги на чистом JS нам по каким-то причинам не подходят. Хорошая новость в том, что в Astro можно интегрировать любую UI-библиотеку, будь то React, Solid, Vue или другие.

Интегрируем React согласно документации:

npx astro add react

В режиме реально времени смотрим, как автоматически обновляются конфиги.

Изображение
Изображение

Подключаем конфетти:

npm i react-confetti-explosion

Дальше, следуя общепринятому соглашению Astro, создаём папку components. В ней размещаем папку ConfettiButton с файлами ConfettiButton.tsx и style.module.css (см. соответственно scratches/ConfettiButton.tsx и scratches/ConfettiButton.css).

Здесь начинается самое интересное. Если мы просто импортируем кнопку в файл blog.astro, при клике ничего не будет происходить. И даже в DevTools мы не увидим react-компонентов.

Это и есть та самая «архитектура островов» в действии. Даже когда мы добавляем JS-библиотеку, которая рисует интерфейс через JavaScript, Astro делает из неё обычные HTML-теги. Интерактивные элементы нужно помечать вручную в файлах .astro.

В нашем случае — надо добавить атрибут client в blog.astro:

import { ConfettiButton } from "../components/ConfettiButton/ConfettiButton.trx";

...

<div class="counter">

	<ConfettiButton client:load />

Заглядываем в Network и — ура — находим наши react-компоненты. Проверяем кнопку и убеждаемся, что она работает.

Изображение
Изображение

Запускаем build и видим, что в сборку добавились JS-файлы.

Изображение
Изображение

Что же представляет собой client, который мы добавили в файл .astro? Это директива, которая отвечает за работу интерактивных элементов:

  • load: загрузка с высоким приоритетом. Используем, когда элемент нужен как можно скорее.

  • idle: загрузка со средним приоритетом. Используем, когда элемент нужен, но пользователь будет взаимодействовать с ним не сразу — важнее, чтобы загрузилось всё остальное.

  • visible: загрузка с низким приоритетом. Загрузка происходит, только когда элемент появляется на экране.

  • only: загрузка с высоким приоритетом, но только на клиенте. Нужно указывать фреймворк, потому что Astro не знает, что вы используете.

Попробуем переключиться на client:only:

<div class="counter">

	<ConfettiButton client:only="react" />

</div>

Теперь, если мы выключим JavaScript, наша кнопка просто пропадёт со страницы.

Изображение
Изображение

Представим другую ситуацию. Мы хотим добавить в блог счётчик прочтений. Компонент уже написан нашей компанией, только вот совершенно на другом фреймворке — скажем, на SolidJS.

Но у нас же React! К счастью, для Astro это не проблема. Мы можем без проблем комбинировать UI-библиотеки. Так что смело интегрируем Solid:

npx astro add solid

Правда, на обновление TS-конфига соглашаться не стоит, ведь большая часть компонентов остаётся на React.

Изображение
Изображение

В уже знакомой нам папке components создаём папку Couter с файлом Counter.tsx:

import styles from "./style.module.css";

interface Props {

  count: number;

}

export const Counter = ({ count = 0 }: Props) => {

  return <div class={styles.counter}>{count}</div>;

};

В ту же папку закидываем стили — style.module.css (см. scratches/Counter.css).

Единственная проблема: в JSX коде Solid (в отличие от React) нет className — только class. А из-за TS-конфига, который мы не стали обновлять, IDE предлагает нам className. 

Изображение
Изображение

Ошибка решается легко: в начало документа нужно добавить комментарий, сообщающий, что мы пишем на Solid:

/** @jsxImportSource solid-js /

Теперь можно подключать счётчик к blog.astro по аналогии с ConfettiButton.

Изображение
Изображение

Работа с состоянием

Логичный следующий этап — соединить счётчик с кнопкой, то есть сделать так, чтобы выводимое значение увеличивалось при каждом нажатии.

Здесь важно понимать: хотя мы можем комбинировать несколько библиотек внутри одного Astro-компонента, мы не можем, например, интегрировать счётчик на Solid в конфетти на React.

Соответственно, нам понадобится некое внешнее состояние. А поскольку клиентского состояния у Astro нет (это просто статические файлы), придётся писать свой или подключать внешний store.

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

npm install nanostores @nanostores/solid @nanostores/react

Создаём папку stores с файлом counterStore.ts внутри:

import { atom } from "nanostores";

export const $counter = atom(0);

export const increaseCounter = () => {

  $counter.set(" class="formula inline">counter.get() + 1);

};

Обновляем ConfettiButton.tsx:

import { increaseCounter } from "../../stores/counterStore";

...

<button

  onClick={() => {

    toggleConfetti(true);

    increaseCounter();

  }}

>

 ...

Обновляем Counter.tsx:

/** @jsxImportSource solid-js */

import styles from "./style.module.css";

import { useStore } from @nanostoress/solid";

import { $counter } from "../../stores/counterStore";

export const Counter = () => {

  const count = useStore(" class="formula inline">counter);

  return <div class={styles.counter}>{count()}</div>;

};

Отправляемся на сайт и видим, что счётчик заработал!

Изображение
Изображение

Обратите внимание, что если мы перейдём на главную страницу, а потом вернёмся в блог, счётчик обнулится. Astro в этом смысле работает по дедовским методам: сайт многостраничный и HTML-файлы никак друг с другом не связаны.

Значит, нам всегда нужно где-то сохранять состояния (в local storage, на бэкенде и т.д.), а при загрузке страницы подгружать их. Большая ли это проблема, зависит от того, насколько много вы работаете с состояниями.

Например, в SPA мы бы могли спокойно переключаться между разделами. Но в то же время при обновлении страницы состояние всё равно пришлось бы откуда-то получать.

На этом фундаментальная часть статьи (про концепцию Astro и его основы) подходит к концу. Начиная со следующего раздела мы будем наполнять блог статьями, работать с сервером и в процессе разбирать некоторые интересные фишки Astro.

Практика: фишки

Коллекции

На странице блога у нас уже давно заготовлено меню статей. Пора, наконец, сделать так, чтобы при клике по ссылкам статьи действительно открывались. Мы разберём вариант, когда материалы хранятся локально в репозитории.

В Astro можно создать коллекцию статей в специальной папке content (это зарезервированное название для коллекций контента). В коллекцию могут входить маркдаун-файлы, JSON-файлы, YAML-файлы и MDX-файлы.

Мы создаём папку content с папкой blog для нашей коллекции внутри и размещаем там маркдаун-файлы со статьями (см. scratches/blog/.md). С теми самыми, которые от лица фронтенд-разработчика написал ChatGPT.

Замечаем, что в сборке сгенерировался непонятный файл .astro/types.

Изображение
Изображение

Открываем его и видим сгенерированные автоматически:

  • декларацию модуля content,

  • кучу разных функций (получение записи по слагу/id, получении коллекции и т.д.),

  • ContentEntryMap c описанием свойств для каждой статьи,

  • какой-то рендер.

Обратите внимание: все названия файлов намеренно написаны через дефисы, т.к. это хорошо читается и парсится под ссылки — так будут называться наши статьи в адресной строке.

Теперь создадим кофиг config.ts в папке content. Это необязательно, но поможет прочувствовать всю прелесть коллекций в Astro:

import { defineCollection, z } from "astro:content";

const blog = defineCollection({

  type: "content",

  schema: z.object({}),

});

export const collections = { blog };

Тип content означает, что мы собираемся работать с маркдаун-файлами (для JSON/YAML мы бы выбрали тип data). 

Для работы со схемами контента Astro использует библиотеку Zod. Идея в том, чтобы проверять frontmatter каждого файла в коллекции и автоматически предоставлять типы TypeScript при запросе содержимого из проекта. 

Кстати, если в процессе создания кофига astro:content загорелся красным, должна помочь перезагрузка сервера.

Вывод статей

Перейдём к выводу статей. Чтобы не создавать отдельную страницу на каждую из них в папке pages, сделаем вложенный динамический роут.

Чтобы получить пути к статьям типа "/blog/article1", "/blog/article2":

  1. Создаём pages/blog.

  2. Переименовываем blog.astro в index.astro и уносим его в папку blog.

  3. В той же папке blog создаём файл […slug].astro (от параметра slug будет генериться роут):

---

import Layout from "../../layouts/Layout.astro";

import { getCollection } from "astro:content";

export async function getStaticPaths() {

  const posts = await getCollection("blog");

  return posts.map((post) => ({

    params: { slug: post.slug },

    props: { post },

  }));

}

const { post } = Astro.props;

const { Content } = await post.render();

---

<Layout>

  <article>

    <Content />

  </article>

</Layout>

Обратим внимание на функцию рендера, которую предоставляет нам коллекция. Она не только возвращает контент (Astro-компонент, переведённый в HTML), но и вытаскивает из маркдаун-файла всякие полезные штуки.

Изображение
Изображение

Кстати, чтобы в случае чего выводилась наша собственная страница «Не найдено», достаточно создать файл 404.astro — и Astro сам его подхватит:

---

import Layout from "../layouts/Layout.astro";

---

<Layout title="Страница не найдена">

  <h1>Страница не найдена ????</h1>

</Layout>
Изображение
Изображение

Наконец, обновляем blog/index.astro, чтобы туда автоматически выводился актуальный список статей из коллекции:

---

import Layout from "../../layouts/Layout.astro";

import { getCollection } from "astro:content";

const posts = await getCollection("blog");

---

<Layout title="Блог">

  <h1>Блог</h1>

  <p>

    В этом разделе вы найдете некоторые статьи, которые я написал, чтобы

    поделиться своими знаниями и опытом в области веб-разработки.

  </p>

  {

    posts.map((post) => {

      return (

        <article>

          <h2>

            <a href={/blog/${post.slug}}>{post.slug}</a>

          </h2>

          <p>Описание</p>

        </article>

      );

    })

  }

</Layout>

Счётчик мы не удаляем, а переносим в […slug].astro — чтобы он считал прочтения каждой конкретной статьи отдельно.

Обновляем код глобально по классу в global.css:

.astro-code {

  padding: 30px 30px;

  border-radius: 20px;

}

Смотрим на результат и фиксируем, чего нам не хватает: названия статей, описания статей и сортировки по дате создания.

Изображение
Изображение

Наша задача — обновить каждую из лежащих в репозитории статей, добавив в начало frontmutter с заголовком, описанием и датой. Мы подготовили маркдаун-файлы с типами заранее (см. scratches/blog-with-types/*.md), поэтому просто заменим их, не останавливая сборку.

Изображение
Изображение

Добавляем типы в config.ts:

import { defineCollection, z } from "astro:content";

const blog = defineCollection({

  type: "content",

  schema: z.object({

    title: z.string(),

    description: z.string(),

    publishedAt: z.string().transform((val) => new Date(val)),

  }),

});

export const collections = { blog };

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

Вносим необходимые изменения в blog/index.astro — добавляем сортировку по дате и вывод заголовка+описания:

---

...

posts.sort((itemA, itemB) => itemB.data.publishedAt - itemA.data.publishedAt);

---

{

  posts.map((post) => {

    return (

      <article>

        <h2>

          <a href={/blog/${post.slug}}>{post.data.title}</a>

        </h2>

        <p>{post.data.description}</p>

      </article>

    );

  })

}
Изображение
Изображение

Для проверки добавим вывод даты в самих статьях […slug].astro:

...

<time datetime={post.data.publishedAt.toISOString()}>

  {formatDate(post.data.publishedAt)}

</time>

<Content />

...
Изображение
Изображение

В сборке видим, что для каждой статьи Astro сделал отдельную папочку с собственным index.html. Готово!

Если вы хотите загружать статьи из админки, алгоритм точно такой же — только вместо getCollection нужен await fetch(url), а вместо рендера надо интегрировать свой маркдаун (документация).

Работа с сервером (SSR)

Работающий сервер даже в условно статическом блоге может быть нужен:

  • чтобы получить контент из админки в реальном времени,

  • чтобы обработать форму заявки,

  • чтобы обойти CORS,

  • чтобы сделать API.

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

Это очень крутая фича. Зачем включать серверный рендеринг всего сайта, если он объективно не нужен на главной странице или странице обо мне? Пусть всё по умолчанию отдаётся в статике.

Допустим, мы хотим сделать форму, которая будет обрабатываться без JS. А ещё нам нужен API-эндпоинт, чтобы отправлять данные на защищённый сервис, который нельзя светить на фронте.

Сперва включим обычный серверный рендеринг. Для этого надо обновить конфиг astro.config.mjs:

export default defineConfig({

  output: 'server',

  ...

});

Ещё для SSR нужно добавить адаптер. Astro должен понимать, где будет крутиться сервер. Есть такие варианты интеграций, но при желании можно сделать свою.

Мы устанавливаем адаптер Netlify, соглашаемся на изменение конфига:

npx astro add netlify

Собираем build и видим, что HTML файлов больше нет. Блог не заводится, потому что мы больше не генерим статику. Зато появилась папка .netlify.

Конечно, так дело не пойдёт: придётся переписывать половину проекта и при этом терять в скорости. Так что включаем гибрид в конфиге astro.config.mjs — и всё возвращается на свои места:

export default defineConfig({

  output: 'hybrid',

  ...

});

Добавляем в pages страницу с формой contact.astro (см. scratches/contact-base.astro).

Изображение
Изображение

Обновляем меню в шаблоне Layout.astro (API мы всё равно не пользуемся, а вот место под форму как раз нужно):

<a href="/contact" class:list={{ active: currentPath === "contact" }}>

  Связаться со мной

</a>

Пишем обработку файла contact.astro, не забыв отменить пререндер:

export const prerender = false;

let message = "";

if (Astro.request.method === "POST") {

  try {

    const data = await Astro.request.formData();

    const email = data.get("email");

    const text = data.get("text");

    // TODO: отправляем данные к себе на сервис

    message = Сообщение отправлено, спасибо! Я отвечу на &lt;i&gt;${email}&lt;/i&gt; в течение дня;

  } catch (error) {

    console.error(error);

  }

}

Теперь при отправке форма нативно обрабатывается за счёт POST-запроса. Фактически можно перевести всё это на React и отправлять данные интерактивно — но если вырубить JS, то форма всё равно будет работать за счёт бэка.

Задеплоить сайт с репозитория GitHub/GitLab на, например, Netlify можно согласно документации.

Выводы

Сейчас Astro идеально подходит как минимум для создания статических сайтов: блогов, новостных порталов, документации, лендингов, каких-то каталогов. Посмотрим, как будет развиваться эта технология дальше. Если в будущем они разрешат делать SPA, то по сути станут универсальным фреймворком без завязки на UI-библиотеке.

Что касается идеологии Astro о том, что JavaScript убивает производительность — её не нужно воспринимать буквально. Всё всегда зависит от конкретного проекта. Иногда быстрее загрузить страницу с нуля, иногда — догрузить нужный контент при переходе. Только замер скорости покажет, как ваш сайт работает быстрее.

Мы не зря назвали эту статью «Введением в Astro»: мы разобрались в основах, поработали со всеми основными инструментами, но точно не покрыли всё. Поэтому, если вы планируете интегрировать Astro в работу, советуем основательно погрузиться в его документацию — вы гарантированно почерпнёте ещё кучу интересных фишек.

–––
В статье упоминается React – продукт организации Meta, признанной экстремистской и запрещённой на территории РФ

Ссылка на репозиторий

Комментарии (6)


  1. aio350
    07.07.2023 05:36
    +1

    Спасибо за статью, но не помешала бы ссылка на репозиторий с кодом проекта.


    1. karpovcourses Автор
      07.07.2023 05:36

      Отличное замечание! Вот ссылка: https://github.com/shalfey41/chatgpt-astro-blog


    1. karpovcourses Автор
      07.07.2023 05:36

      И ещё в саму статью закинули, конечно же


  1. dizatorr
    07.07.2023 05:36

    На мой взгляд, переплюнуть по удобству разработки Гюго не получилось. Использовал его даже для локальных целей, в качестве утилиты для генерации галереи изображений, с публикацией кода для простого копирования. Кидаешь картинки в папочку, нажимаешь сгенерировать и вуаля, готовый набор отмаштабированных изображений с тумбами и код для вставки. Плюсом идёт простая генерация текстов в маркдаун, шаблонизация и если уж так хочется, дополнительные модули на Go.


  1. wertex15
    07.07.2023 05:36

    Подскажите, а как Astro подружить с БД?

    А как статический сайт подружить с некой админкой для изменения сайта секретаршей?


    1. alexnozer
      07.07.2023 05:36

      Разные способы есть. Можно прикрутить какую-нибудь безголовую CMSку, вроде Strapi, Contentful или что-то более привычное секретаршам, типа WordPress или Битрикс. Ну и далее через API ходить за данными в эту CMSку.