Как настроить 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 не знают домен
- Домен в
</strong> — занимает место в выдаче</li> <li><strong>Одинаковый description</strong> на всех страницах — слабый сигнал для поисковиков</li> <li><strong>Относительный og:image</strong> — соцсети не подхватывают превью</li> <li><strong>В sitemap попадают черновики</strong> — индексируются незавершённые страницы</li> </ol> <p><strong>Результат:</strong> LOW_DEMAND в Яндекс Вебмастере, дубли страниц, плохие сниппеты в выдаче.</p> <hr> <p>Рабочее решение: пошаговая настройка</p> <p>Шаг 1: Задаём site в astro.config.mjs<br> </p> <div class="highlight"><pre class="highlight javascript"><code> <span class="c1">// astro.config.mjs</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">astro/config</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="nf">defineConfig</span><span class="p">({</span> <span class="na">site</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://example.com</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// Полный URL с протоколом</span> <span class="c1">// ...</span> <span class="p">});</span> </code></pre></div> <p></p> <p><strong>Важно:</strong> указывай полный URL с протоколом (https://). Без site интеграция @astrojs/sitemap не заработает.</p> <p>Шаг 2: Canonical только из origin + pathname<br> </p> <div class="highlight"><pre class="highlight typescript"><code> <span class="c1">// В компоненте SeoHead.astro</span> <span class="kd">const</span> <span class="nx">origin</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://example.com</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Из astro.config.mjs site</span> <span class="kd">const</span> <span class="nx">pathname</span> <span class="o">=</span> <span class="nx">Astro</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">search</span> <span class="o">=</span> <span class="nx">Astro</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">search</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">canonical</span> <span class="o">=</span> <span class="nx">$</span><span class="p">{</span><span class="nx">origin</span><span class="p">}</span><span class="nx">$</span><span class="p">{</span><span class="nx">pathname</span><span class="p">}</span><span class="nx">$</span><span class="p">{</span><span class="nx">search</span> <span class="o">||</span> <span class="dl">""</span><span class="p">};</span> </code></pre></div> <p></p> <p><strong>Почему не Astro.url.href:</strong> в dev-режиме Astro.url может быть <a href="http://localhost:4321">http://localhost:4321</a>, а в проде — твой домен. Поисковики хотят видеть <strong>один стабильный каноничный адрес</strong>.</p> <p>Шаг 3: Создаём компонент SeoHead.astro</p> <p>Создай файл src/components/SeoHead.astro:<br> </p> <div class="highlight"><pre class="highlight plaintext"><code> --- 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 /> </code></pre></div> <p></p> <p>Шаг 4: Используем SeoHead в layout<br> </p> <div class="highlight"><pre class="highlight plaintext"><code> --- 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> </code></pre></div> <p></p> <p>Шаг 5: Создаём robots.txt</p> <p>Положи файл в public/robots.txt:<br> </p> <div class="highlight"><pre class="highlight plaintext"><code> User-agent: \* Allow: / Disallow: /admin/ Disallow: /api/ User-agent: Yandex Allow: / Disallow: /admin/ Disallow: /api/ Sitemap: https://example.com/sitemap.xml </code></pre></div> <p></p> <p>Шаг 6: Генерация sitemap.xml</p> <p>Вариант A: интеграция @astrojs/sitemap (SSG)<br> </p> <div class="highlight"><pre class="highlight shell"><code> pnpm astro add sitemap </code></pre></div> <p></p> <p>В astro.config.mjs должен быть задан site. Интеграция сгенерирует sitemap.xml в dist.</p> <p><strong>Ограничение:</strong> в sitemap попадут только статические страницы. Для блога на Content Collections этого обычно достаточно.</p> <p>Вариант B: кастомный endpoint (SSR или полный контроль)</p> <p>Создай файл src/pages/sitemap.xml.ts:<br> </p> <div class="highlight"><pre class="highlight typescript"><code> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">APIRoute</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">astro</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">getCollection</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">astro:content</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">SITE</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://example.com</span><span class="dl">"</span><span class="p">;</span> <span class="kd">function</span> <span class="nf">formatLastmod</span><span class="p">(</span><span class="nx">d</span><span class="p">:</span> <span class="nb">Date</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">):</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">d</span> <span class="p">?</span> <span class="nx">d</span><span class="p">.</span><span class="nf">toISOString</span><span class="p">().</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">T</span><span class="dl">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="p">:</span> <span class="kc">undefined</span><span class="p">;</span> <span class="p">}</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">prerender</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">GET</span><span class="p">:</span> <span class="nx">APIRoute</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getCollection</span><span class="p">(</span><span class="dl">"</span><span class="s2">blog</span><span class="dl">"</span><span class="p">,</span> <span class="p">({</span> <span class="nx">data</span> <span class="p">})</span> <span class="o">=></span> <span class="o">!</span><span class="nx">data</span><span class="p">.</span><span class="nx">draft</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">urls</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="nx">SITE</span><span class="p">,</span> <span class="na">changefreq</span><span class="p">:</span> <span class="dl">"</span><span class="s2">daily</span><span class="dl">"</span><span class="p">,</span> <span class="na">priority</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="nx">$</span><span class="p">{</span><span class="nx">SITE</span><span class="p">}</span><span class="sr">/blog, changefreq: "weekly", priority: "0.9" }</span><span class="err">, </span> <span class="p">...</span><span class="nx">posts</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="o">=></span> <span class="p">({</span> <span class="na">url</span><span class="p">:</span> <span class="nx">$</span><span class="p">{</span><span class="nx">SITE</span><span class="p">}</span><span class="sr">/blog/</span><span class="nx">$</span><span class="p">{</span><span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">slug</span> <span class="o">??</span> <span class="nx">p</span><span class="p">.</span><span class="nx">slug</span><span class="p">},</span> <span class="na">lastmod</span><span class="p">:</span> <span class="nf">formatLastmod</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">updatedDate</span> <span class="o">??</span> <span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">pubDate</span><span class="p">),</span> <span class="na">changefreq</span><span class="p">:</span> <span class="dl">"</span><span class="s2">monthly</span><span class="dl">"</span> <span class="k">as</span> <span class="kd">const</span><span class="p">,</span> <span class="na">priority</span><span class="p">:</span> <span class="dl">"</span><span class="s2">0.8</span><span class="dl">"</span><span class="p">,</span> <span class="p">})),</span> <span class="p">];</span> <span class="kd">const</span> <span class="nx">xml</span> <span class="o">=</span> <span class="s2">`<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </span><span class="p">${</span><span class="nx">urls</span> <span class="p">.</span><span class="nf">map</span><span class="p">(</span> <span class="p">(</span><span class="nx">u</span><span class="p">)</span> <span class="o">=></span> <span class="s2">` <url> <loc></span><span class="p">${</span><span class="nx">u</span><span class="p">.</span><span class="nx">url</span><span class="p">}</span><span class="s2"></loc> </span><span class="p">${</span><span class="nx">u</span><span class="p">.</span><span class="nx">lastmod</span> <span class="p">?</span> <span class="o"><</span><span class="nx">lastmod</span><span class="o">></span><span class="nx">$</span><span class="p">{</span><span class="nx">u</span><span class="p">.</span><span class="nx">lastmod</span><span class="p">}</span><span class="o"><</span><span class="sr">/lastmod> : ""</span><span class="err">} </span> <span class="o"><</span><span class="nx">changefreq</span><span class="o">></span><span class="nx">$</span><span class="p">{</span><span class="nx">u</span><span class="p">.</span><span class="nx">changefreq</span><span class="p">}</span><span class="o"><</span><span class="sr">/changefreq</span><span class="err">> </span> <span class="o"><</span><span class="nx">priority</span><span class="o">></span><span class="nx">$</span><span class="p">{</span><span class="nx">u</span><span class="p">.</span><span class="nx">priority</span><span class="p">}</span><span class="o"><</span><span class="sr">/priority</span><span class="err">> </span> <span class="o"><</span><span class="sr">/url></span><span class="err">` </span> <span class="p">)</span> <span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">)}</span> <span class="o"><</span><span class="sr">/urlset>`</span><span class="err">; </span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Response</span><span class="p">(</span><span class="nx">xml</span><span class="p">,</span> <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/xml; charset=utf-8</span><span class="dl">"</span> <span class="p">},</span> <span class="p">});</span> <span class="p">};</span> </code></pre></div> <p></p> <p>Шаг 7: Настраиваем RSS<br> </p> <div class="highlight"><pre class="highlight shell"><code> pnpm add @astrojs/rss </code></pre></div> <p></p> <p>Создай файл src/pages/rss.xml.ts:<br> </p> <div class="highlight"><pre class="highlight typescript"><code> <span class="k">import</span> <span class="nx">rss</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@astrojs/rss</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">getCollection</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">astro:content</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">GET</span><span class="p">(</span><span class="nx">context</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="nf">getCollection</span><span class="p">(</span><span class="dl">"</span><span class="s2">blog</span><span class="dl">"</span><span class="p">))</span> <span class="p">.</span><span class="nf">filter</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="o">=></span> <span class="o">!</span><span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">draft</span><span class="p">)</span> <span class="p">.</span><span class="nf">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="nx">b</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">pubDate</span><span class="p">.</span><span class="nf">valueOf</span><span class="p">()</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">pubDate</span><span class="p">.</span><span class="nf">valueOf</span><span class="p">());</span> <span class="k">return</span> <span class="nf">rss</span><span class="p">({</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Блог Example</span><span class="dl">"</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Заметки о разработке</span><span class="dl">"</span><span class="p">,</span> <span class="na">site</span><span class="p">:</span> <span class="nx">context</span><span class="p">.</span><span class="nx">site</span><span class="p">?.</span><span class="nx">href</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">https://example.com</span><span class="dl">"</span><span class="p">,</span> <span class="na">items</span><span class="p">:</span> <span class="nx">posts</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="o">=></span> <span class="p">({</span> <span class="na">title</span><span class="p">:</span> <span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">description</span> <span class="o">??</span> <span class="dl">""</span><span class="p">,</span> <span class="na">pubDate</span><span class="p">:</span> <span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">pubDate</span><span class="p">,</span> <span class="na">link</span><span class="p">:</span> <span class="sr">/blog/</span><span class="nx">$</span><span class="p">{</span><span class="nx">p</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">slug</span> <span class="o">??</span> <span class="nx">p</span><span class="p">.</span><span class="nx">slug</span><span class="p">}</span><span class="sr">/</span><span class="err">, </span> <span class="p">})),</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div> <p></p> <p>В <head> основного layout добавь:<br> </p> <div class="highlight"><pre class="highlight html"><code> <span class="nt"><link</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">type=</span><span class="s">"application/rss+xml"</span> <span class="na">title=</span><span class="s">"RSS"</span> <span class="na">href=</span><span class="s">"/rss.xml"</span> <span class="nt">/></span> </code></pre></div> <p></p> <hr> <p>Проверка результата</p> <ol> <li>Проверка canonical </li> </ol> <div class="highlight"><pre class="highlight shell"><code> Проверить заголовок canonical curl <span class="nt">-s</span> https://example.com/blog/astro-part-1/ | <span class="nb">grep </span>canonical Должно быть: <<span class="nb">link </span><span class="nv">rel</span><span class="o">=</span><span class="s2">"canonical"</span> <span class="nv">href</span><span class="o">=</span><span class="s2">"https://example.com/blog/astro-part-1/"</span><span class="o">></span> </code></pre></div> <p></p> <ol> <li>Проверка robots.txt </li> </ol> <div class="highlight"><pre class="highlight shell"><code> Проверить наличие Sitemap curl <span class="nt">-s</span> https://example.com/robots.txt | <span class="nb">grep </span>Sitemap </code></pre></div> <p></p> <ol> <li>Проверка sitemap.xml </li> </ol> <div class="highlight"><pre class="highlight shell"><code> Проверить наличие URL постов curl <span class="nt">-s</span> https://example.com/sitemap.xml | <span class="nb">grep</span> <span class="s2">"blog/"</span> </code></pre></div> <p></p> <ol> <li><p>Проверка в Яндекс Вебмастере</p></li> <li><p>Открыть <a href="https://webmaster.yandex.ru/">Яндекс Вебмастер</a></p></li> <li><p>Добавить сайт</p></li> <li><p>Проверить разделы:</p></li> </ol> <ul> <li><strong>Индексирование → Sitemap</strong> — должна быть загружена</li> <li><strong>Диагностика → Страницы в поиске</strong> — проверить наличие страниц</li> <li><strong>Поведение в выдаче</strong> — посмотреть CTR и позиции</li> </ul> <hr> <p>Типичные ошибки</p> <p>❌ Нет site в конфиге</p> <p><strong>Проблема:</strong> sitemap и RSS не знают домен, canonical собирается неправильно.</p> <p><strong>Решение:</strong> задать site в astro.config.mjs.</p> <p>❌ Canonical из Astro.url.href</p> <p><strong>Проблема:</strong> на localhost и production получаются разные каноничные URL.</p> <p><strong>Решение:</strong> использовать origin из конфига + pathname.</p> <p>❌ Домен в <title></p> <p><strong>Проблема:</strong> занимает место в выдаче.</p> <p><strong>Решение:</strong> убрать домен из title или использовать короткое имя сайта.</p> <p>❌ Одинаковый description на всех страницах</p> <p><strong>Проблема:</strong> слабый сигнал для поисковиков.</p> <p><strong>Решение:</strong> делать уникальные описания до ~155 символов.</p> <p>❌ Относительный og:image</p> <p><strong>Проблема:</strong> соцсети не подхватывают превью.</p> <p><strong>Решение:</strong> использовать абсолютный URL (origin + путь).</p> <p>❌ В sitemap попадают черновики</p> <p><strong>Проблема:</strong> индексируются незавершённые страницы.</p> <p><strong>Решение:</strong> фильтровать по draft: false при генерации sitemap.</p> <hr> <p>Где применять</p> <ul> <li><strong>Блоги на Astro:</strong> индексация статей в Яндексе и Google</li> <li><strong>Документация:</strong> правильное отображение в поиске</li> <li><strong>Маркетинговые сайты:</strong> улучшенные сниппеты в выдаче</li> <li><strong>Портфолио:</strong> индексация проектов</li> </ul> <p><strong>Связанные статьи:</strong></p> <ul> <li>Как создать блог на Astro</li> <li>Actions, API Routes, SSR и деплой</li> </ul> <p><strong>Документация:</strong></p> <ul> <li><a href="https://docs.astro.build/en/reference/configuration-reference/">Astro Configuration Reference</a></li> <li><a href="https://docs.astro.build/en/guides/integrations-guide/sitemap/">Sitemap Integration</a></li> <li><a href="https://docs.astro.build/en/guides/rss/">RSS Guide</a></li> </ul> <hr> <p>Чек-лист для Яндекс Вебмастера</p> <ul> <li>[] <strong>site</strong> в astro.config.mjs задан и совпадает с основным зеркалом</li> <li>[] На каждой странице есть <strong>один</strong> <link rel="canonical"> с абсолютным URL</li> <li>[] <strong>meta description</strong> уникален и длиной до ~155 символов</li> <li>[] <strong><title></strong> без домена, осмысленный для каждой страницы</li> <li>[] <strong>robots.txt</strong> доступен с Sitemap:</li> <li>[] <strong>sitemap.xml</strong> открывается по URL из robots.txt</li> <li>[] <strong>Open Graph</strong> : og:title, og:description, og:url, og:image (абсолютный URL)</li> <li>[] Служебные разделы (/admin/, /api/) закрыты в robots.txt</li> <li>[] <strong>RSS</strong> доступен и объявлен в <head></li> <li>[] Нет дублей страниц (одинаковый контент по разным URL без canonical)</li> </ul> <p><a href="https://viku-lov.ru/blog/astro-seo-sitemap-rss-setup">Read more on viku-lov.ru</a></p>

Top comments (0)