import type { Metadata } from "next"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { Megaphone, Pin } from "lucide-react"; import { fetchAnnouncementDetail, fetchOverview } from "@/lib/api"; import { authCookieHeader } from "@/lib/cookies"; import { getMeCached, getPublicSettingsCached } from "@/lib/serverData"; import { hasPerm, PERMS } from "@/lib/roles"; import { extractToc, estimateReadingMinutes, markdownHeadingToc } from "@/lib/toc"; import MarkdownBody from "@/components/MarkdownBody"; import PostToc from "@/components/PostToc"; import { SiteDocShell, SiteDocIdentity, SiteDocPageNav, SiteDocAnnoNav, SiteDocMasthead, } from "@/components/site-doc"; import { requestCanonical } from "@/lib/canonicalMeta"; import { announcementPath } from "@/lib/urlStyle"; import { webPageJsonLd } from "@/lib/jsonLd"; import JsonLd from "@/components/JsonLd"; interface PageProps { params: Promise<{ id: string }>; } export async function generateMetadata({ params }: PageProps): Promise { const { id } = await params; const data = await fetchAnnouncementDetail(id).catch(() => null); if (!data) return { title: "公告不存在" }; const urlStyle = (await getPublicSettingsCached()).url_style; return { title: data.announcement.title, description: `站点公告 · ${data.announcement.tag}`, openGraph: { title: data.announcement.title, description: `站点公告 · ${data.announcement.tag}`, type: "article", publishedTime: data.announcement.created_at, }, ...(await requestCanonical(announcementPath(urlStyle, data.announcement.id))), }; } export default async function AnnouncementPage({ params }: PageProps) { const { id } = await params; const cookie = authCookieHeader(await cookies()); const [data, me, overview, settings] = await Promise.all([ fetchAnnouncementDetail(id, cookie || undefined).catch(() => null), getMeCached(cookie || undefined), fetchOverview(cookie || undefined).catch(() => null), getPublicSettingsCached(), ]); if (!data) notFound(); const a = data.announcement; const annId = Number(id); const loginHref = `/login?redirect=${encodeURIComponent(`/announcement/${id}`)}`; const sidebarPages = overview?.sidebar_pages ?? []; const announcements = overview?.announcements ?? []; const canManagePages = !!me.user && hasPerm(me.user, PERMS.PAGES); const siteLead = settings.site_description?.trim() || undefined; const urlStyle = settings.url_style; const toc = extractToc(a.content); const headingToc = markdownHeadingToc(toc); const readingMinutes = estimateReadingMinutes(a.content, a.title.length); let headingIndex = 0; const headingRenderer = (Tag: "h2" | "h3") => function Heading({ children }: { children?: React.ReactNode }) { const item = headingToc[headingIndex++]; return ( {children} ); }; const jsonLd = webPageJsonLd({ type: "Article", name: a.title, path: announcementPath(urlStyle, a.id), datePublished: a.created_at, dateModified: a.updated_at, articleSection: "站点公告", keywords: a.tag, }); const left = ( <> ); const right = ( <> {toc.length > 0 ? : null} ); return ( <>
{a.pinned ? ( ) : null} {a.tag} } /> 0 ? { h2: headingRenderer("h2"), h3: headingRenderer("h3") } : undefined } />
{/* 手机端 #main 无底部内边距,补 mb 与 footer 拉开距离;桌面端由 lg:pb 提供,归零 */}
由站点管理团队发布
); }