Astro 2025–2026 (часть 3): Actions, API Routes, SSR/SSG и деплой — от разработки до production
Astro 5: формы с Actions и Zod, API Routes, SSR и SSG, адаптеры и деплой. Создание форм, REST endpoints, выбор режима рендеринга, настройка Netlify, Vercel, Node.js.
Требования
- Node.js 18+
- Знание Astro из частей 1 и 2
- Понимание HTTP методов (GET, POST)
- Базовые знания серверной разработки
Astro 2025–2026 (часть 3): Actions, API Routes, SSR/SSG и деплой
В первых двух частях мы разобрали фундамент: структуру проекта, MDX, Content Collections, islands и SEO. Теперь углубимся в серверную сторону: как обрабатывать формы через Actions, создавать API endpoints, выбирать между SSR и SSG, и деплоить приложение в production.
План части 3:
- Actions: обработка форм с валидацией Zod и type-safety
- API Routes: создание REST endpoints для динамических данных
- SSR vs SSG: в каких случаях использовать каждый режим
- Адаптеры: настройка для разных платформ
- Деплой: практические примеры для Netlify, Vercel, Node.js
1) Astro Actions: обработка форм без боли
Actions — это официальный способ обработки форм и серверных операций в Astro 5. Альтернатива API routes для форм, но с автоматической валидацией, type-safety и прогрессивным улучшением.
Зачем нужны Actions (когда есть API routes)
API routes хороши для REST API, но для форм есть нюансы:
- нужно вручную парсить
FormData - нет встроенной валидации
- нет автоматической типизации
- сложнее реализовать прогрессивное улучшение (работа без JS)
Actions как раз закрывают эти задачи: валидация, типы и работа без JS уже встроены.
2) Создание первого Action: форма обратной связи
2.1) Определяем Action в src/actions/index.ts
Все Actions должны экспортироваться из объекта server в файле src/actions/index.ts:
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const server = {
// Action для формы обратной связи
contact: defineAction({
// accept: "form" - говорит, что принимаем FormData
accept: "form",
// Схема валидации через Zod
input: z.object({
name: z.string().min(2, "Имя должно быть минимум 2 символа"),
email: z.string().email("Некорректный email"),
message: z.string().min(10, "Сообщение должно быть минимум 10 символов"),
}),
// Обработчик - выполняется на сервере
handler: async (formData) => {
// formData уже валиден и типизирован!
const { name, email, message } = formData;
// Здесь может быть отправка email, запись в БД и т.д.
console.log("Получена заявка:", { name, email, message });
// Пример: отправка в Telegram
// await sendToTelegram({ name, email, message });
// Возвращаем результат (автоматически сериализуется)
return {
success: true,
message: "Спасибо за обращение! Мы свяжемся с вами в ближайшее время.",
};
},
}),
};
Из документации: Actions используют Zod для валидации входных данных на этапе выполнения; данные валидируются перед передачей в handler(). При неуспешной валидации возвращается ошибка.
2.2) Используем Action в HTML-форме
Создаём страницу с формой. Actions работают без JavaScript (прогрессивное улучшение). Для вызова action через action формы страница должна быть отрендерена on-demand: добавь в frontmatter страницы export const prerender = false (или используй режим output: "server" / "hybrid").
---
// src/pages/contacts.astro
import BaseLayout from "../layouts/BaseLayout.astro";
import { actions } from "astro:actions";
// Получаем результат после отправки формы
const result = Astro.getActionResult(actions.contact);
---
<BaseLayout title="Контакты" description="Свяжитесь с нами">
<h1>Контактная форма</h1>
{/* Показываем сообщение об успехе */}
{result?.data?.success && (
<div class="success-message">
{result.data.message}
</div>
)}
{/* Показываем ошибки валидации */}
{result?.error && (
<div class="error-message">
{result.error.message}
</div>
)}
<form method="POST" action={actions.contact}>
<div class="form-group">
<label for="name">Имя</label>
<input
type="text"
id="name"
name="name"
required
/>
{/* Показываем ошибки для конкретного поля */}
{result?.error?.fields?.name && (
<span class="field-error">{Array.isArray(result.error.fields.name) ? result.error.fields.name[0] : result.error.fields.name}</span>
)}
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
/>
{result?.error?.fields?.email && (
<span class="field-error">{Array.isArray(result.error.fields.email) ? result.error.fields.email[0] : result.error.fields.email}</span>
)}
</div>
<div class="form-group">
<label for="message">Сообщение</label>
<textarea
id="message"
name="message"
rows="5"
required
></textarea>
{result?.error?.fields?.message && (
<span class="field-error">{Array.isArray(result.error.fields.message) ? result.error.fields.message[0] : result.error.fields.message}</span>
)}
</div>
<button type="submit">Отправить</button>
</form>
</BaseLayout>
<style>
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
input, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: rgba(0,0,0,0.2);
color: inherit;
}
.success-message {
padding: 1rem;
margin-bottom: 1rem;
background: rgba(0,255,0,0.1);
border: 1px solid rgba(0,255,0,0.3);
border-radius: 4px;
color: #0f0;
}
.error-message {
padding: 1rem;
margin-bottom: 1rem;
background: rgba(255,0,0,0.1);
border: 1px solid rgba(255,0,0,0.3);
border-radius: 4px;
color: #f00;
}
.field-error {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #f00;
}
button {
padding: 0.75rem 1.5rem;
background: rgba(0,180,255,0.2);
border: 1px solid rgba(0,180,255,0.5);
border-radius: 4px;
color: inherit;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: rgba(0,180,255,0.3);
}
</style>
При отправке формы (даже без JS) браузер шлёт POST; Astro валидирует данные по Zod-схеме и при успехе вызывает handler(). Результат доступен через Astro.getActionResult(), страница отдаётся уже с сообщением об успехе или ошибке.
3) Улучшаем Action: добавляем клиентскую обработку
Для лучшего UX можно обрабатывать форму через JavaScript (прогрессивное улучшение):
---
// src/pages/contacts-enhanced.astro
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Контакты (enhanced)" description="Форма с JS">
<h1>Контактная форма (с улучшениями)</h1>
<div id="result-message"></div>
<form id="contact-form">
<div class="form-group">
<label for="name">Имя</label>
<input type="text" id="name" name="name" required />
<span class="field-error" id="name-error"></span>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<span class="field-error" id="email-error"></span>
</div>
<div class="form-group">
<label for="message">Сообщение</label>
<textarea id="message" name="message" rows="5" required></textarea>
<span class="field-error" id="message-error"></span>
</div>
<button type="submit" id="submit-btn">Отправить</button>
</form>
</BaseLayout>
<script>
import { actions, isInputError } from "astro:actions";
const form = document.getElementById("contact-form") as HTMLFormElement;
const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
const resultDiv = document.getElementById("result-message") as HTMLDivElement;
// Очищаем ошибки полей
function clearFieldErrors() {
document.querySelectorAll(".field-error").forEach(el => {
el.textContent = "";
});
}
// Показываем ошибки полей
function showFieldErrors(fields: Record<string, string[]>) {
Object.entries(fields).forEach(([field, errors]) => {
const errorEl = document.getElementById(`${field}-error`);
if (errorEl && errors.length > 0) {
errorEl.textContent = errors[0];
}
});
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
// Очищаем предыдущие ошибки
clearFieldErrors();
resultDiv.textContent = "";
// Блокируем кнопку
submitBtn.disabled = true;
submitBtn.textContent = "Отправка...";
try {
// Получаем данные формы
const formData = new FormData(form);
// Вызываем Action
const { data, error } = await actions.contact(formData);
if (error) {
// Проверяем, это ошибка валидации или серверная ошибка
if (isInputError(error)) {
// Показываем ошибки полей
showFieldErrors(error.fields);
resultDiv.innerHTML = `<div class="error-message">${error.message}</div>`;
} else {
// Серверная ошибка
resultDiv.innerHTML = `<div class="error-message">Произошла ошибка: ${error.message}</div>`;
}
} else if (data) {
// Успех!
resultDiv.innerHTML = `<div class="success-message">${data.message}</div>`;
form.reset();
}
} catch (err) {
resultDiv.innerHTML = `<div class="error-message">Произошла непредвиденная ошибка</div>`;
console.error(err);
} finally {
// Разблокируем кнопку
submitBtn.disabled = false;
submitBtn.textContent = "Отправить";
}
});
</script>
<style>
/* ... те же стили, что и выше ... */
</style>
Функция isInputError() из astro:actions различает ошибки валидации полей (input errors) и остальные ошибки, что удобно при обработке на клиенте.
4) Группировка Actions: организация кода
Когда Actions становится много, их удобно группировать в отдельные файлы:
// src/actions/user.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const user = {
login: defineAction({
accept: "form",
input: z.object({
email: z.string().email(),
password: z.string().min(8),
}),
handler: async ({ email, password }) => {
// Логика авторизации
return { success: true, token: "..." };
},
}),
register: defineAction({
accept: "form",
input: z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
}),
handler: async ({ name, email, password }) => {
// Логика регистрации
return { success: true };
},
}),
};
// src/actions/blog.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const blog = {
like: defineAction({
accept: "json",
input: z.object({
slug: z.string(),
}),
handler: async ({ slug }) => {
// Логика лайка
return { likes: 42 };
},
}),
};
// src/actions/index.ts
import { user } from "./user";
import { blog } from "./blog";
export const server = {
user, // actions.user.login, actions.user.register
blog, // actions.blog.like
};
Использование:
---
import { actions } from "astro:actions";
---
<form method="POST" action={actions.user.login}>
<!-- форма авторизации -->
</form>
5) API Routes: когда Actions недостаточно
API Routes — это серверные endpoints для REST API. Их имеет смысл использовать, когда:
- нужен именно REST API (а не формы);
- нужна работа с разными HTTP-методами;
- нужна интеграция с внешними сервисами;
- нужен webhook.
5.1) Создание простого API endpoint
// src/pages/api/blog/[slug]/view.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ params }) => {
const { slug } = params;
// Здесь может быть запрос к БД
const views = Math.floor(Math.random() * 1000);
return new Response(
JSON.stringify({ slug, views }),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
};
export const POST: APIRoute = async ({ params, request }) => {
const { slug } = params;
// Увеличиваем счётчик просмотров
// await incrementViews(slug);
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
};
Доступ:
GET /api/blog/my-post/view— получить количество просмотровPOST /api/blog/my-post/view— зарегистрировать просмотр
5.2) Обработка разных HTTP методов
// src/pages/api/products/[id].ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ params }) => {
// Получить продукт
return new Response(JSON.stringify({ id: params.id, name: "Product" }));
};
export const PUT: APIRoute = async ({ params, request }) => {
// Обновить продукт
const body = await request.json();
return new Response(JSON.stringify({ success: true }));
};
export const DELETE: APIRoute = async ({ params }) => {
// Удалить продукт
return new Response(JSON.stringify({ success: true }));
};
// Fallback для неподдерживаемых методов
export const ALL: APIRoute = async ({ request }) => {
return new Response(
JSON.stringify({ error: `Method ${request.method} not supported` }),
{ status: 405 }
);
};
5.3) Авторизация в API Routes
// src/pages/api/admin/posts.ts
import type { APIRoute } from "astro";
const ADMIN_TOKEN = import.meta.env.ADMIN_TOKEN || "admin123";
export const GET: APIRoute = async ({ request }) => {
// Проверяем токен
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (token !== ADMIN_TOKEN) {
return new Response(
JSON.stringify({ error: "Unauthorized" }),
{ status: 401 }
);
}
// Возвращаем данные
const posts = [/* данные */];
return new Response(JSON.stringify(posts));
};
6) SSR vs SSG: когда использовать каждый режим
Astro поддерживает три режима рендеринга:
- Static (SSG) — генерация статических файлов на этапе сборки (по умолчанию)
- Server (SSR) — рендеринг на сервере по запросу
- Hybrid — SSG по умолчанию, SSR выборочно
6.1) Режим Static (SSG) — по умолчанию
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
// output: "static", // можно не указывать, это дефолт
});
Когда использовать:
- блоги, документация, маркетинговые сайты;
- контент редко меняется;
- динамика на сервере не нужна.
Плюсы:
- максимальная скорость (статические файлы);
- недорогой хостинг (Netlify, Vercel, Cloudflare Pages);
- CDN из коробки.
Минусы:
- при изменении контента нужна пересборка;
- серверная логика недоступна.
6.2) Режим Server (SSR) — всё на сервере
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone",
}),
});
Когда использовать:
- приложения с частыми обновлениями;
- персонализация контента;
- авторизация и сессии;
- работа с БД.
Плюсы:
- динамический контент;
- доступ к серверным API;
- работа с cookies и headers.
Минусы:
- нужен постоянно работающий сервер (дороже);
- медленнее статики.
6.3) Режим Hybrid — лучшее из двух миров
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "hybrid", // SSG по умолчанию
adapter: node(),
});
В Hybrid режиме:
- по умолчанию все страницы — SSG
- отдельные страницы можно сделать SSR через
export const prerender = false
---
// src/pages/dashboard.astro
export const prerender = false; // эта страница будет SSR
---
<h1>Динамический контент: {new Date().toLocaleString()}</h1>
Пример: блог с динамическими просмотрами
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
// Генерируем статические страницы для всех постов
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map(post => ({
params: { slug: post.data.slug ?? post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
// Получаем динамические просмотры через API
const slug = post.data.slug ?? post.slug;
const viewsRes = await fetch(`https://example.com/api/blog/${slug}/view`);
const { views } = await viewsRes.json();
---
<article>
<h1>{post.data.title}</h1>
<p>Просмотров: {views}</p>
<Content />
</article>
Рекомендация: начни с Hybrid и включай SSR только там, где он действительно нужен.
7) Адаптеры: настройка для разных платформ
Для SSR нужен adapter — интеграция, которая адаптирует Astro под конкретную платформу.
7.1) Установка адаптера
# Node.js
pnpm astro add node
# Netlify
pnpm astro add netlify
# Vercel
pnpm astro add vercel
# Cloudflare
pnpm astro add cloudflare
# Deno
pnpm add @deno/astro-adapter
7.2) Настройка Node.js адаптера
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone", // standalone | middleware
}),
});
Режимы:
standalone— самостоятельный сервер (запускается черезnode dist/server/entry.mjs)middleware— для интеграции с Express/Fastify
Запуск после сборки:
pnpm build
node dist/server/entry.mjs
7.3) Настройка Netlify адаптера
// astro.config.mjs
import { defineConfig } from "astro/config";
import netlify from "@astrojs/netlify";
export default defineConfig({
output: "server",
adapter: netlify({
edgeMiddleware: true, // использовать Edge Functions для middleware
}),
});
Деплой на Netlify:
- Подключи репозиторий к Netlify
- Build command:
pnpm build - Publish directory:
dist - Netlify автоматически определит Astro и настроит всё
7.4) Настройка Vercel адаптера
// astro.config.mjs
import { defineConfig } from "astro/config";
import vercel from "@astrojs/vercel/serverless";
export default defineConfig({
output: "server",
adapter: vercel({
isr: {
// Incremental Static Regeneration
expiration: 60, // ревалидация каждые 60 секунд
},
}),
});
Деплой на Vercel:
- Подключи репозиторий к Vercel
- Framework Preset: Astro
- Build Command:
pnpm build - Output Directory:
dist - Деплой автоматический
8) Environment Variables: безопасное хранение секретов
8.1) Создание .env файла
# .env
PUBLIC_API_URL=https://api.example.com
ADMIN_TOKEN=super-secret-token
DATABASE_URL=postgresql://...
Важно:
- переменные с префиксом
PUBLIC_доступны на клиенте - остальные доступны только на сервере
8.2) Использование в коде
---
// Серверная переменная (только на сервере)
const adminToken = import.meta.env.ADMIN_TOKEN;
// Публичная переменная (доступна на клиенте)
const apiUrl = import.meta.env.PUBLIC_API_URL;
---
<script>
// Публичная переменная доступна
console.log(import.meta.env.PUBLIC_API_URL);
// Серверная переменная НЕ доступна (undefined)
console.log(import.meta.env.ADMIN_TOKEN); // undefined
</script>
8.3) Типизация environment variables
// src/env.d.ts
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly ADMIN_TOKEN: string;
readonly DATABASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
После этого TypeScript будет знать о ваших переменных окружения.
9) Деплой на production: пошаговые примеры
9.1) Деплой на Netlify (рекомендуется для начинающих)
Шаг 1: Установи адаптер
pnpm astro add netlify
Шаг 2: Настрой netlify.toml (опционально)
# netlify.toml
[build]
command = "pnpm build"
publish = "dist"
[[plugins]]
package = "@astrojs/netlify"
[build.environment]
NODE_VERSION = "18"
Шаг 3: Подключи репозиторий
- Зайди на netlify.com
- “Add new site” → “Import an existing project”
- Выбери GitHub/GitLab/Bitbucket
- Выбери репозиторий
- Netlify автоматически определит Astro
Шаг 4: Настрой переменные окружения
- Site settings → Environment variables
- Добавь секреты (
ADMIN_TOKEN,DATABASE_URL)
Готово! При каждом пуше в main ветку сайт автоматически деплоится.
9.2) Деплой на Vercel
Шаг 1: Установи адаптер
pnpm astro add vercel
Шаг 2: Подключи репозиторий
- Зайди на vercel.com
- “Add New” → “Project”
- Выбери репозиторий
- Framework Preset: Astro
- Build Command:
pnpm build
Шаг 3: Добавь переменные окружения
- Project Settings → Environment Variables
- Добавь секреты
Готово! Деплой автоматический при каждом пуше.
9.3) Деплой на VPS с Node.js (самый гибкий вариант)
Шаг 1: Настрой проект
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone",
}),
});
Шаг 2: Собери проект
pnpm build
Шаг 3: Настрой сервер
# На VPS
# 1. Установи Node.js
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
# 2. Скопируй проект на сервер
scp -r dist user@server:/var/www/mysite/
scp package.json user@server:/var/www/mysite/
scp pnpm-lock.yaml user@server:/var/www/mysite/
# 3. Установи зависимости
ssh user@server
cd /var/www/mysite
pnpm install --prod
# 4. Запусти сервер
node dist/server/entry.mjs
Шаг 4: Настрой systemd (автозапуск)
# /etc/systemd/system/astro-site.service
[Unit]
Description=Astro Site
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/mysite
EnvironmentFile=/var/www/mysite/.env
ExecStart=/usr/bin/node dist/server/entry.mjs
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable astro-site
sudo systemctl start astro-site
sudo systemctl status astro-site
Шаг 5: Настрой Nginx (reverse proxy)
# /etc/nginx/sites-available/mysite
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:4321;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
10) Production-ready: чеклист перед деплоем
10.1) Безопасность
- Все секреты в environment variables (не в коде!)
-
.envв.gitignore - HTTPS настроен (Let’s Encrypt)
- CORS настроен правильно (если есть API)
- Rate limiting для API endpoints
- Валидация всех входных данных
10.2) Производительность
- Изображения оптимизированы (через
<Image />) - CSS/JS минифицированы (автоматически при build)
- Используется CDN для статики
- Настроено кеширование (Cache-Control headers)
- Lazy loading для изображений ниже фолда
10.3) SEO
- Sitemap.xml настроен
- RSS лента настроена
- Canonical URLs корректные
- OpenGraph метатеги на всех страницах
- robots.txt настроен
10.4) Мониторинг
- Логирование ошибок (Sentry, LogRocket)
- Аналитика (Google Analytics, Plausible)
- Uptime monitoring (UptimeRobot, Pingdom)
11) Практический пример: блог с лайками и просмотрами
Создадим полноценный пример, объединяющий всё изученное:
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const server = {
blog: {
like: defineAction({
accept: "json",
input: z.object({
slug: z.string(),
}),
handler: async ({ slug }, context) => {
// Получаем IP из контекста
const ip = context.clientAddress;
// Проверяем, не лайкал ли пользователь уже
// (здесь должна быть БД, например libSQL)
const hasLiked = false; // await checkIfLiked(slug, ip);
if (hasLiked) {
throw new Error("Вы уже лайкали этот пост");
}
// Добавляем лайк
// await addLike(slug, ip);
// Возвращаем новое количество
const likes = 42; // await getLikesCount(slug);
return { likes };
},
}),
},
};
// src/pages/api/blog/[slug]/view.ts
import type { APIRoute } from "astro";
export const prerender = false; // SSR для этого endpoint
export const GET: APIRoute = async ({ params }) => {
const { slug } = params;
// Получаем количество просмотров из БД
// const views = await getViewsCount(slug);
const views = Math.floor(Math.random() * 1000);
return new Response(
JSON.stringify({ views }),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60", // кешируем на 1 минуту
},
}
);
};
export const POST: APIRoute = async ({ params, request }) => {
const { slug } = params;
const ip = request.headers.get("x-forwarded-for") || "unknown";
// Регистрируем просмотр
// await incrementView(slug, ip);
return new Response(
JSON.stringify({ success: true }),
{ status: 200 }
);
};
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map(post => ({
params: { slug: post.data.slug ?? post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<h1>{post.data.title}</h1>
<div class="stats">
<span id="views-count">... просмотров</span>
<button id="like-btn" type="button">
❤️ <span id="likes-count">...</span>
</button>
</div>
<Content />
</article>
</BaseLayout>
<script>
import { actions } from "astro:actions";
const slug = window.location.pathname.split("/").pop()!;
// Загружаем просмотры
async function loadViews() {
const res = await fetch(`/api/blog/${slug}/view`);
const { views } = await res.json();
document.getElementById("views-count")!.textContent = `${views} просмотров`;
}
// Регистрируем просмотр
async function registerView() {
await fetch(`/api/blog/${slug}/view`, { method: "POST" });
}
// Обработка лайка
const likeBtn = document.getElementById("like-btn")!;
const likesCount = document.getElementById("likes-count")!;
likeBtn.addEventListener("click", async () => {
try {
const { data, error } = await actions.blog.like({ slug });
if (error) {
alert(error.message);
} else if (data) {
likesCount.textContent = String(data.likes);
likeBtn.disabled = true;
}
} catch (err) {
console.error(err);
alert("Ошибка при лайке");
}
});
// Инициализация
loadViews();
registerView();
</script>
<style>
.stats {
display: flex;
gap: 1rem;
margin: 1rem 0;
padding: 1rem;
background: rgba(0,0,0,0.2);
border-radius: 8px;
}
#like-btn {
padding: 0.5rem 1rem;
background: rgba(255,0,100,0.2);
border: 1px solid rgba(255,0,100,0.5);
border-radius: 4px;
cursor: pointer;
color: inherit;
}
#like-btn:hover:not(:disabled) {
background: rgba(255,0,100,0.3);
}
#like-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
12) Итог части 3
В этой части разобрали:
- Actions: type-safe формы с валидацией Zod
- API Routes: строить REST endpoints для динамических данных
- SSR/SSG/Hybrid: выбирать правильный режим рендеринга
- Адаптеры: настраивать деплой на разные платформы
- Production: готовить проект к боевому использованию
Что дальше:
В следующих частях цикла можно разобрать:
- Работу с базами данных (libSQL, PostgreSQL)
- Авторизацию и сессии
- Интеграцию с CMS (Strapi, Directus, Contentful)
- Оптимизацию производительности
- E2E тестирование
Полезные ссылки:
- Официальная документация Astro
- Actions API Reference
- Endpoints Guide
- On-demand Rendering
- Deploy Guide
Итого: Astro 5 даёт всё необходимое для быстрых, SEO-дружелюбных сайтов с серверной логикой там, где она нужна. Actions упрощают формы, API Routes дают гибкость для REST, а hybrid-режим сочетает статику и SSR без лишних затрат.



Комментарии