refactor: i18n routing, shared GeneratorLayout, analytics component, content collections

- Add Astro native i18n routing (prefixDefaultLocale: false)
- Introduce GeneratorLayout to deduplicate all generator pages
- Enrich generator data with pageTitle, ruPageTitle, howTo/whenTo arrays
- Make EN and RU generator pages identical using @/ path aliases
- Extract analytics (Yandex Metrika + Top.Mail.Ru) into Analytics.astro
- Move tracker IDs to src/data/config.ts, inject via define:vars
- Migrate generator metadata to Content Collections (src/content/generators/*.json)
- Add Zod schema with .strict() validation in src/lib/generator-schema.ts
- Update CLAUDE.md with new conventions
This commit is contained in:
emil
2026-05-10 20:42:05 +03:00
parent 195c2deb54
commit f1ba82735b
46 changed files with 960 additions and 1369 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"_variables": {
"lastUpdateCheck": 1778278468140
}
}
-1
View File
@@ -1 +0,0 @@
/// <reference types="astro/client" />
+1
View File
@@ -2,3 +2,4 @@ node_modules/
dist/
.env
*.log
.astro/
+22 -5
View File
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Randify is a bilingual (EN/RU) static site (Astro 4 + Tailwind CSS 4) hosting random-value generators. All 10 generators are live. Deployed to randify.pro on reg.ru via GitHub Actions + rsync on push to `main`.
**i18n is handled via Astro's built-in i18n routing.** English pages live at `src/pages/`, Russian pages at `src/pages/ru/`. Both share identical templates because `BaseLayout`, `GeneratorLayout`, and components derive the active locale from `Astro.currentLocale`.
## Commands
```bash
@@ -27,16 +29,29 @@ No test runner or linter is configured.
Every new generator must be created in **both English and Russian simultaneously**.
1. **`src/data/generators.ts`** — add an entry with all fields: `slug`, `title`, `description`, `icon`, `status: 'live'`, `seoTitle`, `seoDescription`, `ruTitle`, `ruDescription`, `ruSeoTitle`, `ruSeoDescription`.
1. **`src/content/generators/<slug>.json`** — create a JSON file with all fields: `slug`, `title`, `description`, `icon`, `status`, `seoTitle`, `seoDescription`, `ruTitle`, `ruDescription`, `ruSeoTitle`, `ruSeoDescription`, `pageTitle`, `ruPageTitle`, `howTo`, `whenTo`, `ruHowTo`, `ruWhenTo`. The schema is defined in `src/lib/generator-schema.ts` and validated automatically at build time via Zod (`.strict()` — unknown fields will fail the build).
2. **`src/components/generators/<Name>Generator.astro`** — the interactive component. Language detection pattern:
- Frontmatter: `const isRu = Astro.url.pathname.startsWith('/ru'); const T = useT(isRu ? 'ru' : 'en');`
- Client script: `const isRu = document.documentElement.lang === 'ru';`
- Use `T.*` keys for all user-visible strings in the template; use `isRu` ternaries in the `<script>` block.
3. **`src/pages/generators/<slug>.astro`** — English page with `<BaseLayout lang="en">`.
3. **`src/pages/generators/<slug>.astro`** — create the page using `GeneratorLayout`:
```astro
---
import GeneratorLayout from '@/layouts/GeneratorLayout.astro';
import <Name>Generator from '@/components/generators/<Name>Generator.astro';
import { generators } from '@/data/generators';
4. **`src/pages/ru/generators/<slug>.astro`** — Russian page with `<BaseLayout lang="ru">`, `ruSeoTitle`/`ruSeoDescription`, breadcrumb back to `/ru/`, Russian `<SeoBlock>` content.
const generator = generators.find((g) => g.slug === '<slug>')!;
---
<GeneratorLayout generator={generator}>
<<Name>Generator />
</GeneratorLayout>
```
4. **`src/pages/ru/generators/<slug>.astro`** — **copy the English file exactly**. Because both files use `@/` path aliases and `GeneratorLayout` derives the locale from `Astro.currentLocale`, the file content is identical for both languages.
**Critical:** Never use a shared dynamic `[slug].astro` for Russian pages. Astro bundles scripts from all imported components — using a single file that imports all 10 generators causes every Russian page to run all 10 scripts, breaking them. Each page must be its own file importing only its generator.
@@ -46,13 +61,15 @@ Every new generator must be created in **both English and Russian simultaneously
- **`src/components/LanguageSwitcher.astro`** — fixed top-right EN/RU toggle; persists choice in `localStorage('lang-pref')`.
- **`src/layouts/BaseLayout.astro`** — accepts `lang` prop (`'en' | 'ru'`), sets `<html lang>`, injects `hreflang` alternates, and includes auto-redirect script (first visit, Russian browser → `/ru/`).
- English pages: `/generators/<slug>/` — Russian pages: `/ru/generators/<slug>/`
- Path aliases `@/*` resolve to `src/*` and are used in all page files so EN and RU templates can be identical.
- `GeneratorLayout.astro` wraps every generator page: breadcrumb, header, AdBanner, SeoBlock, and slot for the interactive component.
## Key conventions
- Accent color: `#534AB7` (CSS var `--accent` in BaseLayout).
- All SVG icons are inlined strings in the `icons` map inside `GeneratorCard.astro`; add new ones there.
- `GeneratorCard.astro` accepts `lang` prop — pass `lang="ru"` on Russian pages to get `ruTitle`/`ruDescription` and `/ru/` links.
- `GeneratorCard.astro` auto-detects locale from `Astro.currentLocale` when no `lang` prop is passed.
- `SeoBlock.astro` accepts `lang` prop for translated "How to use" / "When to use" headers.
- `AdBanner.astro` randomises between two Yandex referral links; three size variants: `leaderboard`, `rectangle`, `tile`.
- Yandex Metrika counter (ID 109130319) is in `BaseLayout.astro`.
- Yandex Metrika counter (ID `109130319`) and Top.Mail.Ru counter (ID `3765043`) live in `src/components/Analytics.astro`. IDs are configured in `src/data/config.ts` and injected via `define:vars`.
- No client-side router — each generator is a separate static HTML page.
+11 -4
View File
@@ -1,11 +1,18 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: 'https://randify.pro',
site: "https://randify.pro",
integrations: [sitemap()],
vite: {
plugins: [tailwindcss()],
},
i18n: {
defaultLocale: "en",
locales: ["en", "ru"],
routing: {
prefixDefaultLocale: false,
},
},
});
+91
View File
@@ -0,0 +1,91 @@
---
import { analytics } from "@/data/config";
const yandexId = analytics.yandexMetrikaId;
const mailRuId = analytics.topMailRuId;
---
<!-- Yandex.Metrika counter -->
<script type="text/javascript" define:vars={{ yandexId }}>
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
((k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a));
})(
window,
document,
"script",
"https://mc.yandex.ru/metrika/tag.js?id=" + String(yandexId),
"ym",
);
ym(Number(yandexId), "init", {
ssr: true,
webvisor: true,
clickmap: true,
ecommerce: "dataLayer",
referrer: document.referrer,
url: location.href,
accurateTrackBounce: true,
trackLinks: true,
});
</script>
<noscript
><div>
<img
src={`https://mc.yandex.ru/watch/${yandexId}`}
style="position:absolute; left:-9999px;"
alt=""
/>
</div></noscript
>
<!-- /Yandex.Metrika counter -->
<!-- Top.Mail.Ru counter -->
<script type="text/javascript" define:vars={{ mailRuId }}>
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({
id: String(mailRuId),
type: "pageView",
start: new Date().getTime(),
});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script");
ts.type = "text/javascript";
ts.async = true;
ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function () {
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(ts, s);
};
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else {
f();
}
})(document, window, "tmr-code");
</script>
<noscript
><div>
<img
src={`https://top-fwz1.mail.ru/counter?id=${mailRuId};js=na`}
style="position:absolute;left:-9999px;"
alt="Top.Mail.Ru"
/>
</div></noscript
>
<!-- /Top.Mail.Ru counter -->
+2 -1
View File
@@ -8,7 +8,8 @@ interface Props {
lang?: Lang;
}
const { generator, lang = 'en' } = Astro.props;
const { generator, lang: langProp } = Astro.props;
const lang = (langProp || (Astro.currentLocale as Lang) || 'en');
const T = useT(lang);
const { slug, icon, status } = generator;
const isLive = status === 'live';
+13 -15
View File
@@ -1,29 +1,27 @@
---
interface Props {
lang: 'en' | 'ru';
alternatePath: string;
}
const { lang, alternatePath } = Astro.props;
const targetLang = lang === 'en' ? 'ru' : 'en';
const currentPath = Astro.url.pathname;
const isRu = currentPath.startsWith('/ru');
const alternatePath = isRu
? (currentPath.replace(/^\/ru/, '') || '/')
: ('/ru' + currentPath);
---
<div class="fixed top-3 right-3 z-50 flex items-center gap-1 bg-zinc-900/80 border border-zinc-800 rounded-lg px-1 py-1 backdrop-blur-sm text-xs font-semibold">
<a
href={lang === 'ru' ? alternatePath : '#'}
href={isRu ? alternatePath : '#'}
id="lang-en"
class={`px-2 py-1 rounded-md transition-colors ${lang === 'en' ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
aria-current={lang === 'en' ? 'true' : undefined}
class={`px-2 py-1 rounded-md transition-colors ${isRu ? 'text-zinc-400 hover:text-zinc-200' : 'bg-[#534AB7] text-white'}`}
aria-current={isRu ? undefined : 'true'}
data-target-lang="en"
data-alternate={alternatePath}
data-alternate={isRu ? alternatePath : ''}
>EN</a>
<a
href={lang === 'en' ? alternatePath : '#'}
href={isRu ? '#' : alternatePath}
id="lang-ru"
class={`px-2 py-1 rounded-md transition-colors ${lang === 'ru' ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
aria-current={lang === 'ru' ? 'true' : undefined}
class={`px-2 py-1 rounded-md transition-colors ${isRu ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
aria-current={isRu ? 'true' : undefined}
data-target-lang="ru"
data-alternate={alternatePath}
data-alternate={isRu ? '' : alternatePath}
>RU</a>
</div>
+11
View File
@@ -0,0 +1,11 @@
import { defineCollection } from "astro:content";
import { generatorSchema } from "@/lib/generator-schema";
const generators = defineCollection({
type: "data",
schema: generatorSchema,
});
export const collections = {
generators,
};
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "cards",
"title": "Card",
"description": "Draw a random playing card from a standard deck.",
"icon": "square-stack",
"status": "live",
"seoTitle": "Random Card Generator — Draw from a Deck | Randify",
"seoDescription": "Draw random playing cards from a standard 52-card deck. Pick any number of cards, with or without replacement. Free online card picker.",
"ruTitle": "Карта",
"ruDescription": "Вытащите случайную карту из стандартной колоды.",
"ruSeoTitle": "Генератор случайных карт | Randify",
"ruSeoDescription": "Тяните случайные карты из колоды 52 карт. С повторениями или без. Бесплатный онлайн-генератор карт.",
"pageTitle": "Card Generator",
"ruPageTitle": "Генератор карт",
"howTo": [
"Set how many cards to draw (1–52).",
"Toggle Allow duplicates if you want cards to repeat.",
"Click Draw — cards are shown as mini card tiles.",
"Click Copy to copy the result (e.g. A♠, K♥) to your clipboard."
],
"whenTo": [
"Drawing a random hand for card games.",
"Practising card probability and statistics.",
"Simulating a random card pull for magic tricks or puzzles.",
"Running fair draws without a physical deck."
],
"ruHowTo": [
"Укажите количество карт для розыгрыша.",
"Включите «Разрешить повторения», если нужны дубли.",
"Нажмите «Тянуть» — карты появятся на экране.",
"Нажмите «Копировать», чтобы скопировать результат."
],
"ruWhenTo": [
"Случайная раздача карт для карточных игр.",
"Выбор случайных карт для гаданий.",
"Генерация случайных комбинаций карт для тестирования."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "coin",
"title": "Coin Flip",
"description": "Flip one or more coins and see heads or tails.",
"icon": "circle-dollar-sign",
"status": "live",
"seoTitle": "Coin Flip Online — Heads or Tails | Randify",
"seoDescription": "Flip a virtual coin online. Flip multiple coins at once and see how many heads and tails you get. Free random coin toss simulator.",
"ruTitle": "Монетка",
"ruDescription": "Подбросьте одну или несколько монет — орёл или решка.",
"ruSeoTitle": "Подбросить монетку онлайн — орёл или решка | Randify",
"ruSeoDescription": "Подбрасывайте виртуальную монету онлайн. Одну или несколько сразу. Бесплатный симулятор подбрасывания монеты.",
"pageTitle": "Coin Flip",
"ruPageTitle": "Подбросить монетку",
"howTo": [
"Drag the slider to choose how many coins to flip (1–20).",
"Click Flip — each coin shows H for heads or T for tails.",
"When flipping more than one coin, the heads/tails count is shown below.",
"Click Copy to copy all results to the clipboard."
],
"whenTo": [
"Making a quick heads-or-tails decision.",
"Simulating a coin toss for a sports match or game.",
"Demonstrating probability and the law of large numbers.",
"Settling a fair 50/50 choice between two options."
],
"ruHowTo": [
"Выберите количество монет с помощью ползунка.",
"Нажмите «Подбросить» — орёл или решка.",
"При нескольких монетах отображается итоговый счёт.",
"Нажмите «Копировать», чтобы сохранить результат."
],
"ruWhenTo": [
"Принятие решений по принципу «орёл или решка».",
"Определение очерёдности в игре.",
"Симуляция случайных событий с равной вероятностью."
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"slug": "colors",
"title": "Color",
"description": "Generate a random color in HEX, RGB, or HSL format.",
"icon": "palette",
"status": "live",
"seoTitle": "Random Color Generator — HEX, RGB, HSL | Randify",
"seoDescription": "Get a random color in HEX, RGB or HSL format with one click. Free online color randomizer for designers and developers.",
"ruTitle": "Цвет",
"ruDescription": "Генерируйте случайный цвет в форматах HEX, RGB или HSL.",
"ruSeoTitle": "Генератор случайных цветов — HEX, RGB, HSL | Randify",
"ruSeoDescription": "Получайте случайные цвета в форматах HEX, RGB и HSL одним нажатием. Бесплатный рандомайзер цветов для дизайнеров.",
"pageTitle": "Color Generator",
"ruPageTitle": "Генератор цветов",
"howTo": [
"Click Generate to produce a random color.",
"Switch between HEX, RGB, and HSL tabs to see the value in your preferred format.",
"Click the color value or the copy icon to copy it to the clipboard."
],
"whenTo": [
"Finding inspiration for a new design palette.",
"Picking a placeholder color while prototyping.",
"Selecting a random theme color for a project or presentation.",
"Teaching or exploring color theory and formats."
],
"ruHowTo": [
"Нажмите «Сгенерировать», чтобы получить случайный цвет.",
"Цвет отображается в форматах HEX, RGB и HSL одновременно.",
"Нажмите на иконку копирования рядом с нужным форматом."
],
"ruWhenTo": [
"Поиск вдохновения для цветовой палитры.",
"Случайный выбор цвета для дизайна или иллюстрации.",
"Генерация тестовых данных с цветовыми значениями."
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"slug": "dice",
"title": "Dice",
"description": "Roll one or more dice with any number of sides.",
"icon": "dice-6",
"status": "live",
"seoTitle": "Dice Roller — d4 d6 d20 + Full Notation | D&D Dice | Randify",
"seoDescription": "Free online dice roller with full XdY+Z notation, keep/drop (4d6dl1), exploding dice (3d6!), reroll, advantage/disadvantage, and roll history. Perfect for D&D, Pathfinder, and Savage Worlds.",
"ruTitle": "Кубик",
"ruDescription": "Бросьте один или несколько кубиков с любым числом граней.",
"ruSeoTitle": "Бросить кубик онлайн — d4 d6 d8 d10 d12 d20 | D&D | Randify",
"ruSeoDescription": "Бесплатный онлайн-бросок кубиков с полной нотацией XdY+Z, keep/drop (4d6dl1), взрывающимися кубиками (3d6!), перебросом, преимуществом/помехой и историей. Для D&D, Pathfinder и Savage Worlds.",
"pageTitle": "Dice Generator",
"ruPageTitle": "Бросить кубик онлайн",
"howTo": [
"Type a notation like 2d6+3 or 1d20 — or use the die buttons and count.",
"Pick a mode: Normal, Advantage, or Disadvantage (single die only).",
"Click Roll — each die result appears with the total sum.",
"Maximum rolls are highlighted in purple; ones are highlighted in red."
],
"whenTo": [
"Rolling dice for D&D, Pathfinder, or any tabletop RPG.",
"Using dice notation shortcuts like 2d6+3 or 1d20 for fast rolling.",
"Rolling with advantage or disadvantage for D&D 5e mechanics.",
"Generating character stats with 4d6 drop-lowest method.",
"Rolling exploding dice for Savage Worlds or house rules (e.g. 3d6!).",
"Using keep/drop modifiers like 4d6kh3 to keep highest rolls."
],
"ruHowTo": [
"Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.",
"Выберите режим: Обычный, Преимущество или Помеха (только для одного кубика).",
"Нажмите «Бросить» — результаты отобразятся мгновенно.",
"Максимумы подсвечены фиолетовым, единицы — красным."
],
"ruWhenTo": [
"Броски в настольных ролевых играх (D&D и другие).",
"Быстрые броски через нотацию: 2d6+3, 1d20 и любые другие.",
"Преимущество и помеха для механик D&D 5e.",
"Бросок характеристик: 4d6, отбросить наименьший (4d6dl1).",
"Взрывающиеся кубики для Savage Worlds или хаус-рулов (например, 3d6!).",
"Использование keep/drop модификаторов типа 4d6kh3 (оставить три лучших)."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "list",
"title": "List Picker",
"description": "Paste a list of items and pick random winners.",
"icon": "list",
"status": "live",
"seoTitle": "Random List Picker — Pick a Random Winner | Randify",
"seoDescription": "Paste any list of names or items and pick random winners instantly. Great for giveaways, choosing who goes first, or any random selection.",
"ruTitle": "Список",
"ruDescription": "Вставьте список элементов и выберите случайных победителей.",
"ruSeoTitle": "Случайный выбор из списка | Randify",
"ruSeoDescription": "Вставьте список имён или вариантов и выберите случайных победителей мгновенно. Идеально для розыгрышей и жеребьёвок.",
"pageTitle": "List Picker",
"ruPageTitle": "Случайный выбор из списка",
"howTo": [
"Type or paste your items into the text area — one item per line.",
"Set how many items to pick in the Pick field.",
"Toggle Allow duplicates if the same item can be chosen more than once.",
"Click Pick (or press Ctrl+Enter) — the selected items appear as chips."
],
"whenTo": [
"Choosing a random winner for a giveaway or contest.",
"Picking who presents first in a meeting or class.",
"Organising a Secret Santa or gift exchange draw.",
"Selecting a random movie, restaurant, or activity from a list."
],
"ruHowTo": [
"Введите список элементов — по одному на строку.",
"Укажите, сколько элементов выбрать.",
"При необходимости разрешите повторения.",
"Нажмите «Выбрать» — результаты появятся на экране."
],
"ruWhenTo": [
"Розыгрыш победителей конкурса.",
"Случайное распределение задач между участниками команды.",
"Выбор случайного варианта из списка."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "lottery",
"title": "Lottery",
"description": "Draw a set of unique numbers for lottery-style picks.",
"icon": "ticket",
"status": "live",
"seoTitle": "Lottery Number Generator — Random Pick | Randify",
"seoDescription": "Draw a set of unique random lottery numbers from any range. Perfect for lotteries, raffles, and lucky draws. Free online lottery picker.",
"ruTitle": "Лотерея",
"ruDescription": "Вытащите набор уникальных чисел в стиле лотереи.",
"ruSeoTitle": "Генератор лотерейных номеров | Randify",
"ruSeoDescription": "Тяните уникальные случайные числа для лотереи, розыгрыша или жеребьёвки. Бесплатный онлайн-генератор лотерейных номеров.",
"pageTitle": "Lottery Generator",
"ruPageTitle": "Генератор лотереи",
"howTo": [
"Set the From and To fields to define your number range (e.g. 1 to 49).",
"Enter how many numbers to pick in the Pick field.",
"Click Draw — a sorted set of unique numbers appears.",
"Click Copy to copy all numbers to the clipboard."
],
"whenTo": [
"Generating lottery or lotto ticket numbers.",
"Running a fair raffle draw with unique entries.",
"Selecting a random sample from a numbered list.",
"Choosing lottery numbers for Powerball, EuroMillions, or local draws."
],
"ruHowTo": [
"Укажите диапазон чисел в полях «От» и «До».",
"Задайте количество чисел для розыгрыша.",
"Нажмите «Тянуть» — выпавшие числа появятся на экране.",
"Нажмите «Копировать», чтобы сохранить результат."
],
"ruWhenTo": [
"Розыгрыш лотерейных номеров.",
"Жеребьёвка участников конкурса.",
"Случайный выбор нескольких уникальных чисел."
]
}
+39
View File
@@ -0,0 +1,39 @@
{
"slug": "numbers",
"title": "Number",
"description": "Pick a random number within any range you choose.",
"icon": "hash",
"status": "live",
"seoTitle": "Random Number Generator | Randify",
"seoDescription": "Generate a random number between any two values instantly. Free online random number picker — perfect for giveaways, games, and decisions.",
"ruTitle": "Число",
"ruDescription": "Выберите случайное число в любом диапазоне.",
"ruSeoTitle": "Генератор случайных чисел | Randify",
"ruSeoDescription": "Генерируйте случайное число в любом диапазоне мгновенно. Идеально для розыгрышей, игр и принятия решений.",
"pageTitle": "Number Generator",
"ruPageTitle": "Генератор чисел",
"howTo": [
"Enter the minimum value in the From field.",
"Enter the maximum value in the To field.",
"Click Generate — a random number within your range appears instantly.",
"Click the number to copy it to the clipboard."
],
"whenTo": [
"Picking a winner in a giveaway or raffle.",
"Choosing a random starting player in a board game.",
"Making a quick decision between numbered options.",
"Generating test data with values in a specific range."
],
"ruHowTo": [
"Введите минимальное значение в поле «От».",
"Введите максимальное значение в поле «До».",
"Нажмите «Сгенерировать» — случайное число появится мгновенно.",
"Нажмите на число, чтобы скопировать его в буфер обмена."
],
"ruWhenTo": [
"Выбор победителя в розыгрыше или конкурсе.",
"Определение случайного первого игрока в настольной игре.",
"Быстрое принятие решения между пронумерованными вариантами.",
"Генерация тестовых данных с числами в заданном диапазоне."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "password",
"title": "Password",
"description": "Create a strong, random password with custom rules.",
"icon": "lock",
"status": "live",
"seoTitle": "Random Password Generator | Randify",
"seoDescription": "Create a strong random password with custom length, uppercase, lowercase, digits and symbols. Free and secure — runs entirely in your browser.",
"ruTitle": "Пароль",
"ruDescription": "Создайте надёжный случайный пароль с настраиваемыми правилами.",
"ruSeoTitle": "Генератор случайных паролей | Randify",
"ruSeoDescription": "Создавайте надёжные пароли с заглавными буквами, цифрами и символами. Бесплатно и безопасно — работает прямо в браузере.",
"pageTitle": "Password Generator",
"ruPageTitle": "Генератор паролей",
"howTo": [
"Drag the length slider to set how many characters you need (4–64).",
"Toggle uppercase, lowercase, digits, and symbols on or off.",
"Click Generate — a new password appears instantly.",
"Click the password to copy it to your clipboard."
],
"whenTo": [
"Creating a strong password for a new account.",
"Generating a temporary access credential.",
"Producing a random secret key for development or testing.",
"Replacing a weak or reused password."
],
"ruHowTo": [
"Задайте длину пароля с помощью ползунка.",
"Выберите типы символов: заглавные, строчные, цифры, спецсимволы.",
"Нажмите «Сгенерировать» — готовый пароль появится на экране.",
"Нажмите на пароль, чтобы скопировать его."
],
"ruWhenTo": [
"Создание надёжного пароля для нового аккаунта.",
"Генерация секретного ключа или токена.",
"Регулярное обновление паролей для повышения безопасности."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "uuid",
"title": "UUID / Token",
"description": "Generate UUIDs, hex tokens, or random base64 strings.",
"icon": "fingerprint",
"status": "live",
"seoTitle": "UUID Generator — UUID v4, Hex & Base64 Tokens | Randify",
"seoDescription": "Generate random UUID v4, hex tokens, or base64 strings online. Cryptographically secure, runs in your browser. Free UUID and token generator.",
"ruTitle": "UUID / Токен",
"ruDescription": "Генерируйте UUID, hex-токены или случайные Base64-строки.",
"ruSeoTitle": "Генератор UUID и токенов | Randify",
"ruSeoDescription": "Генерируйте UUID v4, hex-токены и Base64-строки онлайн. Криптографически безопасно, работает в браузере.",
"pageTitle": "UUID / Token",
"ruPageTitle": "Генератор UUID и токенов",
"howTo": [
"Choose a token type: UUID v4, Hex, or Base64.",
"For Hex and Base64, set the byte length (4–64 bytes).",
"Enter how many tokens to generate (1–20).",
"Click Generate, then Copy all to copy every token to the clipboard."
],
"whenTo": [
"Generating unique IDs for database records or API resources.",
"Creating random session tokens or API keys during development.",
"Producing test data with realistic-looking identifiers.",
"Generating a one-time secret for environment variables or config files."
],
"ruHowTo": [
"Выберите тип: UUID v4, Hex или Base64.",
"Для Hex и Base64 задайте длину в байтах.",
"Укажите количество токенов.",
"Нажмите «Сгенерировать» и скопируйте результат."
],
"ruWhenTo": [
"Генерация уникальных идентификаторов для баз данных.",
"Создание токенов для API и сессий.",
"Генерация случайных ключей шифрования."
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"slug": "wheel",
"title": "Spin the Wheel",
"description": "Add your items, spin the wheel, and let chance decide.",
"icon": "pie-chart",
"status": "live",
"seoTitle": "Spin the Wheel — Random Picker | Randify",
"seoDescription": "Spin a customizable wheel of fortune to pick a random winner, choice, or option. Add your own items and let the wheel decide.",
"ruTitle": "Колесо фортуны",
"ruDescription": "Добавьте элементы, крутите колесо и пусть случай решит.",
"ruSeoTitle": "Колесо фортуны онлайн — случайный выбор | Randify",
"ruSeoDescription": "Крутите колесо фортуны онлайн. Добавьте свои варианты и пусть случай решит победителя.",
"pageTitle": "Spin the Wheel",
"ruPageTitle": "Колесо фортуны онлайн",
"howTo": [
"Type your items into the text box — one per line.",
"Click \"Update wheel\" to apply changes.",
"Hit \"Spin\" and watch the wheel decide.",
"Copy the result or spin again."
],
"whenTo": [
"Picking a winner for a giveaway or raffle.",
"Deciding whose turn it is in a game or meeting.",
"Choosing a random activity, movie, or restaurant.",
"Any decision you want to leave to chance."
],
"ruHowTo": [
"Введите варианты в поле — по одному на строку (от 2 до 24).",
"Колесо обновляется автоматически.",
"Нажмите «Крутить» и дождитесь результата.",
"Победитель выделяется — скопируйте его кнопкой «Копировать»."
],
"ruWhenTo": [
"Случайный выбор победителя розыгрыша.",
"Принятие решений в команде.",
"Распределение ролей или задач между участниками."
]
}
+6
View File
@@ -0,0 +1,6 @@
export const siteUrl = 'https://randify.pro';
export const analytics = {
yandexMetrikaId: '109130319',
topMailRuId: '3765043',
} as const;
+19 -144
View File
@@ -1,146 +1,21 @@
export interface Generator {
slug: string;
title: string;
description: string;
icon: string;
status: 'live' | 'coming-soon';
seoTitle: string;
seoDescription: string;
ruTitle: string;
ruDescription: string;
ruSeoTitle: string;
ruSeoDescription: string;
import { generatorSchema } from "@/lib/generator-schema";
import type { Generator } from "@/lib/generator-schema";
const modules = import.meta.glob<{ default: unknown }>(
"../content/generators/*.json",
{
eager: true,
},
);
export const generators: Generator[] = Object.values(modules).map((mod) =>
generatorSchema.parse(mod.default),
);
export function findGenerator(slug: string): Generator {
const g = generators.find((gen) => gen.slug === slug);
if (!g) throw new Error(`Generator not found: ${slug}`);
return g;
}
export const generators: Generator[] = [
{
slug: 'numbers',
title: 'Number',
description: 'Pick a random number within any range you choose.',
icon: 'hash',
status: 'live',
seoTitle: 'Random Number Generator | Randify',
seoDescription: 'Generate a random number between any two values instantly. Free online random number picker — perfect for giveaways, games, and decisions.',
ruTitle: 'Число',
ruDescription: 'Выберите случайное число в любом диапазоне.',
ruSeoTitle: 'Генератор случайных чисел | Randify',
ruSeoDescription: 'Генерируйте случайное число в любом диапазоне мгновенно. Идеально для розыгрышей, игр и принятия решений.',
},
{
slug: 'dice',
title: 'Dice',
description: 'Roll one or more dice with any number of sides.',
icon: 'dice-6',
status: 'live',
seoTitle: 'Dice Roller — d4 d6 d20 + Full Notation | D&D Dice | Randify',
seoDescription: 'Free online dice roller with full XdY+Z notation, keep/drop (4d6dl1), exploding dice (3d6!), reroll, advantage/disadvantage, and roll history. Perfect for D&D, Pathfinder, and Savage Worlds.',
ruTitle: 'Кубик',
ruDescription: 'Бросьте один или несколько кубиков с любым числом граней.',
ruSeoTitle: 'Бросить кубик онлайн — d4 d6 d8 d10 d12 d20 | D&D | Randify',
ruSeoDescription: 'Бесплатный онлайн-бросок кубиков с полной нотацией XdY+Z, keep/drop (4d6dl1), взрывающимися кубиками (3d6!), перебросом, преимуществом/помехой и историей. Для D&D, Pathfinder и Savage Worlds.',
},
{
slug: 'wheel',
title: 'Spin the Wheel',
description: 'Add your items, spin the wheel, and let chance decide.',
icon: 'pie-chart',
status: 'live',
seoTitle: 'Spin the Wheel — Random Picker | Randify',
seoDescription: 'Spin a customizable wheel of fortune to pick a random winner, choice, or option. Add your own items and let the wheel decide.',
ruTitle: 'Колесо фортуны',
ruDescription: 'Добавьте элементы, крутите колесо и пусть случай решит.',
ruSeoTitle: 'Колесо фортуны онлайн — случайный выбор | Randify',
ruSeoDescription: 'Крутите колесо фортуны онлайн. Добавьте свои варианты и пусть случай решит победителя.',
},
{
slug: 'colors',
title: 'Color',
description: 'Generate a random color in HEX, RGB, or HSL format.',
icon: 'palette',
status: 'live',
seoTitle: 'Random Color Generator — HEX, RGB, HSL | Randify',
seoDescription: 'Get a random color in HEX, RGB or HSL format with one click. Free online color randomizer for designers and developers.',
ruTitle: 'Цвет',
ruDescription: 'Генерируйте случайный цвет в форматах HEX, RGB или HSL.',
ruSeoTitle: 'Генератор случайных цветов — HEX, RGB, HSL | Randify',
ruSeoDescription: 'Получайте случайные цвета в форматах HEX, RGB и HSL одним нажатием. Бесплатный рандомайзер цветов для дизайнеров.',
},
{
slug: 'password',
title: 'Password',
description: 'Create a strong, random password with custom rules.',
icon: 'lock',
status: 'live',
seoTitle: 'Random Password Generator | Randify',
seoDescription: 'Create a strong random password with custom length, uppercase, lowercase, digits and symbols. Free and secure — runs entirely in your browser.',
ruTitle: 'Пароль',
ruDescription: 'Создайте надёжный случайный пароль с настраиваемыми правилами.',
ruSeoTitle: 'Генератор случайных паролей | Randify',
ruSeoDescription: 'Создавайте надёжные пароли с заглавными буквами, цифрами и символами. Бесплатно и безопасно — работает прямо в браузере.',
},
{
slug: 'lottery',
title: 'Lottery',
description: 'Draw a set of unique numbers for lottery-style picks.',
icon: 'ticket',
status: 'live',
seoTitle: 'Lottery Number Generator — Random Pick | Randify',
seoDescription: 'Draw a set of unique random lottery numbers from any range. Perfect for lotteries, raffles, and lucky draws. Free online lottery picker.',
ruTitle: 'Лотерея',
ruDescription: 'Вытащите набор уникальных чисел в стиле лотереи.',
ruSeoTitle: 'Генератор лотерейных номеров | Randify',
ruSeoDescription: 'Тяните уникальные случайные числа для лотереи, розыгрыша или жеребьёвки. Бесплатный онлайн-генератор лотерейных номеров.',
},
{
slug: 'cards',
title: 'Card',
description: 'Draw a random playing card from a standard deck.',
icon: 'square-stack',
status: 'live',
seoTitle: 'Random Card Generator — Draw from a Deck | Randify',
seoDescription: 'Draw random playing cards from a standard 52-card deck. Pick any number of cards, with or without replacement. Free online card picker.',
ruTitle: 'Карта',
ruDescription: 'Вытащите случайную карту из стандартной колоды.',
ruSeoTitle: 'Генератор случайных карт | Randify',
ruSeoDescription: 'Тяните случайные карты из колоды 52 карт. С повторениями или без. Бесплатный онлайн-генератор карт.',
},
{
slug: 'coin',
title: 'Coin Flip',
description: 'Flip one or more coins and see heads or tails.',
icon: 'circle-dollar-sign',
status: 'live',
seoTitle: 'Coin Flip Online — Heads or Tails | Randify',
seoDescription: 'Flip a virtual coin online. Flip multiple coins at once and see how many heads and tails you get. Free random coin toss simulator.',
ruTitle: 'Монетка',
ruDescription: 'Подбросьте одну или несколько монет — орёл или решка.',
ruSeoTitle: 'Подбросить монетку онлайн — орёл или решка | Randify',
ruSeoDescription: 'Подбрасывайте виртуальную монету онлайн. Одну или несколько сразу. Бесплатный симулятор подбрасывания монеты.',
},
{
slug: 'list',
title: 'List Picker',
description: 'Paste a list of items and pick random winners.',
icon: 'list',
status: 'live',
seoTitle: 'Random List Picker — Pick a Random Winner | Randify',
seoDescription: 'Paste any list of names or items and pick random winners instantly. Great for giveaways, choosing who goes first, or any random selection.',
ruTitle: 'Список',
ruDescription: 'Вставьте список элементов и выберите случайных победителей.',
ruSeoTitle: 'Случайный выбор из списка | Randify',
ruSeoDescription: 'Вставьте список имён или вариантов и выберите случайных победителей мгновенно. Идеально для розыгрышей и жеребьёвок.',
},
{
slug: 'uuid',
title: 'UUID / Token',
description: 'Generate UUIDs, hex tokens, or random base64 strings.',
icon: 'fingerprint',
status: 'live',
seoTitle: 'UUID Generator — UUID v4, Hex & Base64 Tokens | Randify',
seoDescription: 'Generate random UUID v4, hex tokens, or base64 strings online. Cryptographically secure, runs in your browser. Free UUID and token generator.',
ruTitle: 'UUID / Токен',
ruDescription: 'Генерируйте UUID, hex-токены или случайные Base64-строки.',
ruSeoTitle: 'Генератор UUID и токенов | Randify',
ruSeoDescription: 'Генерируйте UUID v4, hex-токены и Base64-строки онлайн. Криптографически безопасно, работает в браузере.',
},
];
export type { Generator };
+67 -81
View File
@@ -1,107 +1,93 @@
---
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
import { useT } from '../i18n/translations';
import type { Lang } from '../i18n/translations';
import { getRelativeLocaleUrl } from "astro:i18n";
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
import Analytics from "../components/Analytics.astro";
import { useT } from "../i18n/translations";
import type { Lang } from "../i18n/translations";
interface Props {
title?: string;
description?: string;
lang?: Lang;
title?: string;
description?: string;
}
const { lang = 'en' } = Astro.props;
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 } = Astro.props;
const currentPath = Astro.url.pathname;
const alternatePath = lang === 'ru'
? (currentPath.replace(/^\/ru/, '') || '/')
: ('/ru' + currentPath);
const pathWithoutLocale = Astro.url.pathname.replace(/^\/ru/, "") || "/";
const enUrl = `https://randify.pro${getRelativeLocaleUrl("en", pathWithoutLocale)}`;
const ruUrl = `https://randify.pro${getRelativeLocaleUrl("ru", pathWithoutLocale)}`;
---
<!doctype html>
<html lang={lang}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet" />
<link rel="alternate" hreflang="en" href={`https://randify.pro${lang === 'ru' ? alternatePath : currentPath}`} />
<link rel="alternate" hreflang="ru" href={`https://randify.pro${lang === 'ru' ? currentPath : alternatePath}`} />
<meta name="verification" content="er9ndnv9ih7agmh8" />
<title>{title}</title>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=109130319', 'ym');
ym(109130319, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/109130319" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
<!-- Top.Mail.Ru counter -->
<script type="text/javascript">
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({id: "3765043", type: "pageView", start: (new Date()).getTime()});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
})(document, window, "tmr-code");
</script>
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3765043;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div></noscript>
<!-- /Top.Mail.Ru counter -->
</head>
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
<LanguageSwitcher lang={lang} alternatePath={alternatePath} />
<slot />
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap"
rel="stylesheet"
/>
<link rel="alternate" hreflang="en" href={enUrl} />
<link rel="alternate" hreflang="ru" href={ruUrl} />
<meta name="verification" content="er9ndnv9ih7agmh8" />
<title>{title}</title>
<Analytics />
</head>
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
<LanguageSwitcher />
<slot />
</body>
</html>
<!-- Auto-detect language on first visit -->
<script>
if (!localStorage.getItem('lang-pref')) {
const browserLang = (navigator.language || '').toLowerCase();
if (browserLang.startsWith('ru') && !location.pathname.startsWith('/ru')) {
location.replace('/ru' + location.pathname);
if (!localStorage.getItem("lang-pref")) {
const browserLang = (navigator.language || "").toLowerCase();
if (
browserLang.startsWith("ru") &&
!location.pathname.startsWith("/ru")
) {
location.replace("/ru" + location.pathname);
}
}
}
</script>
<style is:global>
@import "tailwindcss";
@import "tailwindcss";
@theme {
--font-sans: 'Space Grotesk', sans-serif;
}
@theme {
--font-sans: "Space Grotesk", sans-serif;
}
:root {
--accent: #534AB7;
}
:root {
--accent: #534ab7;
}
* {
box-sizing: border-box;
}
* {
box-sizing: border-box;
}
body {
background-image:
radial-gradient(ellipse 90% 45% at 50% -5%, rgba(83, 74, 183, 0.2) 0%, transparent 65%),
radial-gradient(ellipse 90% 45% at 50% 105%, rgba(83, 74, 183, 0.13) 0%, transparent 65%);
}
body {
background-image:
radial-gradient(
ellipse 90% 45% at 50% -5%,
rgba(83, 74, 183, 0.2) 0%,
transparent 65%
),
radial-gradient(
ellipse 90% 45% at 50% 105%,
rgba(83, 74, 183, 0.13) 0%,
transparent 65%
);
}
input[type="number"] {
color-scheme: dark;
}
input[type="number"] {
color-scheme: dark;
}
</style>
+76
View File
@@ -0,0 +1,76 @@
---
import BaseLayout from './BaseLayout.astro';
import AdBanner from '../components/AdBanner.astro';
import SeoBlock from '../components/SeoBlock.astro';
import { useT } from '../i18n/translations';
import type { Lang } from '../i18n/translations';
import type { Generator } from '../data/generators';
interface Props {
generator: Generator;
}
const { generator } = Astro.props;
const lang = (Astro.currentLocale as Lang) || 'en';
const T = useT(lang);
const isRu = lang === 'ru';
---
<BaseLayout
title={isRu ? generator.ruSeoTitle : generator.seoTitle}
description={isRu ? generator.ruSeoDescription : generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href={isRu ? '/ru/' : '/'}
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label={T.backToAll}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
{T.backToAll}
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{isRu ? generator.ruPageTitle : generator.pageTitle}
</h1>
<p class="mt-2 text-base text-zinc-400">{isRu ? generator.ruDescription : generator.description}</p>
</header>
<main>
<slot />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
lang={lang}
howTo={isRu ? generator.ruHowTo : generator.howTo}
whenTo={isRu ? generator.ruWhenTo : generator.whenTo}
/>
</div>
</BaseLayout>
+25
View File
@@ -0,0 +1,25 @@
import { z } from "zod";
export const generatorSchema = z
.object({
slug: z.string(),
title: z.string(),
description: z.string(),
icon: z.string(),
status: z.enum(["live", "coming-soon"]),
seoTitle: z.string(),
seoDescription: z.string(),
ruTitle: z.string(),
ruDescription: z.string(),
ruSeoTitle: z.string(),
ruSeoDescription: z.string(),
pageTitle: z.string(),
ruPageTitle: z.string(),
howTo: z.array(z.string()),
whenTo: z.array(z.string()),
ruHowTo: z.array(z.string()),
ruWhenTo: z.array(z.string()),
})
.strict();
export type Generator = z.infer<typeof generatorSchema>;
+7 -73
View File
@@ -1,77 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import CardGenerator from '../../components/generators/CardGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import CardGenerator from "@/components/generators/CardGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'cards')!;
const generator = generators.find((g) => g.slug === "cards")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<CardGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Set how many cards to draw (1–52).',
'Toggle Allow duplicates if you want cards to repeat.',
'Click Draw — cards are shown as mini card tiles.',
'Click Copy to copy the result (e.g. A♠, K♥) to your clipboard.',
]}
whenTo={[
'Drawing a random hand for card games.',
'Practising card probability and statistics.',
'Simulating a random card pull for magic tricks or puzzles.',
'Running fair draws without a physical deck.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<CardGenerator />
</GeneratorLayout>
+7 -52
View File
@@ -1,56 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import CoinGenerator from '../../components/generators/CoinGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import CoinGenerator from "@/components/generators/CoinGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'coin')!;
const generator = generators.find((g) => g.slug === "coin")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main><CoinGenerator /></main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Drag the slider to choose how many coins to flip (1–20).',
'Click Flip — each coin shows H for heads or T for tails.',
'When flipping more than one coin, the heads/tails count is shown below.',
'Click Copy to copy all results to the clipboard.',
]}
whenTo={[
'Making a quick heads-or-tails decision.',
'Simulating a coin toss for a sports match or game.',
'Demonstrating probability and the law of large numbers.',
'Settling a fair 50/50 choice between two options.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<CoinGenerator />
</GeneratorLayout>
+7 -72
View File
@@ -1,76 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import ColorGenerator from '../../components/generators/ColorGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import ColorGenerator from "@/components/generators/ColorGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'colors')!;
const generator = generators.find((g) => g.slug === "colors")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<ColorGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Click Generate to produce a random color.',
'Switch between HEX, RGB, and HSL tabs to see the value in your preferred format.',
'Click the color value or the copy icon to copy it to the clipboard.',
]}
whenTo={[
'Finding inspiration for a new design palette.',
'Picking a placeholder color while prototyping.',
'Selecting a random theme color for a project or presentation.',
'Teaching or exploring color theory and formats.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<ColorGenerator />
</GeneratorLayout>
+7 -75
View File
@@ -1,79 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import DiceGenerator from '../../components/generators/DiceGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import DiceGenerator from "@/components/generators/DiceGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'dice')!;
const generator = generators.find((g) => g.slug === "dice")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<DiceGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Type a notation like 2d6+3 or 1d20 — or use the die buttons and count.',
'Pick a mode: Normal, Advantage, or Disadvantage (single die only).',
'Click Roll — each die result appears with the total sum.',
'Maximum rolls are highlighted in purple; ones are highlighted in red.',
]}
whenTo={[
'Rolling dice for D&D, Pathfinder, or any tabletop RPG.',
'Using dice notation shortcuts like 2d6+3 or 1d20 for fast rolling.',
'Rolling with advantage or disadvantage for D&D 5e mechanics.',
'Generating character stats with 4d6 drop-lowest method.',
'Rolling exploding dice for Savage Worlds or house rules (e.g., 3d6!).',
'Using keep/drop modifiers like 4d6kh3 to keep highest rolls.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<DiceGenerator />
</GeneratorLayout>
+7 -52
View File
@@ -1,56 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import ListGenerator from '../../components/generators/ListGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import ListGenerator from "@/components/generators/ListGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'list')!;
const generator = generators.find((g) => g.slug === "list")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main><ListGenerator /></main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Type or paste your items into the text area — one item per line.',
'Set how many items to pick in the Pick field.',
'Toggle Allow duplicates if the same item can be chosen more than once.',
'Click Pick (or press Ctrl+Enter) — the selected items appear as chips.',
]}
whenTo={[
'Choosing a random winner for a giveaway or contest.',
'Picking who presents first in a meeting or class.',
'Organising a Secret Santa or gift exchange draw.',
'Selecting a random movie, restaurant, or activity from a list.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<ListGenerator />
</GeneratorLayout>
+7 -73
View File
@@ -1,77 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import LotteryGenerator from '../../components/generators/LotteryGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import LotteryGenerator from "@/components/generators/LotteryGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'lottery')!;
const generator = generators.find((g) => g.slug === "lottery")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<LotteryGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Set the From and To fields to define your number range (e.g. 1 to 49).',
'Enter how many numbers to pick in the Pick field.',
'Click Draw — a sorted set of unique numbers appears.',
'Click Copy to copy all numbers to the clipboard.',
]}
whenTo={[
'Generating lottery or lotto ticket numbers.',
'Running a fair raffle draw with unique entries.',
'Selecting a random sample from a numbered list.',
'Choosing lottery numbers for Powerball, EuroMillions, or local draws.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<LotteryGenerator />
</GeneratorLayout>
+7 -73
View File
@@ -1,77 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import NumberGenerator from '../../components/generators/NumberGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import NumberGenerator from "@/components/generators/NumberGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'numbers')!;
const generator = generators.find((g) => g.slug === "numbers")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<NumberGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Enter the minimum value in the From field.',
'Enter the maximum value in the To field.',
'Click Generate — a random number within your range appears instantly.',
'Click the number to copy it to the clipboard.',
]}
whenTo={[
'Picking a winner in a giveaway or raffle.',
'Choosing a random starting player in a board game.',
'Making a quick decision between numbered options.',
'Generating test data with values in a specific range.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<NumberGenerator />
</GeneratorLayout>
+7 -73
View File
@@ -1,77 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import PasswordGenerator from '../../components/generators/PasswordGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import PasswordGenerator from "@/components/generators/PasswordGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'password')!;
const generator = generators.find((g) => g.slug === "password")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title} Generator
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<PasswordGenerator />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Drag the length slider to set how many characters you need (4–64).',
'Toggle uppercase, lowercase, digits, and symbols on or off.',
'Click Generate — a new password appears instantly.',
'Click the password to copy it to your clipboard.',
]}
whenTo={[
'Creating a strong password for a new account.',
'Generating a temporary access credential.',
'Producing a random secret key for development or testing.',
'Replacing a weak or reused password.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<PasswordGenerator />
</GeneratorLayout>
+7 -52
View File
@@ -1,56 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import UuidGenerator from '../../components/generators/UuidGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import SeoBlock from '../../components/SeoBlock.astro';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import UuidGenerator from "@/components/generators/UuidGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'uuid')!;
const generator = generators.find((g) => g.slug === "uuid")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main><UuidGenerator /></main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Choose a token type: UUID v4, Hex, or Base64.',
'For Hex and Base64, set the byte length (4–64 bytes).',
'Enter how many tokens to generate (1–20).',
'Click Generate, then Copy all to copy every token to the clipboard.',
]}
whenTo={[
'Generating unique IDs for database records or API resources.',
'Creating random session tokens or API keys during development.',
'Producing test data with realistic-looking identifiers.',
'Generating a one-time secret for environment variables or config files.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<UuidGenerator />
</GeneratorLayout>
+7 -73
View File
@@ -1,77 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import WheelSpinner from '../../components/generators/WheelSpinner.astro';
import AdBanner from '../../components/AdBanner.astro';
import SeoBlock from '../../components/SeoBlock.astro';
import { generators } from '../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import WheelSpinner from "@/components/generators/WheelSpinner.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'wheel')!;
const generator = generators.find((g) => g.slug === "wheel")!;
---
<BaseLayout lang="en"
title={generator.seoTitle}
description={generator.seoDescription}
>
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a
href="/"
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
aria-label="Back to all generators"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6"/>
</svg>
All generators
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
{generator.title}
</h1>
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
</header>
<main>
<WheelSpinner />
</main>
<div class="mt-12 flex justify-center">
<AdBanner size="tile" />
</div>
<SeoBlock
howTo={[
'Type your items into the text box — one per line.',
'Click "Update wheel" to apply changes.',
'Hit "Spin" and watch the wheel decide.',
'Copy the result or spin again.',
]}
whenTo={[
'Picking a winner for a giveaway or raffle.',
'Deciding whose turn it is in a game or meeting.',
'Choosing a random activity, movie, or restaurant.',
'Any decision you want to leave to chance.',
]}
/>
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<WheelSpinner />
</GeneratorLayout>
+46 -39
View File
@@ -1,46 +1,53 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import GeneratorCard from '../components/GeneratorCard.astro';
import AdBanner from '../components/AdBanner.astro';
import { generators } from '../data/generators';
import { useT } from '../i18n/translations';
import BaseLayout from "@/layouts/BaseLayout.astro";
import GeneratorCard from "@/components/GeneratorCard.astro";
import AdBanner from "@/components/AdBanner.astro";
import { generators } from "@/data/generators";
import { useT } from "@/i18n/translations";
import type { Lang } from "@/i18n/translations";
const T = useT('en');
const lang = (Astro.currentLocale as Lang) || "en";
const T = useT(lang);
---
<BaseLayout lang="en">
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
<header class="mb-12">
<div class="flex items-center gap-2 mb-6">
<span
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-xl font-bold tracking-tight text-zinc-100">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
{T.homeTitle}
</h1>
<p class="mt-3 text-base text-zinc-400 max-w-md">
{T.homeSubtitle}
</p>
</header>
<BaseLayout>
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
<header class="mb-12">
<div class="flex items-center gap-2 mb-6">
<span
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
aria-hidden="true"></span>
<span class="text-xl font-bold tracking-tight text-zinc-100"
>{T.brandLabel}</span
>
</div>
<h1
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
>
{T.homeTitle}
</h1>
<p class="mt-3 text-base text-zinc-400 max-w-md">
{T.homeSubtitle}
</p>
</header>
<AdBanner size="leaderboard" />
<AdBanner size="leaderboard" />
<main class="mt-8">
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
role="list"
aria-label="Generator catalog"
>
{generators.map((generator) => (
<div role="listitem">
<GeneratorCard generator={generator} />
</div>
))}
</div>
</main>
</div>
<main class="mt-8">
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
role="list"
aria-label="Generator catalog"
>
{
generators.map((generator) => (
<div role="listitem">
<GeneratorCard generator={generator} />
</div>
))
}
</div>
</main>
</div>
</BaseLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import CardGenerator from '../../../components/generators/CardGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import CardGenerator from "@/components/generators/CardGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'cards')!;
const generator = generators.find((g) => g.slug === "cards")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор карт</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><CardGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Укажите количество карт для розыгрыша.',
'Включите «Разрешить повторения», если нужны дубли.',
'Нажмите «Тянуть» — карты появятся на экране.',
'Нажмите «Копировать», чтобы скопировать результат.',
]} whenTo={[
'Случайная раздача карт для карточных игр.',
'Выбор случайных карт для гаданий.',
'Генерация случайных комбинаций карт для тестирования.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<CardGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import CoinGenerator from '../../../components/generators/CoinGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import CoinGenerator from "@/components/generators/CoinGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'coin')!;
const generator = generators.find((g) => g.slug === "coin")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Подбросить монетку</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><CoinGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Выберите количество монет с помощью ползунка.',
'Нажмите «Подбросить» — орёл или решка.',
'При нескольких монетах отображается итоговый счёт.',
'Нажмите «Копировать», чтобы сохранить результат.',
]} whenTo={[
'Принятие решений по принципу «орёл или решка».',
'Определение очерёдности в игре.',
'Симуляция случайных событий с равной вероятностью.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<CoinGenerator />
</GeneratorLayout>
+7 -35
View File
@@ -1,39 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import ColorGenerator from '../../../components/generators/ColorGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import ColorGenerator from "@/components/generators/ColorGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'colors')!;
const generator = generators.find((g) => g.slug === "colors")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор цветов</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><ColorGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Нажмите «Сгенерировать», чтобы получить случайный цвет.',
'Цвет отображается в форматах HEX, RGB и HSL одновременно.',
'Нажмите на иконку копирования рядом с нужным форматом.',
]} whenTo={[
'Поиск вдохновения для цветовой палитры.',
'Случайный выбор цвета для дизайна или иллюстрации.',
'Генерация тестовых данных с цветовыми значениями.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<ColorGenerator />
</GeneratorLayout>
+7 -39
View File
@@ -1,43 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import DiceGenerator from '../../../components/generators/DiceGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import DiceGenerator from "@/components/generators/DiceGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'dice')!;
const generator = generators.find((g) => g.slug === "dice")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Бросить кубик онлайн</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><DiceGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.',
'Выберите режим: Обычный, Преимущество или Помеха (только для одного кубика).',
'Нажмите «Бросить» — результаты отобразятся мгновенно.',
'Максимумы подсвечены фиолетовым, единицы — красным.',
]} whenTo={[
'Броски в настольных ролевых играх (D&D и другие).',
'Быстрые броски через нотацию: 2d6+3, 1d20 и любые другие.',
'Преимущество и помеха для механик D&D 5e.',
'Бросок характеристик: 4d6, отбросить наименьший (4d6dl1).',
'Взрывающиеся кубики для Savage Worlds или хаус-рулов (например, 3d6!).',
'Использование keep/drop модификаторов типа 4d6kh3 (оставить три лучших).',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<DiceGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import ListGenerator from '../../../components/generators/ListGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import ListGenerator from "@/components/generators/ListGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'list')!;
const generator = generators.find((g) => g.slug === "list")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Случайный выбор из списка</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><ListGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Введите список элементов — по одному на строку.',
'Укажите, сколько элементов выбрать.',
'При необходимости разрешите повторения.',
'Нажмите «Выбрать» — результаты появятся на экране.',
]} whenTo={[
'Розыгрыш победителей конкурса.',
'Случайное распределение задач между участниками команды.',
'Выбор случайного варианта из списка.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<ListGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import LotteryGenerator from '../../../components/generators/LotteryGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import LotteryGenerator from "@/components/generators/LotteryGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'lottery')!;
const generator = generators.find((g) => g.slug === "lottery")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор лотереи</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><LotteryGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Укажите диапазон чисел в полях «От» и «До».',
'Задайте количество чисел для розыгрыша.',
'Нажмите «Тянуть» — выпавшие числа появятся на экране.',
'Нажмите «Копировать», чтобы сохранить результат.',
]} whenTo={[
'Розыгрыш лотерейных номеров.',
'Жеребьёвка участников конкурса.',
'Случайный выбор нескольких уникальных чисел.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<LotteryGenerator />
</GeneratorLayout>
+7 -37
View File
@@ -1,41 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import NumberGenerator from '../../../components/generators/NumberGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import NumberGenerator from "@/components/generators/NumberGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'numbers')!;
const generator = generators.find((g) => g.slug === "numbers")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор чисел</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><NumberGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Введите минимальное значение в поле «От».',
'Введите максимальное значение в поле «До».',
'Нажмите «Сгенерировать» — случайное число появится мгновенно.',
'Нажмите на число, чтобы скопировать его в буфер обмена.',
]} whenTo={[
'Выбор победителя в розыгрыше или конкурсе.',
'Определение случайного первого игрока в настольной игре.',
'Быстрое принятие решения между пронумерованными вариантами.',
'Генерация тестовых данных с числами в заданном диапазоне.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<NumberGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import PasswordGenerator from '../../../components/generators/PasswordGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import PasswordGenerator from "@/components/generators/PasswordGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'password')!;
const generator = generators.find((g) => g.slug === "password")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор паролей</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><PasswordGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Задайте длину пароля с помощью ползунка.',
'Выберите типы символов: заглавные, строчные, цифры, спецсимволы.',
'Нажмите «Сгенерировать» — готовый пароль появится на экране.',
'Нажмите на пароль, чтобы скопировать его.',
]} whenTo={[
'Создание надёжного пароля для нового аккаунта.',
'Генерация секретного ключа или токена.',
'Регулярное обновление паролей для повышения безопасности.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<PasswordGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import UuidGenerator from '../../../components/generators/UuidGenerator.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import UuidGenerator from "@/components/generators/UuidGenerator.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'uuid')!;
const generator = generators.find((g) => g.slug === "uuid")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор UUID и токенов</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><UuidGenerator /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Выберите тип: UUID v4, Hex или Base64.',
'Для Hex и Base64 задайте длину в байтах.',
'Укажите количество токенов.',
'Нажмите «Сгенерировать» и скопируйте результат.',
]} whenTo={[
'Генерация уникальных идентификаторов для баз данных.',
'Создание токенов для API и сессий.',
'Генерация случайных ключей шифрования.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<UuidGenerator />
</GeneratorLayout>
+7 -36
View File
@@ -1,40 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import WheelSpinner from '../../../components/generators/WheelSpinner.astro';
import AdBanner from '../../../components/AdBanner.astro';
import SeoBlock from '../../../components/SeoBlock.astro';
import { generators } from '../../../data/generators';
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
import WheelSpinner from "@/components/generators/WheelSpinner.astro";
import { generators } from "@/data/generators";
const generator = generators.find((g) => g.slug === 'wheel')!;
const generator = generators.find((g) => g.slug === "wheel")!;
---
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
<nav aria-label="Breadcrumb" class="mb-10">
<a href="/ru/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded" aria-label="Все генераторы">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
Все генераторы
</a>
</nav>
<header class="mb-2">
<div class="flex items-center gap-2 mb-4">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Колесо фортуны онлайн</h1>
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
</header>
<main><WheelSpinner /></main>
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
<SeoBlock lang="ru" howTo={[
'Введите варианты в поле — по одному на строку (от 2 до 24).',
'Колесо обновляется автоматически.',
'Нажмите «Крутить» и дождитесь результата.',
'Победитель выделяется — скопируйте его кнопкой «Копировать».',
]} whenTo={[
'Случайный выбор победителя розыгрыша.',
'Принятие решений в команде.',
'Распределение ролей или задач между участниками.',
]} />
</div>
</BaseLayout>
<GeneratorLayout generator={generator}>
<WheelSpinner />
</GeneratorLayout>
+46 -43
View File
@@ -1,50 +1,53 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import GeneratorCard from '../../components/GeneratorCard.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
import { useT } from '../../i18n/translations';
import BaseLayout from "@/layouts/BaseLayout.astro";
import GeneratorCard from "@/components/GeneratorCard.astro";
import AdBanner from "@/components/AdBanner.astro";
import { generators } from "@/data/generators";
import { useT } from "@/i18n/translations";
import type { Lang } from "@/i18n/translations";
const T = useT('ru');
const lang = (Astro.currentLocale as Lang) || "en";
const T = useT(lang);
---
<BaseLayout
title={T.defaultTitle}
description={T.defaultDesc}
lang="ru"
>
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
<header class="mb-12">
<div class="flex items-center gap-2 mb-6">
<span
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
aria-hidden="true"
></span>
<span class="text-xl font-bold tracking-tight text-zinc-100">{T.brandLabel}</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
{T.homeTitle}
</h1>
<p class="mt-3 text-base text-zinc-400 max-w-md">
{T.homeSubtitle}
</p>
</header>
<BaseLayout>
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
<header class="mb-12">
<div class="flex items-center gap-2 mb-6">
<span
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
aria-hidden="true"></span>
<span class="text-xl font-bold tracking-tight text-zinc-100"
>{T.brandLabel}</span
>
</div>
<h1
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
>
{T.homeTitle}
</h1>
<p class="mt-3 text-base text-zinc-400 max-w-md">
{T.homeSubtitle}
</p>
</header>
<AdBanner size="leaderboard" />
<AdBanner size="leaderboard" />
<main class="mt-8">
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
role="list"
aria-label="Generator catalog"
>
{generators.map((generator) => (
<div role="listitem">
<GeneratorCard generator={generator} lang="ru" />
</div>
))}
</div>
</main>
</div>
<main class="mt-8">
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
role="list"
aria-label="Generator catalog"
>
{
generators.map((generator) => (
<div role="listitem">
<GeneratorCard generator={generator} />
</div>
))
}
</div>
</main>
</div>
</BaseLayout>