fix(seo): убрать дубли title и description
- 6 pages (o-nas, nashi-druzya, 4 миры) получили свой description в frontmatter; раньше [slug].astro для типа page передавал description=undefined → fallback на SITE_DESCRIPTION даёт 6 одинаковых meta-description. - [slug].astro: для постов в SEO-title подставляется год публикации (или полная дата, если есть второй пост с тем же title в том же году). Покрывает дубли «Внимание! Технические работы!» (×5) и «С 23 февраля!» (×2). - index.astro: свой description для главной (вместо SITE_DESCRIPTION). - category/[slug].astro: вычисляемый description с количеством публикаций и диапазоном лет — на случай если категории попадут в индексацию.
This commit is contained in:
@@ -5,6 +5,7 @@ legacyId: "132"
|
|||||||
menuOrder: "4"
|
menuOrder: "4"
|
||||||
pubDate: "2011-05-18T00:40:47+03:00"
|
pubDate: "2011-05-18T00:40:47+03:00"
|
||||||
updatedDate: "2014-07-14T00:12:19+03:00"
|
updatedDate: "2014-07-14T00:12:19+03:00"
|
||||||
|
description: "Игровая вселенная по «Хроникам Амбера» Роджера Желязны: Истинный мир, его Отражения и принцы крови с непростой семейной историей."
|
||||||
---
|
---
|
||||||
|
|
||||||
Тест
|
Тест
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ legacyId: "130"
|
|||||||
menuOrder: "5"
|
menuOrder: "5"
|
||||||
pubDate: "2011-05-18T00:39:40+03:00"
|
pubDate: "2011-05-18T00:39:40+03:00"
|
||||||
updatedDate: "2014-07-14T00:12:27+03:00"
|
updatedDate: "2014-07-14T00:12:27+03:00"
|
||||||
|
description: "Ролевая игра по циклу «Киндрэт» Алексея Пехова, Елены Бычковой и Натальи Турчаниновой: вампирские кланы ночной Столицы и их теневая политика."
|
||||||
---
|
---
|
||||||
|
|
||||||
«Киндрэт. Кровные братья» — первая книга цикла Киндрэт известных российских писателей Алексея Пехова, Елены Бычковой и Натальи Турчаниновой, рассказывающая о жизни кланов вампиров. В цикл романов по миру ночной Столицы вошли 4 книги: «Кровные братья», «Колдун из клана смерти», «Основатель», «Новые боги».
|
«Киндрэт. Кровные братья» — первая книга цикла Киндрэт известных российских писателей Алексея Пехова, Елены Бычковой и Натальи Турчаниновой, рассказывающая о жизни кланов вампиров. В цикл романов по миру ночной Столицы вошли 4 книги: «Кровные братья», «Колдун из клана смерти», «Основатель», «Новые боги».
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ legacyId: "124"
|
|||||||
menuOrder: "3"
|
menuOrder: "3"
|
||||||
pubDate: "2011-05-18T00:27:35+03:00"
|
pubDate: "2011-05-18T00:27:35+03:00"
|
||||||
updatedDate: "2014-07-14T00:12:09+03:00"
|
updatedDate: "2014-07-14T00:12:09+03:00"
|
||||||
|
description: "Главная ролевая игра «Иных Отражений» по миру «Дозоров» Сергея Лукьяненко и Владимира Васильева: Иные, Свет и Тьма, древний Договор."
|
||||||
---
|
---
|
||||||
|
|
||||||
**Иные Отражения: Сумерки Дозоров**
|
**Иные Отражения: Сумерки Дозоров**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ legacyId: "339"
|
|||||||
menuOrder: "2"
|
menuOrder: "2"
|
||||||
pubDate: "2013-07-09T13:29:47+03:00"
|
pubDate: "2013-07-09T13:29:47+03:00"
|
||||||
updatedDate: "2025-06-07T02:31:08+03:00"
|
updatedDate: "2025-06-07T02:31:08+03:00"
|
||||||
|
description: "Партнёры и друзья ролевой группы «Иные Отражения»: ООО «АйТи Решения» (hhivp.com) — IT-услуги для бизнеса в Москве и области."
|
||||||
---
|
---
|
||||||
|
|
||||||
[ООО "АйТи Решения"](https://hhivp.com) предоставляет полный спектр IT услуг на территории Москвы и Московской области, как частным лицам, так и представителям бизнеса. Мы способствуем развитию Вашего бизнеса и достижению самых смелых результатов!
|
[ООО "АйТи Решения"](https://hhivp.com) предоставляет полный спектр IT услуг на территории Москвы и Московской области, как частным лицам, так и представителям бизнеса. Мы способствуем развитию Вашего бизнеса и достижению самых смелых результатов!
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ legacyId: "137"
|
|||||||
menuOrder: "1"
|
menuOrder: "1"
|
||||||
pubDate: "2011-05-18T00:50:11+03:00"
|
pubDate: "2011-05-18T00:50:11+03:00"
|
||||||
updatedDate: "2014-07-14T00:10:31+03:00"
|
updatedDate: "2014-07-14T00:10:31+03:00"
|
||||||
|
description: "История ролевой группы «Иные Отражения»: основана в 2006 году как «Иные Миры», в 2007-м объединилась с проектом «Отражения»."
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ролевая группа "Иные Отражения"**
|
**Ролевая группа "Иные Отражения"**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ legacyId: "378"
|
|||||||
menuOrder: "6"
|
menuOrder: "6"
|
||||||
pubDate: "2013-10-06T02:59:22+03:00"
|
pubDate: "2013-10-06T02:59:22+03:00"
|
||||||
updatedDate: "2014-07-14T00:12:33+03:00"
|
updatedDate: "2014-07-14T00:12:33+03:00"
|
||||||
|
description: "«Ренессанс» — ролевая игра по миру «Дозоров» Сергея Лукьяненко: Иные, Сумрак и древний Договор в декорациях эпохи Возрождения."
|
||||||
---
|
---
|
||||||
|
|
||||||
**Иные Отражения: Ренессанс**
|
**Иные Отражения: Ренессанс**
|
||||||
|
|||||||
@@ -6,13 +6,27 @@ import { CATEGORY_COLORS } from '../consts';
|
|||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('posts');
|
const posts = await getCollection('posts');
|
||||||
const pages = await getCollection('pages');
|
const pages = await getCollection('pages');
|
||||||
|
|
||||||
|
// Считаем сколько постов с одинаковым {title, year} — если >1, в SEO-title
|
||||||
|
// подставим полную дату, иначе только год.
|
||||||
|
const titleYearCount = new Map<string, number>();
|
||||||
|
for (const p of posts) {
|
||||||
|
const key = `${p.data.title}|${p.data.pubDate.getFullYear()}`;
|
||||||
|
titleYearCount.set(key, (titleYearCount.get(key) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...posts.map((p) => ({ params: { slug: p.data.slug }, props: { entry: p, kind: 'post' as const } })),
|
...posts.map((p) => {
|
||||||
...pages.map((p) => ({ params: { slug: p.data.slug }, props: { entry: p, kind: 'page' as const } })),
|
const year = p.data.pubDate.getFullYear();
|
||||||
|
const key = `${p.data.title}|${year}`;
|
||||||
|
const sameYearDup = (titleYearCount.get(key) ?? 1) > 1;
|
||||||
|
return { params: { slug: p.data.slug }, props: { entry: p, kind: 'post' as const, sameYearDup } };
|
||||||
|
}),
|
||||||
|
...pages.map((p) => ({ params: { slug: p.data.slug }, props: { entry: p, kind: 'page' as const, sameYearDup: false } })),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entry, kind } = Astro.props;
|
const { entry, kind, sameYearDup } = Astro.props;
|
||||||
const { Content } = await render(entry);
|
const { Content } = await render(entry);
|
||||||
// Буквица — только для постов с телом длиннее короткого порога.
|
// Буквица — только для постов с телом длиннее короткого порога.
|
||||||
const bodyLen = entry.body?.length ?? 0;
|
const bodyLen = entry.body?.length ?? 0;
|
||||||
@@ -20,10 +34,18 @@ const useDropCap = kind === 'post' && bodyLen > 240;
|
|||||||
|
|
||||||
const fmtDate = (d: Date) =>
|
const fmtDate = (d: Date) =>
|
||||||
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
|
||||||
|
// Короткая дата без "г." для SEO-title постов с одинаковым названием в одном году.
|
||||||
|
const fmtDateForTitle = (d: Date) =>
|
||||||
|
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' }).replace(/\sг\.?$/, '');
|
||||||
|
|
||||||
|
const seoTitle = kind === 'post'
|
||||||
|
? `${entry.data.title} (${sameYearDup ? fmtDateForTitle(entry.data.pubDate) : entry.data.pubDate.getFullYear()})`
|
||||||
|
: entry.data.title;
|
||||||
---
|
---
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={entry.data.title}
|
title={seoTitle}
|
||||||
description={kind === 'post' ? entry.data.description : undefined}
|
description={entry.data.description || undefined}
|
||||||
ogType={kind === 'post' ? 'article' : 'website'}
|
ogType={kind === 'post' ? 'article' : 'website'}
|
||||||
>
|
>
|
||||||
<article class="post">
|
<article class="post">
|
||||||
|
|||||||
@@ -27,8 +27,18 @@ const sorted = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.va
|
|||||||
|
|
||||||
const fmtDate = (d: Date) =>
|
const fmtDate = (d: Date) =>
|
||||||
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
|
||||||
|
const yearsRange = (() => {
|
||||||
|
if (!sorted.length) return '';
|
||||||
|
const years = sorted.map((p) => p.data.pubDate.getFullYear());
|
||||||
|
const min = Math.min(...years);
|
||||||
|
const max = Math.max(...years);
|
||||||
|
return min === max ? `${min}` : `${min}–${max}`;
|
||||||
|
})();
|
||||||
|
const pubWord = plural(sorted.length, ['публикация', 'публикации', 'публикаций']);
|
||||||
|
const catDescription = `${name} — ${sorted.length} ${pubWord}${yearsRange ? ` (${yearsRange})` : ''} в архиве ролевой группы «Иные Отражения».`;
|
||||||
---
|
---
|
||||||
<BaseLayout title={`Категория: ${name}`}>
|
<BaseLayout title={`Категория: ${name}`} description={catDescription}>
|
||||||
<section class="hero hero-compact" style={`--cat-color: ${catColor};`}>
|
<section class="hero hero-compact" style={`--cat-color: ${catColor};`}>
|
||||||
<span class="hero-eyebrow" style={`color: ${catColor}; border-color: ${catColor};`}>Категория</span>
|
<span class="hero-eyebrow" style={`color: ${catColor}; border-color: ${catColor};`}>Категория</span>
|
||||||
<h1 style={`background: linear-gradient(180deg, #ffffff 0%, ${catColor} 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;`}>{name}</h1>
|
<h1 style={`background: linear-gradient(180deg, #ffffff 0%, ${catColor} 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;`}>{name}</h1>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const oldestYear = posts.length ? posts[posts.length - 1].data.pubDate.getFullYe
|
|||||||
const fmtDate = (d: Date) =>
|
const fmtDate = (d: Date) =>
|
||||||
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
---
|
---
|
||||||
<BaseLayout>
|
<BaseLayout description="Главная ролевой группы «Иные Отражения»: новости, хроника публикаций с 2009 года и игровые миры — Дозоры, Амбер, Киндрэт, Ренессанс, Над бездной, Глубина, Warhammer 40k.">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<span class="hero-eyebrow">Ролевой проект · с {SITE_FOUNDED} года</span>
|
<span class="hero-eyebrow">Ролевой проект · с {SITE_FOUNDED} года</span>
|
||||||
<h1>Иные<br/>Отражения</h1>
|
<h1>Иные<br/>Отражения</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user