TASKS-1823 WebManager module

This commit is contained in:
Igor Martyniuk
2026-08-28 08:35:58 +00:00
parent fdb2cf3837
commit c93d8c025b
654 changed files with 26232 additions and 6075 deletions
@@ -0,0 +1,47 @@
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { resolvePageRenderData } from '#web-runtime/api/render-data';
import { pageMetadata } from '#web-runtime/lib/page-metadata';
import PageRenderer from '#web-runtime/renderer';
import { getPathname } from '#i18n/navigation';
import { routing } from '#i18n/routing';
interface PageRouteProps {
params: Promise<{
locale: string;
path?: string[];
}>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}
function routePath(path?: string[]): string {
return path?.join('/') ?? '';
}
function publicPath(locale: string, path: string): string {
return getPathname({ locale, href: path ? `/${path}` : '/' });
}
export async function generateMetadata({ params }: PageRouteProps): Promise<Metadata> {
const { locale, path } = await params;
const data = await resolvePageRenderData(locale, routePath(path));
if (!data) {
const t = await getTranslations({ locale, namespace: 'Pages.NotFoundPage' });
return { title: t('metaTitle') };
}
return pageMetadata(data, routing.defaultLocale, publicPath);
}
export default async function PublicPage({ params, searchParams }: PageRouteProps) {
const { locale, path } = await params;
const data = await resolvePageRenderData(locale, routePath(path));
if (!data) {
notFound();
}
return <PageRenderer data={data} searchParams={await searchParams} />;
}
+49
View File
@@ -0,0 +1,49 @@
import type { Metadata } from 'next';
import { hasLocale, NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { siteUrl } from '#web-runtime/lib/site-url';
import { routing } from '#i18n/routing';
import '../globals.css';
export async function generateMetadata({
params
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
return {
metadataBase: siteUrl(),
title: {
default: t('title'),
template: t('template')
},
description: t('description')
};
}
export default async function LocaleLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale} className="scroll-smooth bg-[#f4f1e9] motion-reduce:scroll-auto">
<body className="min-h-dvh bg-[#f4f1e9] font-sans text-[#172019] antialiased">
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { useTranslations } from 'next-intl';
import { Link } from '#i18n/navigation';
import { Button } from '#shared/ui/Button';
export default function NotFound() {
const t = useTranslations('Pages.NotFoundPage');
return (
<main className="grid min-h-dvh place-items-center p-8">
<div className="max-w-176">
<p className="mb-5 font-mono text-xs font-bold tracking-[0.12em] text-[#517000] uppercase">
404
</p>
<h1 className="max-w-[15ch] text-[clamp(3rem,8vw,6rem)] leading-[0.98] font-semibold tracking-[-0.065em]">
{t('title')}
</h1>
<p className="mt-6 mb-8 text-lg text-[#5d675f]">{t('description')}</p>
<Button asChild>
<Link href="/">{t('cta')}</Link>
</Button>
</div>
</main>
);
}
+38
View File
@@ -0,0 +1,38 @@
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { resolvePreviewRenderData } from '#web-runtime/api/render-data';
import PageRenderer from '#web-runtime/renderer';
export const metadata: Metadata = {
title: 'Preview',
robots: { index: false, follow: false }
};
export const dynamic = 'force-dynamic';
interface PreviewRouteProps {
searchParams: Promise<{ token?: string }>;
}
async function PreviewUnavailable() {
const t = await getTranslations('Pages.PreviewPage');
return (
<main className="flex min-h-dvh flex-col items-center justify-center gap-3 px-6 text-center">
<h1 className="text-2xl font-semibold">{t('title')}</h1>
<p className="max-w-md text-sm opacity-70">{t('description')}</p>
</main>
);
}
export default async function PreviewPage({ searchParams }: PreviewRouteProps) {
const { token } = await searchParams;
const data = token ? await resolvePreviewRenderData(token) : null;
if (!data) {
return <PreviewUnavailable />;
}
return <PageRenderer data={data} />;
}
+52
View File
@@ -0,0 +1,52 @@
import { revalidateTag } from 'next/cache';
const MAX_TAGS = 50;
const MAX_TAG_LENGTH = 256;
function parseTags(payload: unknown): string[] | null {
if (typeof payload !== 'object' || payload === null) {
return null;
}
const tags = (payload as { tags?: unknown }).tags;
if (!Array.isArray(tags) || tags.length === 0 || tags.length > MAX_TAGS) {
return null;
}
if (
!tags.every((tag) => typeof tag === 'string' && tag.length > 0 && tag.length <= MAX_TAG_LENGTH)
) {
return null;
}
return tags;
}
export async function POST(request: Request) {
const secret = process.env['REVALIDATE_SECRET'];
if (!secret || request.headers.get('x-revalidate-secret') !== secret) {
return Response.json({ message: 'Unauthorized' }, { status: 401 });
}
let payload: unknown;
try {
payload = await request.json();
} catch {
return Response.json({ message: 'Invalid JSON body' }, { status: 400 });
}
const tags = parseTags(payload);
if (!tags) {
return Response.json(
{ message: 'Body must contain a non-empty "tags" array of strings' },
{ status: 400 }
);
}
for (const tag of tags) {
revalidateTag(tag, 'max');
}
return Response.json({ revalidated: tags });
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
+17
View File
@@ -0,0 +1,17 @@
import type { MetadataRoute } from 'next';
import { siteUrl } from '#web-runtime/lib/site-url';
export default function robots(): MetadataRoute.Robots {
const origin = siteUrl();
return {
rules: {
userAgent: '*',
allow: '/',
// Preview URLs render unpublished drafts behind a signed token; they must never be indexed.
// The page also sends `noindex`, this just saves crawlers the fetch.
disallow: '/*/preview'
},
sitemap: new URL('/sitemap.xml', origin).href
};
}
+15
View File
@@ -0,0 +1,15 @@
import type { MetadataRoute } from 'next';
import { getWebRuntimeAPI } from '#web-runtime/api/web-runtime-api';
import { siteUrl } from '#web-runtime/lib/site-url';
import { buildSitemap } from '#web-runtime/lib/sitemap';
import { getPathname } from '#i18n/navigation';
function publicPath(locale: string, path: string): string {
return getPathname({ locale, href: path ? `/${path}` : '/' });
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const entries = await getWebRuntimeAPI().listSitemapPages();
return buildSitemap(entries, siteUrl(), publicPath);
}