← Назад в блог

Как настроить canonical, sitemap и RSS в Astro для индексации

Практическая настройка SEO в Astro: canonical из origin, sitemap.xml, robots.txt, RSS, meta description, Open Graph. Чеклист для Яндекс Вебмастера.

Как настроить canonical, sitemap и RSS в Astro для индексации

Требования

  • Node.js LTS (18+)
  • Базовые знания HTML/CSS/JS
  • Проект на Astro (часть 1)

Как настроить canonical, sitemap и RSS в Astro для индексации

Проблема: страницы блога на Astro не индексируются в Яндексе, попадают в LOW_DEMAND или дублируются в выдаче.

Решение: canonical из origin (не из Astro.url.href), sitemap.xml с фильтрацией черновиков, robots.txt с Sitemap, RSS с автообнаружением, meta description до 155 символов, Open Graph с абсолютными URL.


В чём проблема: почему страницы не индексируются

Типичные ошибки в Astro-проектах:

  1. Canonical из Astro.url.href — на localhost и production получаются разные URL
  2. Нет site в конфиге — sitemap и RSS не знают домен
  3. Домен в <title> — занимает место в выдаче
  4. Одинаковый description на всех страницах — слабый сигнал для поисковиков
  5. Относительный og:image — соцсети не подхватывают превью
  6. В sitemap попадают черновики — индексируются незавершённые страницы

Результат: LOW_DEMAND в Яндекс Вебмастере, дубли страниц, плохие сниппеты в выдаче.


Рабочее решение: пошаговая настройка

Шаг 1: Задаём site в astro.config.mjs

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://example.com",  // Полный URL с протоколом
  // ...
});

Важно: указывай полный URL с протоколом (https://). Без site интеграция @astrojs/sitemap не заработает.

Шаг 2: Canonical только из origin + pathname

// В компоненте SeoHead.astro
const origin = "https://example.com";  // Из astro.config.mjs site
const pathname = Astro.url.pathname;
const search = Astro.url.search;
const canonical = `${origin}${pathname}${search || ""}`;

Почему не Astro.url.href: в dev-режиме Astro.url может быть http://localhost:4321, а в проде — твой домен. Поисковики хотят видеть один стабильный каноничный адрес.

Шаг 3: Создаём компонент SeoHead.astro

Создай файл src/components/SeoHead.astro:

---
const {
  title = "Сайт",
  description = "",
  canonicalUrl,
  ogImage = "/assets/og-default.png",
  noIndex = false,
} = Astro.props;

// Origin из конфига (один источник правды)
const origin = "https://example.com";
const pathname = Astro.url.pathname;
const search = Astro.url.search;
const canonical = canonicalUrl ?? `${origin}${pathname}${search || ""}`;

// Убираем домен из title (если есть)
const titleWithoutDomain = title.replace(/\s*[|\-—]\s*example\.com\s*$/i, "").trim() || title;
const safeDescription = description.slice(0, 155).trim();
---

<title>{titleWithoutDomain}</title>
<meta name="description" content={safeDescription} />
<link rel="canonical" href={canonical} />

{noIndex ? (
  <meta name="robots" content="noindex, nofollow" />
) : (
  <meta name="robots" content="index, follow" />
)}

<!-- Open Graph -->
<meta property="og:title" content={titleWithoutDomain} />
<meta property="og:description" content={safeDescription} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={`${origin}${ogImage}`} />
<meta property="og:locale" content="ru_RU" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={titleWithoutDomain} />
<meta name="twitter:description" content={safeDescription} />
<meta name="twitter:image" content={`${origin}${ogImage}`} />

<slot />

Шаг 4: Используем SeoHead в layout

---
import SeoHead from "../components/SeoHead.astro";
const { title = "Сайт", description = "", canonicalUrl, ogImage, noIndex } = Astro.props;
---

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <SeoHead
    title={title}
    description={description}
    canonicalUrl={canonicalUrl}
    ogImage={ogImage}
    noIndex={noIndex}
  />
</head>
<body>
  <slot />
</body>

Шаг 5: Создаём robots.txt

Положи файл в public/robots.txt:

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/

User-agent: Yandex
Allow: /
Disallow: /admin/
Disallow: /api/

Sitemap: https://example.com/sitemap.xml

Шаг 6: Генерация sitemap.xml

Вариант A: интеграция @astrojs/sitemap (SSG)

pnpm astro add sitemap

В astro.config.mjs должен быть задан site. Интеграция сгенерирует sitemap.xml в dist.

Ограничение: в sitemap попадут только статические страницы. Для блога на Content Collections этого обычно достаточно.

Вариант B: кастомный endpoint (SSR или полный контроль)

Создай файл src/pages/sitemap.xml.ts:

import type { APIRoute } from "astro";
import { getCollection } from "astro:content";

const SITE = "https://example.com";

function formatLastmod(d: Date | undefined): string | undefined {
  return d ? d.toISOString().split("T")[0] : undefined;
}

export const prerender = false;

export const GET: APIRoute = async () => {
  const posts = await getCollection("blog", ({ data }) => !data.draft);
  const urls = [
    { url: SITE, changefreq: "daily", priority: "1.0" },
    { url: `${SITE}/blog`, changefreq: "weekly", priority: "0.9" },
    ...posts.map((p) => ({
      url: `${SITE}/blog/${p.data.slug ?? p.slug}`,
      lastmod: formatLastmod(p.data.updatedDate ?? p.data.pubDate),
      changefreq: "monthly" as const,
      priority: "0.8",
    })),
  ];

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
  .map(
    (u) => `  <url>
    <loc>${u.url}</loc>
    ${u.lastmod ? `    <lastmod>${u.lastmod}</lastmod>` : ""}
    <changefreq>${u.changefreq}</changefreq>
    <priority>${u.priority}</priority>
  </url>`
  )
  .join("\n")}
</urlset>`;

  return new Response(xml, {
    headers: { "Content-Type": "application/xml; charset=utf-8" },
  });
};

Шаг 7: Настраиваем RSS

pnpm add @astrojs/rss

Создай файл src/pages/rss.xml.ts:

import rss from "@astrojs/rss";
import { getCollection } from "astro:content";

export async function GET(context: any) {
  const posts = (await getCollection("blog"))
    .filter((p) => !p.data.draft)
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

  return rss({
    title: "Блог Example",
    description: "Заметки о разработке",
    site: context.site?.href ?? "https://example.com",
    items: posts.map((p) => ({
      title: p.data.title,
      description: p.data.description ?? "",
      pubDate: p.data.pubDate,
      link: `/blog/${p.data.slug ?? p.slug}/`,
    })),
  });
}

В <head> основного layout добавь:

<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />

Проверка результата

1. Проверка canonical

# Проверить заголовок canonical
curl -s https://example.com/blog/astro-part-1/ | grep canonical

# Должно быть:
# <link rel="canonical" href="https://example.com/blog/astro-part-1/">

2. Проверка robots.txt

# Проверить наличие Sitemap
curl -s https://example.com/robots.txt | grep Sitemap

3. Проверка sitemap.xml

# Проверить наличие URL постов
curl -s https://example.com/sitemap.xml | grep "blog/"

4. Проверка в Яндекс Вебмастере

  1. Открыть Яндекс Вебмастер
  2. Добавить сайт
  3. Проверить разделы:
    • Индексирование → Sitemap — должна быть загружена
    • Диагностика → Страницы в поиске — проверить наличие страниц
    • Поведение в выдаче — посмотреть CTR и позиции

Типичные ошибки

❌ Нет site в конфиге

Проблема: sitemap и RSS не знают домен, canonical собирается неправильно.

Решение: задать site в astro.config.mjs.

❌ Canonical из Astro.url.href

Проблема: на localhost и production получаются разные каноничные URL.

Решение: использовать origin из конфига + pathname.

❌ Домен в <title>

Проблема: занимает место в выдаче.

Решение: убрать домен из title или использовать короткое имя сайта.

❌ Одинаковый description на всех страницах

Проблема: слабый сигнал для поисковиков.

Решение: делать уникальные описания до ~155 символов.

❌ Относительный og:image

Проблема: соцсети не подхватывают превью.

Решение: использовать абсолютный URL (origin + путь).

❌ В sitemap попадают черновики

Проблема: индексируются незавершённые страницы.

Решение: фильтровать по draft: false при генерации sitemap.


Где применять

  • Блоги на Astro: индексация статей в Яндексе и Google
  • Документация: правильное отображение в поиске
  • Маркетинговые сайты: улучшенные сниппеты в выдаче
  • Портфолио: индексация проектов

Связанные статьи:

Документация:


Чек-лист для Яндекс Вебмастера

  • site в astro.config.mjs задан и совпадает с основным зеркалом
  • На каждой странице есть один <link rel="canonical"> с абсолютным URL
  • meta description уникален и длиной до ~155 символов
  • <title> без домена, осмысленный для каждой страницы
  • robots.txt доступен с Sitemap:
  • sitemap.xml открывается по URL из robots.txt
  • Open Graph: og:title, og:description, og:url, og:image (абсолютный URL)
  • Служебные разделы (/admin/, /api/) закрыты в robots.txt
  • RSS доступен и объявлен в <head>
  • Нет дублей страниц (одинаковый контент по разным URL без canonical)
Предыдущая часть: Как создать блог на Astro: установка, MDX, Content Collections
0 просмотров

Комментарии

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