From 52a8e486b9e89649bd272052c2de398fc1a64105 Mon Sep 17 00:00:00 2001 From: Goutam Date: Thu, 23 Jul 2026 21:09:22 +0530 Subject: [PATCH] Scope blog to UP Parivaar tag via GHOST_BLOG_TAG; allow http Ghost images --- .env.example | 3 +++ lib/ghost.ts | 29 ++++++++++++++++++++++++----- next.config.mjs | 8 +++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 30834e7..90cf2da 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ GHOST_CONTENT_API_KEY=your_content_api_key # Admin API key (id:secret) — only needed for server-side write tooling, not the blog. GHOST_ADMIN_API_KEY= +# Only show posts tagged with this slug. Leave empty to show all posts. +GHOST_BLOG_TAG=upparivaar + # Optional GHOST_API_VERSION=v5.0 GHOST_REVALIDATE=60 diff --git a/lib/ghost.ts b/lib/ghost.ts index 224ce2d..05a38d4 100644 --- a/lib/ghost.ts +++ b/lib/ghost.ts @@ -15,6 +15,10 @@ 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); @@ -81,25 +85,40 @@ async function ghostFetch(path: string, params: Record): Prom } } -/** All published posts, newest first. Falls back to demo posts when unconfigured. */ +// 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: "status:published", + filter: scopedFilter(), }); return data?.posts ?? DEMO_POSTS; } -/** A single post by slug, with full HTML. Returns null when not found. */ +/** + * 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/slug/${slug}/`, { + const data = await ghostFetch<{ posts: GhostPost[] }>("/posts/", { include: "tags,authors", + filter: scopedFilter(`slug:${slug}`), + limit: "1", }); if (data?.posts?.[0]) return data.posts[0]; - return DEMO_POSTS.find((p) => p.slug === slug) ?? null; + if (!ghostConfigured) return DEMO_POSTS.find((p) => p.slug === slug) ?? null; + return null; } /* -------------------------------------------------------------------------- */ diff --git a/next.config.mjs b/next.config.mjs index 0565e15..3acb057 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -13,7 +13,13 @@ const nextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "images.unsplash.com" }, - ...(ghostHost ? [{ protocol: "https", hostname: ghostHost }] : []), + // Ghost may serve feature images over either http or https, so allow both. + ...(ghostHost + ? [ + { protocol: "https", hostname: ghostHost }, + { protocol: "http", hostname: ghostHost }, + ] + : []), ], }, };