feat: страница /kontakty/, robots.txt, ai.txt, llms.txt, sitemap.txt

- /kontakty/ — email, Telegram, ВКонтакте + сетка игровых форумов
- Меню обновлено: добавлен пункт «Контакты»
- public/robots.txt — статика, разрешено всё; Host для Yandex; перечислены
  AI-краулеры (GPTBot, ClaudeBot, Google-Extended, CCBot, PerplexityBot,
  anthropic-ai); ссылки на оба sitemap (XML + TXT)
- public/ai.txt — Train/Cite/Quote: yes, контактная почта
- public/llms.txt — структура llmstxt.org с описанием проекта, ключевыми
  страницами, форумами, RSS-фидами, контактами
- src/pages/sitemap.txt.ts — endpoint, генерит plain-text список 66 URL
  (главная + статика + страницы + посты + категории)
This commit is contained in:
2026-05-21 01:51:51 +03:00
parent fce2278129
commit 841826ba8e
7 changed files with 214 additions and 0 deletions

View File

@@ -3,6 +3,8 @@ export const SOCIAL = {
vk: { url: 'https://vk.com/anotherreflections', label: 'ВКонтакте' },
};
export const CONTACT_EMAIL = 'info@anotherreflections.ru';
/** Русская плюрализация: [1, 2-4, 5+]. Пример: plural(8, ['мир', 'мира', 'миров']) → 'миров' */
export function plural(n: number, forms: [string, string, string]): string {
const a = Math.abs(n) % 100;
@@ -127,4 +129,5 @@ export const MAIN_NAV = [
{ label: 'О нас', href: '/o-nas/' },
{ label: 'Миры', href: '/miry/' },
{ label: 'Друзья', href: '/nashi-druzya/' },
{ label: 'Контакты', href: '/kontakty/' },
];

50
src/pages/kontakty.astro Normal file
View File

@@ -0,0 +1,50 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { CONTACT_EMAIL, SOCIAL, WORLDS } from '../consts';
---
<BaseLayout
title="Контакты"
description={`Связаться с проектом «Иные Отражения»: ${CONTACT_EMAIL}, Telegram, ВКонтакте.`}
>
<section class="hero" style="padding: 4rem 1rem 2rem;">
<span class="hero-eyebrow">На связи</span>
<h1>Контакты</h1>
<p class="hero-tagline">
Пишите нам по любым вопросам проекта: предложить идею мира, рассказать о баге форума, договориться о сотрудничестве.
</p>
</section>
<section class="contacts">
<a class="contact-card" href={`mailto:${CONTACT_EMAIL}`}>
<span class="contact-label">Электронная почта</span>
<span class="contact-value">{CONTACT_EMAIL}</span>
<span class="contact-hint">Основной канал — отвечаем в течение нескольких дней</span>
</a>
<a class="contact-card" href={SOCIAL.telegram.url} target="_blank" rel="noopener">
<span class="contact-label">Telegram-канал</span>
<span class="contact-value">@anotherreflections</span>
<span class="contact-hint">Анонсы, новости, объявления администрации</span>
</a>
<a class="contact-card" href={SOCIAL.vk.url} target="_blank" rel="noopener">
<span class="contact-label">ВКонтакте</span>
<span class="contact-value">vk.com/anotherreflections</span>
<span class="contact-hint">Сообщество — здесь же открытое обсуждение</span>
</a>
</section>
<section style="margin-top: 3rem;">
<h2 style="font-family: var(--font-serif); font-size: 1.6rem; margin-bottom: 1rem;">Игровые форумы</h2>
<p style="color: var(--fg-muted);">Хотите включиться в игру? Заглядывайте на соответствующий форум — там модераторы каждого мира.</p>
<ul class="worlds-grid">
{WORLDS.map((w) => (
<li>
<a class="world-card" href={w.url} target="_blank" rel="noopener" style={`--cat-color: ${w.color}`}>
<span class="world-tag">{w.tag}</span>
<span class="world-name">{w.name}</span>
<span class="world-desc">{w.desc}</span>
</a>
</li>
))}
</ul>
</section>
</BaseLayout>

29
src/pages/sitemap.txt.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { APIContext } from 'astro';
import { getCollection } from 'astro:content';
import { SITE_URL } from '../consts';
export async function GET(_context: APIContext) {
const posts = await getCollection('posts');
const pages = await getCollection('pages');
const staticPaths = [
'/',
'/miry/',
'/kontakty/',
];
// Категории — собираем из постов
const categorySlugs = new Set<string>();
posts.forEach((p) => p.data.categorySlugs.forEach((s) => categorySlugs.add(s)));
const urls = [
...staticPaths.map((p) => new URL(p, SITE_URL).toString()),
...pages.map((pg) => new URL(`/${pg.data.slug}/`, SITE_URL).toString()),
...posts.map((p) => new URL(`/${p.data.slug}/`, SITE_URL).toString()),
...Array.from(categorySlugs).map((s) => new URL(`/category/${s}/`, SITE_URL).toString()),
];
return new Response(urls.join('\n') + '\n', {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}

View File

@@ -519,6 +519,50 @@ pre {
background: var(--bg-elev-2);
}
/* ============================== CONTACTS ============================== */
.contacts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1rem 0 0;
}
.contact-card {
display: flex;
flex-direction: column;
gap: .4rem;
padding: 1.5rem 1.6rem;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--fg);
border-bottom: 1px solid var(--border);
transition: border-color .25s, transform .25s, background .25s;
}
.contact-card:hover {
border-color: var(--accent);
background: var(--bg-elev-2);
transform: translateY(-2px);
border-bottom-color: var(--accent);
}
.contact-card .contact-label {
font-size: .72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-dim);
}
.contact-card .contact-value {
font-family: var(--font-serif);
font-size: 1.3rem;
font-weight: 600;
color: #ebe3ff;
}
.contact-card:hover .contact-value { color: var(--accent); }
.contact-card .contact-hint {
font-size: .88rem;
color: var(--fg-muted);
line-height: 1.5;
}
/* ============================== SOCIAL RAIL ============================== */
.social-rail {
position: fixed;