ПубликацияИскусство РФ22 мая 2026 г.
Публичная выставка старых книг и самоваров
Публичная выставка старых книг и самоваров в Москве и области.
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import SiteHeader from "../../../components/site/SiteHeader";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://iskusstvorf.ru";
type PageProps = {
params: Promise<{
slug: string;
}>;
};
type PublicPost = {
id: string;
ownerType: string;
ownerUserId: string | null;
projectId: string | null;
professionalProfileId: string | null;
type: string;
title: string;
slug: string;
excerpt: string | null;
content: string | null;
imageUrl: string | null;
status: string;
seoTitle: string | null;
seoDescription: string | null;
publishedAt: string | null;
createdAt: string;
updatedAt: string;
};
type PostResponse = {
post?: PublicPost | null;
};
function formatDate(value: string | null | undefined) {
if (!value) {
return "";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "long",
year: "numeric",
}).format(date);
}
function getOwnerLabel(ownerType: string) {
if (ownerType === "PROJECT") {
return "Проект";
}
if (ownerType === "USER") {
return "Пользователь";
}
if (ownerType === "PROFESSIONAL_PROFILE") {
return "Профессиональный профиль";
}
return "Искусство РФ";
}
function buildDescription(post: PublicPost) {
const value = post.seoDescription || post.excerpt || post.content || "";
if (!value) {
return "Публикация на платформе «Искусство РФ».";
}
const normalized = value.replace(/\s+/g, " ").trim();
if (normalized.length <= 160) {
return normalized;
}
return `${normalized.slice(0, 157)}...`;
}
async function fetchPost(slug: string) {
try {
const response = await fetch(
`${SITE_URL}/api/publikatsii?slug=${encodeURIComponent(slug)}`,
{
cache: "no-store",
},
);
if (!response.ok) {
return null;
}
const data = (await response.json()) as PostResponse;
return data.post || null;
} catch (error) {
console.error("fetchPost error:", error);
return null;
}
}
function buildArticleJsonLd(post: PublicPost) {
return {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: buildDescription(post),
image: post.imageUrl ? [post.imageUrl] : undefined,
datePublished: post.publishedAt || post.createdAt,
dateModified: post.updatedAt || post.publishedAt || post.createdAt,
author: {
"@type": "Organization",
name: getOwnerLabel(post.ownerType),
},
publisher: {
"@type": "Organization",
name: "Искусство РФ",
url: SITE_URL,
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `${SITE_URL}/publikatsii/${post.slug}`,
},
};
}
function buildBreadcrumbJsonLd(post: PublicPost) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Главная",
item: SITE_URL,
},
{
"@type": "ListItem",
position: 2,
name: "Публикации",
item: `${SITE_URL}/publikatsii`,
},
{
"@type": "ListItem",
position: 3,
name: post.title,
item: `${SITE_URL}/publikatsii/${post.slug}`,
},
],
};
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPost(slug);
if (!post) {
return {
title: "Публикация не найдена — Искусство РФ",
};
}
const title = post.seoTitle || `${post.title} — Искусство РФ`;
const description = buildDescription(post);
const canonical = `${SITE_URL}/publikatsii/${post.slug}`;
return {
title,
description,
alternates: {
canonical,
},
openGraph: {
title,
description,
url: canonical,
siteName: "Искусство РФ",
type: "article",
images: post.imageUrl
? [
{
url: post.imageUrl,
alt: post.title,
},
]
: undefined,
publishedTime: post.publishedAt || post.createdAt,
modifiedTime: post.updatedAt,
},
};
}
export default async function PublikatsiyaPage({ params }: PageProps) {
const { slug } = await params;
const post = await fetchPost(slug);
if (!post) {
notFound();
}
const articleJsonLd = buildArticleJsonLd(post);
const breadcrumbJsonLd = buildBreadcrumbJsonLd(post);
return (
<div className="min-h-screen bg-neutral-50 text-neutral-950">
<SiteHeader />
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
<nav className="text-sm text-neutral-500">
<Link href="/" className="hover:text-red-700">
Главная
</Link>
<span className="mx-2">/</span>
<Link href="/publikatsii" className="hover:text-red-700">
Публикации
</Link>
<span className="mx-2">/</span>
<span>{post.title}</span>
</nav>
<article className="overflow-hidden rounded-[36px] border border-neutral-200 bg-white shadow-sm">
{post.imageUrl ? (
<div className="border-b border-neutral-100 bg-neutral-50">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.imageUrl}
alt={post.title}
className="max-h-[560px] w-full object-contain"
/>
</div>
) : null}
<div className="p-6 md:p-8 lg:p-10">
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-neutral-200 bg-neutral-50 px-3 py-1 text-xs font-medium text-neutral-700">
{post.type}
</span>
<span className="rounded-full border border-neutral-200 bg-neutral-50 px-3 py-1 text-xs font-medium text-neutral-700">
{getOwnerLabel(post.ownerType)}
</span>
<span className="rounded-full border border-neutral-200 bg-neutral-50 px-3 py-1 text-xs font-medium text-neutral-700">
{formatDate(post.publishedAt || post.createdAt)}
</span>
</div>
<h1 className="mt-6 text-4xl font-semibold tracking-tight text-neutral-950 md:text-6xl">
);
}
Обсуждение
Комментарии
Загружаем комментарии...