ghost integrated
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user