import { client, urlFor } from "@/lib/sanity"; import { PortableText, PortableTextComponents } from "@portabletext/react"; import Image from "next/image"; import Link from "next/link"; import { ArrowLeft, Rss } from "lucide-react"; import { Separator } from "@/components/ui/separator"; import { notFound } from "next/navigation"; import { Metadata } from "next"; import { format } from "date-fns"; import Prism from "prismjs"; import "prismjs/components/prism-javascript"; import "prismjs/components/prism-python"; import "prismjs/components/prism-php"; import "prismjs/components/prism-bash"; import "prismjs/components/prism-sql"; import "prismjs/components/prism-yaml"; import "prismjs/components/prism-markdown"; import "prismjs/components/prism-json"; import "prismjs/components/prism-perl"; import CopyButton from "@/components/shared/copyButton"; import "./_styles/prism-twilight.css"; import "./_styles/prism.twilight.min.css"; import PrismLoader from "@/components/loader/prismLoader"; import { Button } from "@/components/ui/button"; async function getData(slug: string) { const query = ` *[_type == "blog" && slug.current == '${slug.replace(/\'/g, "\\'")}'] { "currentSlug": slug.current, title, content, smallDescription, titleImage, _createdAt }[0]`; const data = await client.fetch(query, {}, { cache: "no-store" }); return data; } interface BlogSlugArticle { currentSlug: string; title: string; content: any; titleImage: string; _createdAt: string; } export const dynamic = "force-static"; export async function generateMetadata(props: { params: Promise<{ slug: string }>; }): Promise<Metadata> { const params = await props.params; const data = await getData(params.slug); if (!data) { return { title: "404 Not Found - SVR.JS", openGraph: { title: "404 Not Found - SVR.JS" }, twitter: { title: "404 Not Found - SVR.JS" } }; } return { title: `${data.title} - SVR.JS`, description: data.smallDescription, alternates: { canonical: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog/${data.currentSlug}` }, openGraph: { title: `${data.title} - SVR.JS`, description: data.smallDescription, url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog/${data.currentSlug}`, type: "website", images: [ { url: data.titleImage ? urlFor(data.titleImage).url() : `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog-missing.png`, width: 800, height: 600, alt: `${data.title} - SVR.JS` } ] }, twitter: { card: "summary_large_image", site: "@SVR_JS", title: `${data.title} - SVR.JS`, description: data.smallDescription, images: [ data.titleImage ? urlFor(data.titleImage).url() : `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog-missing.png` ], creator: "@SVR_JS" } }; } const customPortableTextComponents: PortableTextComponents = { types: { image: ({ value }) => { return ( <div className="my-8"> <Image src={urlFor(value).url()} alt={value.alt || "Blog Image"} width={1200} height={800} className="w-full h-auto rounded-lg" /> {value.caption && ( <p className="mt-2 text-center text-sm text-muted-foreground"> {value.caption} </p> )} </div> ); }, code: ({ value }) => { const language = value.language || "none"; const grammar = Prism.languages[language]; if (language != "none" && !grammar) { console.error(`No grammar found for language: "${language}"`); } return ( <div className="relative my-8"> <pre className={`language-${language} p-4 rounded-md overflow-x-auto text-sm`} > <code className={`language-${language}`}>{value.code}</code> </pre> {language == "none" ? "" : <PrismLoader />} <CopyButton code={value.code} /> </div> ); } } }; export default async function BlogSlugArticle(props: { params: Promise<{ slug: string }>; }) { const params = await props.params; const data: BlogSlugArticle = await getData(params.slug); if (!data) { notFound(); } const formattedDate = format(new Date(data._createdAt), "MMMM d, yyyy"); return ( <> <section className="max-w-5xl container mx-auto py-8 md:py-28 flex flex-col items-center px-4"> <div className="w-full mx-auto flex-center"> <Link href="/blog" className="group text-primary transition-all flex items-center" > <Button variant={"ghost"} size={"lg"} className="mx-0 px-2 "> <ArrowLeft className="mr-2 w-5 h-5 group-hover:translate-x-1 transition-all" /> Back </Button> </Link> <Link href="/rss.xml" className="ml-auto" rel="alternate" type="application/rss+xml" > <Button variant={"link"} size={"lg"} className="mx-0 px-2 text-primary" > <Rss className="w-5 h-5 mr-1" /> Subscribe to RSS </Button> </Link> </div> <header className="text-start mb-8 w-full"> <div className="mb-2"> <h1 className="text-3xl md:text-5xl mb-8 py-4 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400"> {data.title} </h1> <Image src={ data.titleImage ? urlFor(data.titleImage).url() : "/blog-missing.png" } alt={data.title} width={1200} height={800} priority className="w-full h-auto object-cover rounded-md" /> <p className="mt-4 text-lg md:text-xl text-muted-foreground"> Published on: {formattedDate} </p> </div> </header> <Separator className="mb-6" /> <article className="prose max-w-full md:prose-lg dark:prose-invert"> <PortableText value={data.content} components={customPortableTextComponents} /> </article> </section> </> ); } export async function generateStaticParams() { const query = `*[_type == 'blog']{ "slug": slug.current, }`; const slugsRaw = await client.fetch(query); return slugsRaw; }