mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 18:58:10 +00:00
feature: intergrated directus for posts
This commit is contained in:
294
app/(website)/tech-talk/[slug]/page.tsx
Normal file
294
app/(website)/tech-talk/[slug]/page.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { getPostBySlug, getPosts } from "@/lib/query/post";
|
||||
import type { Metadata } from "next";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ContentBlock,
|
||||
OutputData,
|
||||
ParagraphData,
|
||||
HeaderData,
|
||||
ListData,
|
||||
ImageData,
|
||||
QuoteData,
|
||||
CodeData,
|
||||
} from "@/types";
|
||||
|
||||
type Props = {
|
||||
params: { slug: string };
|
||||
};
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts();
|
||||
if (!Array.isArray(posts)) {
|
||||
console.error("getPosts did not return an array:", posts);
|
||||
return [];
|
||||
}
|
||||
return posts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
function extractDescriptionFromBlocks(
|
||||
content: OutputData | null
|
||||
): string | null {
|
||||
if (
|
||||
!content ||
|
||||
!content.blocks ||
|
||||
!Array.isArray(content.blocks) ||
|
||||
content.blocks.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const firstParagraph = content.blocks.find(
|
||||
(block): block is ContentBlock & { data: ParagraphData } =>
|
||||
block.type === "paragraph"
|
||||
);
|
||||
if (firstParagraph?.data?.text) {
|
||||
const plainText = firstParagraph.data.text
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/&[^;]+;/g, "");
|
||||
return plainText.substring(0, 160) + (plainText.length > 160 ? "..." : "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await getPostBySlug(slug, {
|
||||
fields: [
|
||||
"slug",
|
||||
"status",
|
||||
"user_created",
|
||||
"date_created",
|
||||
"user_updated",
|
||||
"date_updated",
|
||||
"title",
|
||||
"content",
|
||||
"excerpt",
|
||||
"featured_image",
|
||||
],
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
title: "Post Not Found",
|
||||
};
|
||||
}
|
||||
|
||||
const imageUrl = post.featured_image;
|
||||
|
||||
const descriptionFromContent =
|
||||
post.content &&
|
||||
typeof post.content === "object" &&
|
||||
Array.isArray(post.content.blocks)
|
||||
? extractDescriptionFromBlocks(post.content as OutputData)
|
||||
: null;
|
||||
|
||||
const description =
|
||||
post.excerpt || descriptionFromContent || "Read this OMS TechTalk article.";
|
||||
|
||||
const publishedTime = post.date_created
|
||||
? new Date(post.date_created).toISOString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
title: `${post.title} | OMS TechTalk`,
|
||||
description: description,
|
||||
alternates: {
|
||||
canonical: `/tech-talk/${post.slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: description,
|
||||
url: `https://oms.africa/tech-talk/${post.slug}`,
|
||||
type: "article",
|
||||
publishedTime: publishedTime,
|
||||
...(imageUrl && {
|
||||
images: [
|
||||
{
|
||||
url: imageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.title,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: description,
|
||||
...(imageUrl && { images: [imageUrl] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const renderBlock = (block: ContentBlock) => {
|
||||
const renderListItems = (items: string[]) => {
|
||||
return items.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="mb-2"
|
||||
dangerouslySetInnerHTML={{ __html: item }}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
switch (block.type) {
|
||||
case "header":
|
||||
const headerBlock = block as ContentBlock & { data: HeaderData };
|
||||
const level = headerBlock.data.level || 2;
|
||||
// Ensure HeaderTag has a type that JSX understands for intrinsic elements.
|
||||
const HeaderTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
return (
|
||||
<HeaderTag
|
||||
key={block.id}
|
||||
className={`text-${
|
||||
6 - (headerBlock.data.level || 2)
|
||||
}xl font-bold my-4 text-primary`}
|
||||
dangerouslySetInnerHTML={{ __html: headerBlock.data.text }}
|
||||
/>
|
||||
);
|
||||
case "paragraph":
|
||||
const paragraphBlock = block as ContentBlock & { data: ParagraphData };
|
||||
return (
|
||||
<p
|
||||
key={block.id}
|
||||
className="my-4 leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraphBlock.data.text }}
|
||||
/>
|
||||
);
|
||||
case "list":
|
||||
const listBlock = block as ContentBlock & { data: ListData };
|
||||
const ListTag = listBlock.data.style === "ordered" ? "ol" : "ul";
|
||||
return (
|
||||
<ListTag
|
||||
key={block.id}
|
||||
className={`my-4 pl-6 ${
|
||||
listBlock.data.style === "ordered" ? "list-decimal" : "list-disc"
|
||||
}`}
|
||||
>
|
||||
{listBlock.data.items &&
|
||||
Array.isArray(listBlock.data.items) &&
|
||||
renderListItems(listBlock.data.items)}
|
||||
</ListTag>
|
||||
);
|
||||
case "image":
|
||||
const imageBlock = block as ContentBlock & { data: ImageData };
|
||||
const imageUrl = imageBlock.data.file?.url;
|
||||
|
||||
if (!imageUrl) return null;
|
||||
|
||||
return (
|
||||
<div key={block.id} className="my-6">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageBlock.data.caption || "Blog post image"}
|
||||
width={imageBlock.data.file?.width || 800}
|
||||
height={imageBlock.data.file?.height || 450}
|
||||
className="rounded-md shadow-sm mx-auto"
|
||||
/>
|
||||
{imageBlock.data.caption && (
|
||||
<p className="text-center text-sm text-muted-foreground mt-2">
|
||||
{imageBlock.data.caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case "quote":
|
||||
const quoteBlock = block as ContentBlock & { data: QuoteData };
|
||||
return (
|
||||
<blockquote
|
||||
key={block.id}
|
||||
className="my-6 pl-4 border-l-4 border-primary italic text-muted-foreground"
|
||||
>
|
||||
<p dangerouslySetInnerHTML={{ __html: quoteBlock.data.text }} />
|
||||
{quoteBlock.data.caption && (
|
||||
<footer className="text-sm mt-2">
|
||||
- {quoteBlock.data.caption}
|
||||
</footer>
|
||||
)}
|
||||
</blockquote>
|
||||
);
|
||||
case "code":
|
||||
const codeBlock = block as ContentBlock & { data: CodeData };
|
||||
const codeContent = codeBlock.data.code || "";
|
||||
return (
|
||||
<pre
|
||||
key={block.id}
|
||||
className="my-6 p-4 bg-muted text-muted-foreground rounded-md overflow-x-auto text-sm"
|
||||
>
|
||||
<code>{codeContent}</code>
|
||||
</pre>
|
||||
);
|
||||
default:
|
||||
console.warn(`Unknown block type: ${block.type}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default async function PostPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const post = await getPostBySlug(slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const contentBlocks =
|
||||
post.content &&
|
||||
typeof post.content === "object" &&
|
||||
Array.isArray(post.content.blocks)
|
||||
? (post.content.blocks as ContentBlock[])
|
||||
: null;
|
||||
|
||||
const coverImageUrl =
|
||||
post.featured_image + "?width=1200&height=600&quality=80&fit=cover";
|
||||
|
||||
return (
|
||||
<article className="container mx-auto px-4 py-8">
|
||||
<header className="text-center mb-12 max-w-3xl mx-auto">
|
||||
{coverImageUrl && (
|
||||
<div className="relative w-full h-64 md:h-96 mb-6 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={coverImageUrl}
|
||||
alt={`Cover image for ${post.title}`}
|
||||
fill={true}
|
||||
style={{ objectFit: "cover" }}
|
||||
className="shadow-md"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold leading-tight mb-4 text-primary">
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.date_created && (
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
Published on {format(new Date(post.date_created), "MMMM dd, yyyy")}
|
||||
</p>
|
||||
)}
|
||||
{post.excerpt && (
|
||||
<p className="text-lg text-muted-foreground italic mt-4">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 max-w-3xl">
|
||||
<div
|
||||
className="prose prose-lg lg:prose-xl dark:prose-invert max-w-none mx-auto
|
||||
prose-headings:text-primary prose-a:text-blue-600 hover:prose-a:text-blue-800 dark:prose-a:text-blue-400 dark:hover:prose-a:text-blue-300
|
||||
prose-strong:font-semibold prose-img:rounded-md prose-img:shadow-sm"
|
||||
>
|
||||
{contentBlocks && contentBlocks.length > 0 ? (
|
||||
contentBlocks.map((block) => renderBlock(block))
|
||||
) : (
|
||||
<p>Content is not available or in an unexpected format.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user