ghost integrated

This commit is contained in:
Mayur Shinde
2026-07-17 18:15:48 +05:30
parent 59e05a858f
commit 09119ae6dc
16 changed files with 1437 additions and 61 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* Ghost Content API client. Runs at build time under Node only — the API key is
* read from the environment here and never reaches the browser bundle.
*
* Direct fetch is used rather than @tryghost/content-api: this site needs one
* read-only endpoint, and the SDK would add a dependency for a single GET.
*/
import fs from "node:fs";
import path from "node:path";
import type { GhostConfig, GhostPost, GhostPostsResponse } from "./types";
/** Only posts carrying this tag are published to this site. */
export const SITE_TAG = "lynkeduppro-new";
const CACHE_DIR = path.resolve("node_modules/.cache/ghost");
const CACHE_FILE = path.join(CACHE_DIR, `posts-${SITE_TAG}.json`);
/**
* Reads Ghost credentials from the environment.
* @throws if either required variable is missing.
*/
export function readGhostConfig(env: Record<string, string | undefined>): GhostConfig {
const apiUrl = env.GHOST_API_URL?.trim();
const contentApiKey = env.GHOST_CONTENT_API_KEY?.trim();
const missing = [
!apiUrl && "GHOST_API_URL",
!contentApiKey && "GHOST_CONTENT_API_KEY",
].filter(Boolean);
if (missing.length) {
throw new Error(
`Missing Ghost environment variable(s): ${missing.join(", ")}. ` +
`Copy .env.example to .env and fill them in (see README of .env.example).`
);
}
return {
apiUrl: apiUrl!.replace(/\/+$/, ""),
contentApiKey: contentApiKey!,
siteUrl: (env.SITE_URL?.trim() || "https://lynkeduppro.com").replace(/\/+$/, ""),
};
}
function readCache(): GhostPost[] | null {
try {
return JSON.parse(fs.readFileSync(CACHE_FILE, "utf8")) as GhostPost[];
} catch {
return null;
}
}
function writeCache(posts: GhostPost[]): void {
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.writeFileSync(CACHE_FILE, JSON.stringify(posts), "utf8");
} catch {
/* Cache is an optimisation; a failure to persist must not fail the build. */
}
}
/**
* Fetches every published post tagged {@link SITE_TAG}, newest first.
*
* Always hits Ghost — one request per dev-server start or build is necessary, not
* wasteful, and serving a TTL-cached copy here would silently hide freshly
* published posts. The on-disk copy is only a fallback for when Ghost is
* unreachable, so a dropped connection doesn't stop local work.
*
* @param allowStaleFallback Fall back to the last good response if the fetch
* fails. Dev only: a production build must fail loudly rather than publish
* stale content.
*/
export async function fetchSitePosts(
config: GhostConfig,
{ allowStaleFallback = false }: { allowStaleFallback?: boolean } = {}
): Promise<GhostPost[]> {
const url = new URL(`${config.apiUrl}/ghost/api/content/posts/`);
url.searchParams.set("key", config.contentApiKey);
url.searchParams.set("filter", `tag:${SITE_TAG}`);
url.searchParams.set("include", "tags,authors");
url.searchParams.set("limit", "all");
url.searchParams.set("order", "published_at DESC");
try {
const res = await fetch(url, { headers: { "Accept-Version": "v5.0" } });
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(
`Ghost API responded ${res.status} ${res.statusText} for ${config.apiUrl}. ${body.slice(0, 300)}`
);
}
const data = (await res.json()) as GhostPostsResponse;
const posts = data.posts ?? [];
writeCache(posts);
return posts;
} catch (error) {
if (!allowStaleFallback) throw error;
const cached = readCache();
if (!cached) throw error;
console.warn(
`[ghost] fetch failed (${error instanceof Error ? error.message : String(error)}).\n` +
`[ghost] Falling back to the last cached response (${cached.length} post(s)) — may be out of date.`
);
return cached;
}
}