diff --git a/app/[category]/[slug]/page.tsx b/app/[category]/[slug]/page.tsx new file mode 100644 index 0000000..4d21a25 --- /dev/null +++ b/app/[category]/[slug]/page.tsx @@ -0,0 +1,156 @@ +import { notFound } from 'next/navigation' +import type { Metadata } from 'next' +import Image from 'next/image' +import Link from 'next/link' +import { + getArticleBySlug, + getRelatedArticles, + getAllCategories, + getArticles, + getAssetUrl, +} from '@/lib/directus' +import ArticleBody from '@/components/article/ArticleBody' +import TagRow from '@/components/article/TagRow' +import ArticleGrid from '@/components/home/ArticleGrid' + +interface Props { + params: Promise<{ category: string; slug: string }> +} + +export const revalidate = false + +export async function generateStaticParams() { + try { + const categories = await getAllCategories() + const articlesByCategory = await Promise.all( + categories.map((cat) => getArticles({ categorySlug: cat.slug, limit: 1000 })) + ) + return articlesByCategory.flat().map((article) => ({ + category: article.category.slug, + slug: article.slug, + })) + } catch { + return [] + } +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + try { + const article = await getArticleBySlug(slug) + if (!article) return {} + return { + title: article.seo_title ?? article.title, + description: article.seo_description ?? article.excerpt ?? undefined, + openGraph: { + title: article.seo_title ?? article.title, + description: article.seo_description ?? article.excerpt ?? undefined, + images: article.featured_image + ? [getAssetUrl(article.featured_image, { width: 1200, quality: 80 })] + : [], + }, + } + } catch { + return {} + } +} + +export default async function ArticlePage({ params }: Props) { + const { category: categorySlug, slug } = await params + let article = null + let related: import('@/lib/types').Article[] = [] + + try { + article = await getArticleBySlug(slug) + if (article && article.category.slug === categorySlug) { + related = await getRelatedArticles(categorySlug, slug) + } else { + article = null + } + } catch { + article = null + } + + if (!article) notFound() + + const publishedDate = article.published_at + ? new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }).format(new Date(article.published_at)) + : null + + return ( + <> +
+ + +

+ {article.category.name} +

+

+ {article.title} +

+ {article.excerpt && ( +

+ {article.excerpt} +

+ )} + +
+ {article.is_featured && ( + + Featured + + )} + {publishedDate && ( + {publishedDate} + )} +
+ + {article.featured_image && ( +
+ {article.title} +
+ )} + + {article.content && } + + +
+ + {related.length > 0 && ( +
+ +
+ )} + + ) +}