Как настроить canonical, sitemap и RSS в Astro для индексации
Практическая настройка SEO в Astro: canonical из origin, sitemap.xml, robots.txt, RSS, meta description, Open Graph. Чеклист для Яндекс Вебмастера.
Требования
- 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-проектах:
- Canonical из
Astro.url.href— на localhost и production получаются разные URL - Нет
siteв конфиге — sitemap и RSS не знают домен - Домен в
<title>— занимает место в выдаче - Одинаковый description на всех страницах — слабый сигнал для поисковиков
- Относительный
og:image— соцсети не подхватывают превью - В 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. Проверка в Яндекс Вебмастере
- Открыть Яндекс Вебмастер
- Добавить сайт
- Проверить разделы:
- Индексирование → 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)



Комментарии