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
+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>