Add Ghost CMS blog section (list + article pages)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { getPost, getPosts, postExcerpt, formatDate, readingTime } from "@/lib/ghost";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
// Pre-render every known post at build time; new ones are handled on demand.
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts();
|
||||
return posts.map((p) => ({ slug: p.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
if (!post) return { title: "Post not found — UP Parivaar Dallas" };
|
||||
return {
|
||||
title: `${post.title} — UP Parivaar Dallas`,
|
||||
description: postExcerpt(post, 160),
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: postExcerpt(post, 160),
|
||||
type: "article",
|
||||
images: post.feature_image ? [{ url: post.feature_image }] : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
if (!post) notFound();
|
||||
|
||||
const author = post.primary_author;
|
||||
|
||||
return (
|
||||
<article className="page-glow blogpost">
|
||||
<div className="container blogpost__inner">
|
||||
<Link href="/blog" className="blogpost__back">
|
||||
← All posts
|
||||
</Link>
|
||||
|
||||
<div className="blogpost__head">
|
||||
{post.primary_tag && <span className="blog-tag">{post.primary_tag.name}</span>}
|
||||
<h1 className="display-title ts-34-44 blogpost__title">{post.title}</h1>
|
||||
|
||||
<div className="blogpost__byline">
|
||||
{author?.profile_image && (
|
||||
<span className="blogpost__avatar">
|
||||
<Image src={author.profile_image} alt={author.name} fill className="img-cover" sizes="44px" />
|
||||
</span>
|
||||
)}
|
||||
<div className="blogpost__byline-text">
|
||||
{author && <span className="blogpost__author">{author.name}</span>}
|
||||
<span className="blog-meta">
|
||||
<span>{formatDate(post.published_at)}</span>
|
||||
{readingTime(post) && <span className="blog-meta__dot">•</span>}
|
||||
{readingTime(post) && <span>{readingTime(post)}</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post.feature_image && (
|
||||
<div className="blogpost__hero">
|
||||
<Image
|
||||
src={post.feature_image}
|
||||
alt={post.feature_image_alt || post.title}
|
||||
fill
|
||||
className="img-cover"
|
||||
sizes="(max-width: 900px) 100vw, 900px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.html ? (
|
||||
<div
|
||||
className="blogpost__content"
|
||||
dangerouslySetInnerHTML={{ __html: post.html }}
|
||||
/>
|
||||
) : (
|
||||
<p className="blogpost__content">{postExcerpt(post, 500)}</p>
|
||||
)}
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="blogpost__tags">
|
||||
{post.tags.map((t) => (
|
||||
<span key={t.id} className="blog-tag blog-tag--muted">
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="blogpost__foot">
|
||||
<Link href="/blog" className="link-arrow link-arrow--spaced-lg">
|
||||
Back to all posts
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getPosts } from "@/lib/ghost";
|
||||
import { BlogCard, FeaturedBlogCard } from "@/components/BlogCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "Blog — UP Parivaar Dallas",
|
||||
description:
|
||||
"Stories, event recaps, and reflections from the UP Parivaar Dallas community.",
|
||||
};
|
||||
|
||||
// Revalidation is driven by the Ghost fetch layer (GHOST_REVALIDATE).
|
||||
export const revalidate = 60;
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getPosts();
|
||||
const [featured, ...rest] = posts;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-glow page-hero--tight">
|
||||
<div className="container hero-center">
|
||||
<span className="eyebrow">Blog</span>
|
||||
<h1 className="display-title ts-36-48 events-hero__title">
|
||||
Stories from our community
|
||||
</h1>
|
||||
<p className="events-hero__lead">
|
||||
Event recaps, member spotlights, and reflections on the traditions we
|
||||
carry forward together.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section-white">
|
||||
<div className="container blog-list">
|
||||
{posts.length === 0 ? (
|
||||
<p className="blog-empty">No posts published yet. Check back soon.</p>
|
||||
) : (
|
||||
<>
|
||||
{featured && <FeaturedBlogCard post={featured} />}
|
||||
{rest.length > 0 && (
|
||||
<div className="blog-grid">
|
||||
{rest.map((post) => (
|
||||
<BlogCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user