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="Свяжитесь с нами">
# Контактная форма
{/ _Показываем сообщение об успехе_ /}
{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">
# Контактная форма (с улучшениями)
<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 };
},
}),
};
ts
// 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 };
},
}),
};
ts
// 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
---
# Динамический контент: {new Date().toLocaleString()}
Пример: блог с динамическими просмотрами
---
// 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>
# {post.data.title}
Просмотров: {views}
<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
bash
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;
}
}
bash
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) Производительность
- [] Изображения оптимизированы (через )
- [] 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 };
},
}),
},
};
ts
// 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 }
);
};
astro
---
// 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>
# {post.data.title}
<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 без лишних затрат.

Top comments (0)