DEV Community

Андрей Викулов (VProger)
Андрей Викулов (VProger)

Posted on • Originally published at viku-lov.ru on

Astro 2025–2026 (часть 3): Actions, API Routes, SSR/SSG и деплой — от разработки до production

Astro 2025–2026 (часть 3): Actions, API Routes, SSR/SSG и деплой — от разработки до production

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: "Спасибо за обращение! Мы свяжемся с вами в ближайшее время.",

};

},

}),

};

Enter fullscreen mode Exit fullscreen mode

Из документации: 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>

Enter fullscreen mode Exit fullscreen mode

При отправке формы (даже без 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>

Enter fullscreen mode Exit fullscreen mode

Функция 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

};

Enter fullscreen mode Exit fullscreen mode

Использование:


---

import { actions } from "astro:actions";

---

<form method="POST" action={actions.user.login}>

<!-- форма авторизации -->

</form>

Enter fullscreen mode Exit fullscreen mode

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",

},

}

);

};

Enter fullscreen mode Exit fullscreen mode

Доступ:

  • 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 }

);

};

Enter fullscreen mode Exit fullscreen mode

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));

};

Enter fullscreen mode Exit fullscreen mode

6) SSR vs SSG: когда использовать каждый режим

Astro поддерживает три режима рендеринга:

  1. Static (SSG) — генерация статических файлов на этапе сборки (по умолчанию)
  2. Server (SSR) — рендеринг на сервере по запросу
  3. Hybrid — SSG по умолчанию, SSR выборочно

6.1) Режим Static (SSG) — по умолчанию


// astro.config.mjs

import { defineConfig } from "astro/config";

export default defineConfig({

// output: "static", // можно не указывать, это дефолт

});

Enter fullscreen mode Exit fullscreen mode

Когда использовать:

  • блоги, документация, маркетинговые сайты;
  • контент редко меняется;
  • динамика на сервере не нужна.

Плюсы:

  • максимальная скорость (статические файлы);
  • недорогой хостинг (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",

}),

});

Enter fullscreen mode Exit fullscreen mode

Когда использовать:

  • приложения с частыми обновлениями;
  • персонализация контента;
  • авторизация и сессии;
  • работа с БД.

Плюсы:

  • динамический контент;
  • доступ к серверным 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(),

});

Enter fullscreen mode Exit fullscreen mode

В Hybrid режиме:

  • по умолчанию все страницы — SSG
  • отдельные страницы можно сделать SSR через export const prerender = false

---

// src/pages/dashboard.astro

export const prerender = false; // эта страница будет SSR

---

# Динамический контент: {new Date().toLocaleString()}

Enter fullscreen mode Exit fullscreen mode

Пример: блог с динамическими просмотрами


---

// 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>

Enter fullscreen mode Exit fullscreen mode

Рекомендация: начни с 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

Enter fullscreen mode Exit fullscreen mode

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

}),

});

Enter fullscreen mode Exit fullscreen mode

Режимы:

  • standalone — самостоятельный сервер (запускается через node dist/server/entry.mjs)
  • middleware — для интеграции с Express/Fastify

Запуск после сборки:


pnpm build

node dist/server/entry.mjs

Enter fullscreen mode Exit fullscreen mode

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

}),

});

Enter fullscreen mode Exit fullscreen mode

Деплой на Netlify:

  1. Подключи репозиторий к Netlify
  2. Build command: pnpm build
  3. Publish directory: dist
  4. 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 секунд

},

}),

});

Enter fullscreen mode Exit fullscreen mode

Деплой на Vercel:

  1. Подключи репозиторий к Vercel
  2. Framework Preset: Astro
  3. Build Command: pnpm build
  4. Output Directory: dist
  5. Деплой автоматический

8) Environment Variables: безопасное хранение секретов

8.1) Создание .env файла


.env

PUBLIC\_API\_URL=https://api.example.com

ADMIN\_TOKEN=super-secret-token

DATABASE\_URL=postgresql://...

Enter fullscreen mode Exit fullscreen mode

Важно:

  • переменные с префиксом 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>

Enter fullscreen mode Exit fullscreen mode

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;

}

Enter fullscreen mode Exit fullscreen mode

После этого TypeScript будет знать о ваших переменных окружения.


9) Деплой на production: пошаговые примеры

9.1) Деплой на Netlify (рекомендуется для начинающих)

Шаг 1: Установи адаптер


pnpm astro add netlify

Enter fullscreen mode Exit fullscreen mode

Шаг 2: Настрой netlify.toml (опционально)


netlify.toml

[build]

command = "pnpm build"

publish = "dist"

[[plugins]]

package = "@astrojs/netlify"

[build.environment]

NODE\_VERSION = "18"

Enter fullscreen mode Exit fullscreen mode

Шаг 3: Подключи репозиторий

  1. Зайди на netlify.com
  2. "Add new site" → "Import an existing project"
  3. Выбери GitHub/GitLab/Bitbucket
  4. Выбери репозиторий
  5. Netlify автоматически определит Astro

Шаг 4: Настрой переменные окружения

  1. Site settings → Environment variables
  2. Добавь секреты (ADMIN_TOKEN, DATABASE_URL)

Готово! При каждом пуше в main ветку сайт автоматически деплоится.

9.2) Деплой на Vercel

Шаг 1: Установи адаптер


pnpm astro add vercel

Enter fullscreen mode Exit fullscreen mode

Шаг 2: Подключи репозиторий

  1. Зайди на vercel.com
  2. "Add New" → "Project"
  3. Выбери репозиторий
  4. Framework Preset: Astro
  5. Build Command: pnpm build

Шаг 3: Добавь переменные окружения

  1. Project Settings → Environment Variables
  2. Добавь секреты

Готово! Деплой автоматический при каждом пуше.

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",

}),

});

Enter fullscreen mode Exit fullscreen mode

Шаг 2: Собери проект


pnpm build

Enter fullscreen mode Exit fullscreen mode

Шаг 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

Enter fullscreen mode Exit fullscreen mode

Шаг 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

Enter fullscreen mode Exit fullscreen mode

Шаг 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

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

12) Итог части 3

В этой части разобрали:

  • Actions : type-safe формы с валидацией Zod
  • API Routes : строить REST endpoints для динамических данных
  • SSR/SSG/Hybrid : выбирать правильный режим рендеринга
  • Адаптеры : настраивать деплой на разные платформы
  • Production : готовить проект к боевому использованию

Что дальше:

В следующих частях цикла можно разобрать:

  • Работу с базами данных (libSQL, PostgreSQL)
  • Авторизацию и сессии
  • Интеграцию с CMS (Strapi, Directus, Contentful)
  • Оптимизацию производительности
  • E2E тестирование

Полезные ссылки:


Итого: Astro 5 даёт всё необходимое для быстрых, SEO-дружелюбных сайтов с серверной логикой там, где она нужна. Actions упрощают формы, API Routes дают гибкость для REST, а hybrid-режим сочетает статику и SSR без лишних затрат.

Read more on viku-lov.ru

Top comments (0)