4b1ef9f951
Build & Push Image / Build and push to Gitea Registry (push) Successful in 2m28s
PocketBase pricing_{lang} остаётся источником переводного маркетингового
контента (название, тэглайн, описание, фичи, порядок) — но сами цифры
price1m/6m/1y теперь подменяются данными с GET /billing/plans нового
бэкенда (см. netrunner-backend::modules::geoip): валюта определяется по
IP посетителя, VPN/датацентр всегда получает базовый USD.
Соответствие тарифа PocketBase к тарифу бэкенда — по полю `name`
(ожидается "GHOST"/"NETRUNNER"/"MAINFRAME"). Бэкенд недоступен или для
конкретного тарифа не нашлось соответствия — тихий фолбэк на цену как
она есть в PocketBase, страница не ломается (тот же принцип, что уже
был у offline-кеша).
73 lines
3.4 KiB
TypeScript
73 lines
3.4 KiB
TypeScript
import { Suspense } from "react";
|
||
import dynamic from "next/dynamic";
|
||
import { getDictionary } from "@/lib/dictionaries";
|
||
import { Hero } from "@/components/sections/Hero";
|
||
import { Features } from "@/components/sections/Features";
|
||
|
||
const SectionSkeleton = () => (
|
||
<div className="h-[500px] w-full animate-pulse bg-card/20 rounded-3xl my-10" />
|
||
);
|
||
|
||
// Тяжёлые интерактивные секции (framer-motion анимации, клиентские запросы к PocketBase,
|
||
// realtime-подписки) грузятся через next/dynamic, а не обычным import, чтобы не раздувать
|
||
// первый JS-бандл главной страницы: Hero и Features рендерятся сразу (статично, легче),
|
||
// а Pricing/FAQ/TerminalGuestbook/BlogPreview подгружаются отдельными чанками с плейсхолдером
|
||
// SectionSkeleton, пока грузится JS.
|
||
const Pricing = dynamic(
|
||
() => import("@/components/sections/Pricing").then((mod) => mod.Pricing),
|
||
{ loading: () => <SectionSkeleton /> },
|
||
);
|
||
const FAQ = dynamic(
|
||
() => import("@/components/sections/Faq").then((mod) => mod.FAQ),
|
||
{ loading: () => <SectionSkeleton /> },
|
||
);
|
||
const TerminalGuestbook = dynamic(
|
||
() =>
|
||
import("@/components/sections/TerminalGuestbook").then(
|
||
(mod) => mod.TerminalGuestbook,
|
||
),
|
||
{ loading: () => <SectionSkeleton /> },
|
||
);
|
||
const BlogPreview = dynamic(() =>
|
||
import("@/components/sections/BlogPreview").then((mod) => mod.BlogPreview),
|
||
);
|
||
|
||
export default async function Home({
|
||
params,
|
||
}: {
|
||
params: Promise<{ lang: string }>;
|
||
}) {
|
||
const { lang } = await params;
|
||
const dict = await getDictionary(lang);
|
||
|
||
// Читаем PB_PUBLIC_URL, а не NEXT_PUBLIC_PB_URL: Next.js статически подставляет
|
||
// NEXT_PUBLIC_*-переменные везде, где они встречаются в коде (в т.ч. внутри серверных
|
||
// компонентов), на этапе `next build` — так что чтение NEXT_PUBLIC_PB_URL здесь давало бы
|
||
// то же самое собранное-в-образ значение, что и в клиентском бандле. PB_PUBLIC_URL —
|
||
// обычная (не NEXT_PUBLIC_) переменная, поэтому читается по-настоящему заново при каждом
|
||
// запросе (см. подробный комментарий в app/[lang]/auth/auth-client.tsx про ACCOUNT_ORIGIN).
|
||
const pbUrl =
|
||
process.env.PB_PUBLIC_URL || "https://netrunner-vpn.com/cms-api";
|
||
// Те же соображения про NEXT_PUBLIC_ vs обычную переменную, что и у pbUrl
|
||
// выше (см. комментарий) — см. также app/[lang]/auth/page.tsx, тот же
|
||
// паттерн для того же самого бэкенда.
|
||
const accountOrigin =
|
||
process.env.ACCOUNT_ORIGIN || "https://account.netrunner-vpn.com";
|
||
const apiUrl = process.env.API_URL || `${accountOrigin}/api/v1`;
|
||
|
||
return (
|
||
<>
|
||
<Hero dict={dict.hero} lang={lang} />
|
||
<Features dict={dict.features} />
|
||
<Pricing dict={dict.pricing} lang={lang} pbUrl={pbUrl} apiUrl={apiUrl} />
|
||
<TerminalGuestbook dict={dict.guestbook} pbUrl={pbUrl} />
|
||
|
||
<Suspense fallback={<SectionSkeleton />}>
|
||
<BlogPreview lang={lang} dict={dict.blog} />
|
||
</Suspense>
|
||
|
||
<FAQ dict={dict.faq} lang={lang} />
|
||
</>
|
||
);
|
||
}
|