Как добавить RSS и sitemap в Astro
RSS и sitemap.xml — два маленьких файла, без которых блог в 2026 году выглядит несерьёзно. RSS читают агрегаторы (от Feedly до Inoreader) и куча уютных читалок. Sitemap нужен поисковикам, чтобы они быстрее находили страницы. В Astro оба ставятся за пять минут — но есть моменты, на которых я в своё время спотыкалась.
Sitemap через официальную интеграцию
Astro поставляет официальную интеграцию @astrojs/sitemap. Ставится одной командой:
pnpm astro add sitemapКоманда подтянет пакет и добавит интеграцию в astro.config.mjs. После этого обязательно укажи site — sitemap без полного URL не соберётся:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [sitemap()],
});После сборки в dist/ появятся sitemap-index.xml и sitemap-0.xml. Если страниц больше 45 000, интеграция автоматически разрежет sitemap на несколько файлов.
Что положить в robots.txt
Sitemap сам себя не пропишет в robots.txt. У меня public/robots.txt выглядит так:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap-index.xmlБез этой строчки Google и Яндекс всё равно найдут карту, но Bing и часть мелких ботов — нет.
Фильтрация и приоритеты
Sitemap по умолчанию включает все страницы. Если у тебя есть служебные (страница входа, превью статей, теги без контента) — отфильтруй:
sitemap({
filter: (page) => !page.includes('/admin/') && !page.includes('/draft/'),
changefreq: 'weekly',
priority: 0.7,
serialize: (item) => {
if (item.url === 'https://example.com/') {
return { ...item, changefreq: 'daily', priority: 1.0 };
}
return item;
},
});Поля priority и changefreq поисковики читают как подсказки. Не надо делать всё «1.0» — просто проигнорируют. Приоритет 1.0 — главная, 0.8 — основные разделы, 0.6–0.7 — статьи, 0.4 — служебные.
RSS: ставится отдельно
Для 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) {
const posts = await getCollection('blog');
return rss({
title: 'Заметки Полины',
description: 'О Astro, MDX и контенте',
site: context.site,
items: posts
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/p/${post.slug}/`,
})),
});
}После билда файл будет доступен по /rss.xml. Я обычно добавляю его в <head>, чтобы браузеры сразу видели feed:
<link
rel="alternate"
type="application/rss+xml"
title="Заметки Полины"
href="/rss.xml"
/>Полные тексты или анонсы
Самый частый вопрос — отдавать в RSS только описание или полный текст статьи. Зависит от целей. Если хочется, чтобы читатели подписывались на сайт и заходили внутрь — короткое описание со ссылкой. Если важна доступность контента где угодно — полный текст в content:
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/p/${post.slug}/`,
content: post.body,
}));В моих проектах работает гибрид: описание короткое, полный текст уезжает только если есть тег full-rss.
Картинки и enclosures
Если хочешь, чтобы RSS-читалки показывали обложку поста — добавь enclosure:
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/p/${post.slug}/`,
enclosure: post.data.cover
? { url: post.data.cover, length: 0, type: 'image/webp' }
: undefined,
}));Длину можно проставить нулём — большинство читалок её игнорируют.
Локализация
Если у тебя сайт на нескольких языках, сделай отдельные feed-маршруты: /ru/rss.xml и /en/rss.xml. И обязательно прописывай в каждом свой title и language:
return rss({
title: 'Полина — записи',
description: '...',
site: context.site,
customData: '<language>ru-RU</language>',
items,
});Sitemap для SSR-страниц
Интеграция @astrojs/sitemap работает только с теми страницами, которые попали в dist/ после билда. Если у тебя SSR-маршрут вроде /p/[slug] и страницы существуют в БД — интеграция о них не узнает. Решений два:
- Сделать страницы полностью статичными через
getStaticPaths, если данные доступны на момент билда. - Сгенерировать sitemap самостоятельно — отдельным маршрутом
src/pages/sitemap.xml.ts, в котором ты сам перечислишь URL из БД.
Второй вариант я использую для блога с большой динамической частью. Выглядит так:
export async function GET() {
const posts = await db.post.findMany({ where: { published: true } });
const items = posts
.map(
(p) => `<url><loc>https://example.com/p/${p.slug}/</loc>` +
`<lastmod>${p.updatedAt.toISOString()}</lastmod></url>`
)
.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${items}</urlset>`;
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
}Что проверить после деплоя
- Открой
/sitemap-index.xmlи убедись, что в нём перечислены все нужные подкарты. - Открой
/rss.xmlв браузере — XML должен быть валидным, без обрезанных тегов. - Прогон через
https://validator.w3.org/feed/ловит большинство ошибок. - Sitemap отправь в Search Console (Google) и Вебмастер (Яндекс).
В сухом остатке
RSS и sitemap — пара файлов, которая стоит десяти минут работы и заметно улучшает индексацию и доступность. Я ставлю их в любой проект, где есть хотя бы три страницы контента, и обновляю по мере роста сайта. Без этого выходишь в эфир «без визитки» — найти такой сайт сложнее, а подписаться невозможно.