import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; import { cache } from "react"; import { PortableText } from "@portabletext/react"; import { defineQuery } from "next-sanity"; import { sanity } from "@/lib/sanity"; import { ReleaseCover } from "@/components/release/ReleaseCover"; import { AnimatedText } from "@/components/AnimatedText"; import { urlFor } from "@/lib/sanityImage"; import { portableTextComponents } from "@/lib/portableTextComponents"; import { ARTIST_PLACEHOLDER_SRC } from "@/lib/constants"; import { Breadcrumb } from "@/components/Breadcrumb"; import { ArtistCardCompact } from "@/components/artist/ArtistCardCompact"; import { ReleaseCard, type ReleaseCardData } from "@/components/release/ReleaseCard"; import { formatYears, type ArtistCardData, type ComposerCardData } from "@/components/artist/types"; import type { SanityImageSource } from "@sanity/image-url"; export const dynamicParams = true; export const revalidate = 86400; type BlogDetail = { title?: string; subtitle?: string; slug?: string; author?: string; publishDate?: string; category?: string; featuredImage?: SanityImageSource; content?: any; releases?: ReleaseCardData[]; artists?: ArtistCardData[]; composers?: ComposerCardData[]; }; const BLOG_DETAIL_QUERY = defineQuery(` *[_type == "blog" && slug.current == $slug][0]{ title, subtitle, "slug": slug.current, author, publishDate, category, featuredImage, content[]{ ..., _type == "image" => { ..., asset-> } }, "releases": releases[]->{ _id, name, albumArtist, catalogNo, releaseDate, "slug": slug.current, albumCover }, "artists": artists[]->{ _id, name, role, "slug": slug.current, image }, "composers": composers[]->{ _id, name, sortKey, "slug": slug.current, birthYear, deathYear, image } } `); const getBlog = cache(async (slug: string) => { try { return await sanity.fetch(BLOG_DETAIL_QUERY, { slug }); } catch (error) { console.error("Failed to fetch blog:", error); return null; } }); const BLOG_SLUGS_QUERY = defineQuery( `*[_type == "blog" && defined(slug.current)]{ "slug": slug.current }`, ); export async function generateStaticParams() { const slugs = await sanity.fetch>(BLOG_SLUGS_QUERY); return slugs.map((b) => ({ slug: b.slug })); } export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug: rawSlug } = await params; const slug = rawSlug.toLowerCase(); const blog = await getBlog(slug); if (!blog) notFound(); const description = blog.subtitle ?? `${blog.title} • Read more on the TRPTK blog.`; const ogImage = blog.featuredImage ? urlFor(blog.featuredImage).width(1200).height(630).url() : undefined; return { title: `${blog.title}`, description, alternates: { canonical: `/blog/${slug}` }, openGraph: { title: blog.title, description, type: "article", ...(blog.publishDate && { publishedTime: blog.publishDate }), ...(blog.author && { authors: [blog.author] }), ...(ogImage && { images: [{ url: ogImage, width: 1200, height: 630, alt: blog.title }], }), }, twitter: { card: "summary_large_image", title: blog.title, description, ...(ogImage && { images: [ogImage] }), }, }; } function formatPublishDate(dateString?: string) { if (!dateString) return null; return new Intl.DateTimeFormat("en-US", { day: "numeric", month: "long", year: "numeric", }).format(new Date(dateString)); } export default async function BlogPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; if (!slug) notFound(); const normalizedSlug = slug.toLowerCase(); if (slug !== normalizedSlug) { redirect(`/blog/${normalizedSlug}`); } const blog = await getBlog(slug); if (!blog) notFound(); const displayDate = formatPublishDate(blog.publishDate); const artists = blog.artists ?? []; const composers = (blog.composers ?? []).sort((a, b) => (a.sortKey ?? "").localeCompare(b.sortKey ?? ""), ); const releases = blog.releases ?? []; const jsonLd = { "@context": "https://schema.org", "@type": "BlogPosting", headline: blog.title, url: `https://trptk.com/blog/${slug}`, ...(blog.publishDate && { datePublished: blog.publishDate }), ...(blog.author && { author: { "@type": "Person", name: blog.author }, }), ...(blog.featuredImage && { image: urlFor(blog.featuredImage).width(1200).url(), }), ...(blog.subtitle && { description: blog.subtitle }), publisher: { "@type": "Organization", name: "TRPTK", url: "https://trptk.com", }, }; return ( <>