/* -------------------------------------------------------------------------- Ghost CMS — Content API client -------------------------------------------------------------------------- Talks to a self-hosted or Ghost(Pro) instance via the read-only Content API. Configure with two env vars (see .env.example): GHOST_URL e.g. https://cms.upparivaar.com GHOST_CONTENT_API_KEY the Content API key from Ghost Admin → Integrations When those are missing (or a request fails) we fall back to a small set of built-in demo posts so the /blog section still renders during local dev. -------------------------------------------------------------------------- */ const GHOST_URL = process.env.GHOST_URL?.replace(/\/$/, ""); const GHOST_KEY = process.env.GHOST_CONTENT_API_KEY; const GHOST_API_VERSION = process.env.GHOST_API_VERSION ?? "v5.0"; // Only show posts carrying this tag slug. The shared Ghost instance hosts many // clients' blogs, each tagged separately — this scopes the site to just ours. const GHOST_BLOG_TAG = process.env.GHOST_BLOG_TAG?.trim(); // How long (seconds) Next.js caches Ghost responses before revalidating. const REVALIDATE = Number(process.env.GHOST_REVALIDATE ?? 60); export const ghostConfigured = Boolean(GHOST_URL && GHOST_KEY); export type GhostAuthor = { id: string; name: string; slug: string; profile_image: string | null; bio: string | null; }; export type GhostTag = { id: string; name: string; slug: string; }; export type GhostPost = { id: string; slug: string; title: string; html: string | null; excerpt: string | null; custom_excerpt: string | null; feature_image: string | null; feature_image_alt: string | null; featured: boolean; reading_time: number | null; published_at: string | null; updated_at: string | null; tags?: GhostTag[]; authors?: GhostAuthor[]; primary_author?: GhostAuthor | null; primary_tag?: GhostTag | null; }; const POST_FIELDS = "id,slug,title,html,excerpt,custom_excerpt,feature_image,feature_image_alt,featured,reading_time,published_at,updated_at"; function buildUrl(path: string, params: Record = {}) { const url = new URL(`${GHOST_URL}/ghost/api/content${path}`); url.searchParams.set("key", GHOST_KEY as string); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); return url.toString(); } async function ghostFetch(path: string, params: Record): Promise { if (!ghostConfigured) return null; try { const res = await fetch(buildUrl(path, params), { headers: { "Accept-Version": GHOST_API_VERSION }, next: { revalidate: REVALIDATE, tags: ["ghost-posts"] }, }); if (!res.ok) { console.warn(`[ghost] ${path} responded ${res.status}`); return null; } return (await res.json()) as T; } catch (err) { console.warn("[ghost] request failed:", err); return null; } } // Ghost NQL filter, always published and (when set) scoped to our tag. function scopedFilter(extra?: string): string { const parts = ["status:published"]; if (GHOST_BLOG_TAG) parts.push(`tag:${GHOST_BLOG_TAG}`); if (extra) parts.push(extra); return parts.join("+"); } /** All published posts for our tag, newest first. Falls back to demo posts when unconfigured. */ export async function getPosts(): Promise { const data = await ghostFetch<{ posts: GhostPost[] }>("/posts/", { include: "tags,authors", fields: POST_FIELDS, limit: "all", order: "published_at desc", filter: scopedFilter(), }); return data?.posts ?? DEMO_POSTS; } /** * A single post by slug, with full HTML. The tag scope is enforced here too, so * another client's post can't be opened via a direct /blog/ URL. * Returns null when not found (or not in our tag). */ export async function getPost(slug: string): Promise { const data = await ghostFetch<{ posts: GhostPost[] }>("/posts/", { include: "tags,authors", filter: scopedFilter(`slug:${slug}`), limit: "1", }); if (data?.posts?.[0]) return data.posts[0]; if (!ghostConfigured) return DEMO_POSTS.find((p) => p.slug === slug) ?? null; return null; } /* -------------------------------------------------------------------------- */ /* Helpers */ /* -------------------------------------------------------------------------- */ export function postExcerpt(post: GhostPost, max = 160): string { const text = post.custom_excerpt || post.excerpt || ""; if (text.length <= max) return text; return text.slice(0, max).trimEnd() + "…"; } export function formatDate(iso: string | null): string { if (!iso) return ""; return new Date(iso).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); } export function readingTime(post: GhostPost): string { const mins = post.reading_time ?? 0; return mins > 0 ? `${mins} min read` : ""; } /* -------------------------------------------------------------------------- */ /* Demo fallback content (used only when Ghost isn't configured) */ /* -------------------------------------------------------------------------- */ const demoAuthor: GhostAuthor = { id: "demo-author", name: "UP Parivaar Dallas Team", slug: "team", profile_image: null, bio: "Sharing stories from the community.", }; function demoPost( slug: string, title: string, tag: string, image: string, excerpt: string, body: string, publishedAt: string, reading: number, ): GhostPost { return { id: slug, slug, title, html: body, excerpt, custom_excerpt: excerpt, feature_image: image, feature_image_alt: title, featured: false, reading_time: reading, published_at: publishedAt, updated_at: publishedAt, tags: [{ id: tag, name: tag, slug: tag.toLowerCase() }], authors: [demoAuthor], primary_author: demoAuthor, primary_tag: { id: tag, name: tag, slug: tag.toLowerCase() }, }; } export const DEMO_POSTS: GhostPost[] = [ demoPost( "welcome-to-up-parivaar-dallas", "Welcome to the UP Parivaar Dallas Blog", "Community", "https://images.unsplash.com/photo-1511578314322-379afb476865?w=1200&q=80", "A new home for stories, updates, and reflections from our growing family in the Dallas–Fort Worth metroplex.", `

Welcome to our community blog. This is where we share stories, event recaps, and reflections from the UP Parivaar Dallas family.

Why we started this space

Our community has grown into hundreds of families who care deeply about staying connected to their roots. This blog is a place to celebrate that journey — one story at a time.

Expect posts about our festivals, volunteer initiatives, member spotlights, and the traditions we carry forward for the next generation.

`, "2026-07-10T09:00:00.000Z", 3, ), demoPost( "diwali-mahotsav-recap", "Diwali Mahotsav 2026 — A Festival of Lights & Togetherness", "Events", "https://images.unsplash.com/photo-1605952375858-c5c2e3fd0f45?w=1200&q=80", "Hundreds of families came together for an unforgettable evening of culture, food, and community spirit.", `

This year's Diwali Mahotsav was our largest celebration yet, bringing families from across the metroplex together for an evening of joy and tradition.

Highlights

  • Cultural dance and music performances
  • Traditional food stalls
  • A dedicated kids' zone
  • Community networking

Thank you to every volunteer, performer, and family who made the night special. We can't wait to do it again next year.

`, "2026-06-28T18:00:00.000Z", 4, ), demoPost( "preserving-our-roots", "Preserving Our Roots for the Next Generation", "Culture", "https://images.unsplash.com/photo-1528605248644-14dd04022da1?w=1200&q=80", "How our community keeps the language, values, and traditions of Uttar Pradesh alive far from home.", `

Living thousands of miles from home doesn't mean losing touch with who we are. Across our community, families are finding creative ways to pass their heritage on to children born and raised in Dallas.

Small traditions, big impact

From weekend language circles to festival preparations at home, these shared moments become the memories our children will carry forward.

`, "2026-06-12T10:30:00.000Z", 5, ), ];