import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; import { cache } from "react"; import { defineQuery } from "next-sanity"; import { sanity } from "@/lib/sanity"; import { ArtistTabs } from "@/components/artist/ArtistTabs"; import { ArtistReleasesTab, type ArtistRelease } from "@/components/artist/ArtistReleasesTab"; import { ArtistConcertsTab } from "@/components/artist/ArtistConcertsTab"; import { ReleaseCover } from "@/components/release/ReleaseCover"; import { AnimatedText } from "@/components/AnimatedText"; import { urlFor } from "@/lib/sanityImage"; import { ARTIST_PLACEHOLDER_SRC } from "@/lib/constants"; import { Breadcrumb } from "@/components/Breadcrumb"; import type { ConcertData } from "@/components/concert/ConcertTable"; export const dynamicParams = true; export const revalidate = 86400; type Artist = { name?: string; slug?: string; role?: string; bio?: any; image?: any; releases?: ArtistRelease[]; upcomingConcerts?: ConcertData[]; pastConcerts?: ConcertData[]; }; const CONCERT_PROJECTION = `{ _id, title, subtitle, date, time, locationName, city, country, "artists": artists[]->{ _id, name, "slug": slug.current }, ticketUrl }`; const ARTIST_DETAIL_QUERY = defineQuery(` *[_type == "artist" && slug.current == $slug][0]{ name, role, "slug": slug.current, bio, image, "releases": *[ _type == "release" && references(^._id) ] | order(releaseDate desc, catalogNo desc) { _id, name, albumArtist, catalogNo, "slug": slug.current, releaseDate, albumCover, genre, instrumentation }, "upcomingConcerts": *[ _type == "concert" && references(^._id) && date >= $today ] | order(date asc, time asc) ${CONCERT_PROJECTION}, "pastConcerts": *[ _type == "concert" && references(^._id) && date < $today ] | order(date desc, time desc) ${CONCERT_PROJECTION} } `); const getArtist = cache(async (slug: string) => { const today = new Date().toISOString().slice(0, 10); try { return await sanity.fetch(ARTIST_DETAIL_QUERY, { slug, today }); } catch (error) { console.error("Failed to fetch artist:", error); return null; } }); const ARTIST_SLUGS_QUERY = defineQuery( `*[_type == "artist" && defined(slug.current)]{ "slug": slug.current }`, ); export async function generateStaticParams() { const slugs = await sanity.fetch>(ARTIST_SLUGS_QUERY); return slugs.map((a) => ({ slug: a.slug })); } export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug: rawSlug } = await params; const slug = rawSlug.toLowerCase(); const artist = await getArtist(slug); if (!artist) notFound(); const description = `Explore ${artist.name}'s releases${artist.role ? `, ${artist.role}` : ""} on TRPTK.`; const ogImage = artist.image ? urlFor(artist.image).width(1200).height(630).url() : undefined; return { title: `${artist.name} ${artist.role ? ` • ${artist.role}` : ""}`, description, alternates: { canonical: `/artist/${slug}` }, openGraph: { title: artist.name, description: `${artist.role || "Artist"}${artist.releases?.length ? ` - ${artist.releases.length} releases` : ""}`, type: "profile", ...(ogImage && { images: [{ url: ogImage, width: 1200, height: 630, alt: artist.name }] }), }, twitter: { card: "summary_large_image", title: artist.name, description, ...(ogImage && { images: [ogImage] }), }, }; } export default async function ArtistPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; if (!slug) notFound(); const normalizedSlug = slug.toLowerCase(); if (slug !== normalizedSlug) { redirect(`/artist/${normalizedSlug}`); } const artist = await getArtist(slug); if (!artist) notFound(); const displayName = artist.name ?? ""; const displayRole = artist.role ?? ""; const jsonLd = { "@context": "https://schema.org", "@type": "Person", name: artist.name, url: `https://trptk.com/artist/${slug}`, ...(artist.role && { jobTitle: artist.role }), ...(artist.image && { image: urlFor(artist.image).width(800).url() }), }; return ( <>