ПубликацияИскусство РФ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"> ); }
Обсуждение

Комментарии

0/2000 символов
Войти или зарегистрироваться
Загружаем комментарии...