fix(seo): Wave 1 — fix critical SEO gaps

- Fix duplicate faq/ruFaq keys in dice.json, list.json, colors.json
- Add <main> landmarks to about/privacy pages (EN + RU)
- Move YandexRTB after <main> on homepage
- Add x-default hreflang to BaseLayout
- Configure sitemap i18n with hreflang alternates
- Add Open Graph + Twitter Card meta tags
- Create default OG image (1200x630)
This commit is contained in:
emil
2026-05-14 02:39:56 +03:00
parent 75038780d5
commit 43ea9bc8f3
23 changed files with 2087 additions and 146 deletions
+8 -2
View File
@@ -26,12 +26,18 @@
"faq": [
{ "q": "What color formats are supported?", "a": "HEX, RGB, and HSL. All three are shown at once, so you can copy the one you need." },
{ "q": "Can I use the generated color in CSS?", "a": "Yes. Copy the HEX or RGB value and paste it directly into your stylesheet or design tool." },
{ "q": "Are the colors truly random?", "a": "Yes. Each color is generated using a cryptographically secure random number generator, covering the entire color space." }
{ "q": "Are the colors truly random?", "a": "Yes. Each color is generated using a cryptographically secure random number generator, covering the entire color space." },
{ "q": "What color formats are supported?", "a": "HEX, RGB, and HSL. All three are shown simultaneously for every generated color." },
{ "q": "Can I copy just one format?", "a": "Yes. Click the copy icon next to any format to copy only that value." },
{ "q": "Are the colors truly random?", "a": "Yes. Each color is generated using random values for hue, saturation, and lightness." }
],
"ruFaq": [
{ "q": "Какие форматы цветов поддерживаются?", "a": "HEX, RGB и HSL. Все три формата отображаются одновременно — скопируйте тот, который нужен." },
{ "q": "Можно ли использовать сгенерированный цвет в CSS?", "a": "Да. Скопируйте значение HEX или RGB и вставьте его прямо в стили или графический редактор." },
{ "q": "Цвета действительно случайные?", "a": "Да. Каждый цвет генерируется с помощью криптографически стойкого генератора случайных чисел, покрывая всё цветовое пространство." }
{ "q": "Цвета действительно случайные?", "a": "Да. Каждый цвет генерируется с помощью криптографически стойкого генератора случайных чисел, покрывая всё цветовое пространство." },
{ "q": "Какие форматы цветов поддерживаются?", "a": "HEX, RGB и HSL. Все три отображаются одновременно для каждого сгенерированного цвета." },
{ "q": "Можно скопировать только один формат?", "a": "Да. Нажмите иконку копирования рядом с нужным форматом." },
{ "q": "Цвета действительно случайные?", "a": "Да. Каждый цвет генерируется из случайных значений оттенка, насыщенности и освещённости." }
],
"ruHowTo": [
"Нажмите «Сгенерировать», чтобы получить случайный цвет.",
+8 -2
View File
@@ -29,12 +29,18 @@
"faq": [
{ "q": "What dice notation can I use?", "a": "You can use standard XdY+Z notation, plus modifiers like advantage, disadvantage, keep/drop (kh/kl/dl/dh), and exploding dice (!)." },
{ "q": "Is this fair for D&D and other RPGs?", "a": "Yes. Randify uses the browser's cryptographically secure random number generator, so every roll is unbiased." },
{ "q": "Can I roll multiple dice at once?", "a": "Absolutely. Enter something like 5d6 or click the die buttons and set the count to roll up to 20 dice in a single throw." }
{ "q": "Can I roll multiple dice at once?", "a": "Absolutely. Enter something like 5d6 or click the die buttons and set the count to roll up to 20 dice in a single throw." },
{ "q": "What dice notation is supported?", "a": "Standard XdY+Z, advantage/disadvantage, keep/drop (kh/kl/dh/dl), reroll (r), and exploding dice (!)." },
{ "q": "Can I roll more than one type of die at once?", "a": "Yes. Enter multiple notations separated by spaces, like 2d6 1d20." },
{ "q": "Is the roll history saved?", "a": "Yes. Your last rolls are shown below the results until you refresh the page." }
],
"ruFaq": [
{ "q": "Какую нотацию кубиков поддерживает генератор?", "a": "Стандартную нотацию XdY+Z, а также модификаторы: преимущество, помеха, keep/drop (kh/kl/dl/dh) и взрывающиеся кубики (!)." },
{ "q": "Подходит ли генератор для D&D и других НРИ?", "a": "Да. Randify использует криптографически стойкий генератор случайных чисел браузера, поэтому каждый бросок честный и непредвзятый." },
{ "q": "Можно ли бросать несколько кубиков одновременно?", "a": "Конечно. Введите что-то вроде 5d6 или настройте кубики вручную — можно бросить до 20 кубиков за раз." }
{ "q": "Можно ли бросать несколько кубиков одновременно?", "a": "Конечно. Введите что-то вроде 5d6 или настройте кубики вручную — можно бросить до 20 кубиков за раз." },
{ "q": "Какая нотация кубиков поддерживается?", "a": "Стандартная XdY+Z, преимущество/помеха, keep/drop (kh/kl/dh/dl), переброс (r) и взрывающиеся кубики (!)." },
{ "q": "Можно ли бросить несколько типов кубиков сразу?", "a": "Да. Введите несколько нотаций через пробел, например 2d6 1d20." },
{ "q": "История бросков сохраняется?", "a": "Да. Последние броски отображаются под результатами до обновления страницы." }
],
"ruHowTo": [
"Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.",
+8 -2
View File
@@ -27,12 +27,18 @@
"faq": [
{ "q": "Can the same item be picked more than once?", "a": "Only if you enable the 'Allow duplicates' toggle. By default, each item can be chosen only once per draw." },
{ "q": "Is there a limit to how many items I can add?", "a": "You can add up to 1,000 items. For most use cases — giveaways, raffles, or team assignments — this is more than enough." },
{ "q": "Can I pick multiple winners at once?", "a": "Yes. Set the 'Pick' field to any number up to the total count of items, and Randify will select that many winners in one go." }
{ "q": "Can I pick multiple winners at once?", "a": "Yes. Set the 'Pick' field to any number up to the total count of items, and Randify will select that many winners in one go." },
{ "q": "Can the same item be picked more than once?", "a": "Only if you toggle Allow duplicates. By default, each item is picked at most once." },
{ "q": "Is there a limit to how many items I can add?", "a": "You can add up to 500 items, and pick up to the total number of unique items." },
{ "q": "Does the order of items matter?", "a": "No. The selection is completely random regardless of the order you enter items." }
],
"ruFaq": [
{ "q": "Может ли один и тот же элемент выпасть несколько раз?", "a": "Только если включён переключатель «Разрешить повторения». По умолчанию каждый элемент выбирается не более одного раза." },
{ "q": "Есть ли ограничение на количество элементов в списке?", "a": "Можно добавить до 1 000 элементов. Для розыгрышей, жеребьёвок и распределения задач этого более чем достаточно." },
{ "q": "Можно ли выбрать несколько победителей сразу?", "a": "Да. Укажите в поле «Выбрать» любое число до общего количества элементов — Randify выберет нужное количество победителей за один раз." }
{ "q": "Можно ли выбрать несколько победителей сразу?", "a": "Да. Укажите в поле «Выбрать» любое число до общего количества элементов — Randify выберет нужное количество победителей за один раз." },
{ "q": "Может ли один элемент выпасть несколько раз?", "a": "Только если включена опция «Разрешить повторения». По умолчанию каждый элемент выбирается не более одного раза." },
{ "q": "Есть ли ограничение на количество элементов?", "a": "Можно добавить до 500 элементов и выбрать до общего числа уникальных элементов." },
{ "q": "Имеет ли значение порядок элементов?", "a": "Нет. Выбор полностью случайный независимо от порядка ввода." }
],
"ruHowTo": [
"Введите список элементов — по одному на строку.",
+16 -1
View File
@@ -9,12 +9,15 @@ import type { Lang } from "../i18n/translations";
interface Props {
title?: string;
description?: string;
ogImage?: string;
}
const lang = (Astro.currentLocale as Lang) || "en";
const T = useT(lang);
const { title = T.defaultTitle, description = T.defaultDesc } = Astro.props;
const { title = T.defaultTitle, description = T.defaultDesc, ogImage = "/og-default.jpg" } = Astro.props;
const ogImageUrl = ogImage.startsWith("http") ? ogImage : `https://randify.pro${ogImage}`;
const pathWithoutLocale = Astro.url.pathname.replace(/^\/ru/, "") || "/";
const enUrl = `https://randify.pro${getRelativeLocaleUrl("en", pathWithoutLocale)}`;
@@ -48,6 +51,7 @@ const webPageSchema = {
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="en" href={enUrl} />
<link rel="alternate" hreflang="ru" href={ruUrl} />
<link rel="alternate" hreflang="x-default" href={enUrl} />
<meta name="verification" content="er9ndnv9ih7agmh8" />
<script
type="application/ld+json"
@@ -61,6 +65,17 @@ const webPageSchema = {
<script src="https://yandex.ru/ads/system/context.js" async></script>
<slot name="head" />
<title>{title}</title>
<meta property="og:site_name" content="Randify" />
<meta property="og:locale" content={lang === "ru" ? "ru_RU" : "en_US"} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={ogImageUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageUrl} />
<Analytics />
<script>
if ("serviceWorker" in navigator) {
+2
View File
@@ -59,6 +59,7 @@ const aboutSchema = {
<div class="w-16 h-0.5 bg-accent/60 rounded-full mt-4" aria-hidden="true"></div>
</header>
<main>
<div class="border-t border-zinc-800/60 pt-8 mt-8 space-y-8">
<section>
<h2 class="text-lg font-semibold text-zinc-100 mb-2">Mission</h2>
@@ -91,5 +92,6 @@ const aboutSchema = {
</a>
</section>
</div>
</main>
</div>
</BaseLayout>
+2 -2
View File
@@ -47,8 +47,6 @@ const webSiteSchema = {
</p>
</header>
<YandexRTB />
<main class="mt-8">
<div
class="grid gap-3"
@@ -65,5 +63,7 @@ const webSiteSchema = {
}
</div>
</main>
<YandexRTB />
</div>
</BaseLayout>
+2
View File
@@ -49,6 +49,7 @@ const T = useT(lang);
<div class="w-16 h-0.5 bg-accent/60 rounded-full mt-4" aria-hidden="true"></div>
</header>
<main>
<div class="border-t border-zinc-800/60 pt-8 mt-8 space-y-8">
<section>
<p class="text-zinc-400 leading-relaxed">{T.privacyIntro}</p>
@@ -81,5 +82,6 @@ const T = useT(lang);
<p class="text-zinc-400 leading-relaxed">{T.privacyChanges}</p>
</section>
</div>
</main>
</div>
</BaseLayout>
+2
View File
@@ -59,6 +59,7 @@ const aboutSchema = {
<div class="w-16 h-0.5 bg-accent/60 rounded-full mt-4" aria-hidden="true"></div>
</header>
<main>
<div class="border-t border-zinc-800/60 pt-8 mt-8 space-y-8">
<section>
<h2 class="text-lg font-semibold text-zinc-100 mb-2">Миссия</h2>
@@ -91,5 +92,6 @@ const aboutSchema = {
</a>
</section>
</div>
</main>
</div>
</BaseLayout>
+2
View File
@@ -49,6 +49,7 @@ const T = useT(lang);
<div class="w-16 h-0.5 bg-accent/60 rounded-full mt-4" aria-hidden="true"></div>
</header>
<main>
<div class="border-t border-zinc-800/60 pt-8 mt-8 space-y-8">
<section>
<p class="text-zinc-400 leading-relaxed">{T.privacyIntro}</p>
@@ -81,5 +82,6 @@ const T = useT(lang);
<p class="text-zinc-400 leading-relaxed">{T.privacyChanges}</p>
</section>
</div>
</main>
</div>
</BaseLayout>