← Назад в блог

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

Astro 5: формы с Actions и Zod, API Routes, SSR и SSG, адаптеры и деплой. Создание форм, REST endpoints, выбор режима рендеринга, настройка Netlify, Vercel, Node.js.

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

Требования

  • 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 поддерживает три режима рендеринга:

  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", // можно не указывать, это дефолт
});

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

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

Плюсы:

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

  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 секунд
    },
  }),
});

Деплой на 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://...

Важно:

  • переменные с префиксом 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: Подключи репозиторий

  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

Шаг 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",
  }),
});

Шаг 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 5 даёт всё необходимое для быстрых, SEO-дружелюбных сайтов с серверной логикой там, где она нужна. Actions упрощают формы, API Routes дают гибкость для REST, а hybrid-режим сочетает статику и SSR без лишних затрат.

Предыдущая часть: Astro 2025–2026 (часть 2): islands, гидратация, View Transitions и SEO
0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!