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:
@@ -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 -->
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user