Files
2026-07-17 18:15:48 +05:30

93 lines
3.4 KiB
TypeScript

import { defineConfig, loadEnv, type Plugin } from "vite";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import path from "node:path";
import { buildGhostBlog } from "./src/ghost/build";
import { renderEmptyState } from "./src/ghost/render";
const root = path.dirname(fileURLToPath(import.meta.url));
/** Marker in blog/index.html where Ghost cards are injected. */
const CARDS_MARKER = "<!-- GHOST_CARDS -->";
/**
* Every blog/<slug>/index.html — the hand-authored posts plus the pages just
* generated from Ghost. Globbed rather than listed so new posts need no config
* change.
*/
function blogPageInputs(): Record<string, string> {
const blogDir = path.resolve(root, "blog");
const inputs: Record<string, string> = {};
for (const entry of fs.readdirSync(blogDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const file = path.join(blogDir, entry.name, "index.html");
if (fs.existsSync(file)) inputs[`blog-${entry.name}`] = file;
}
return inputs;
}
/** Injects the rendered Ghost cards into the blog listing grid. */
function ghostListingPlugin(cardsHtml: string): Plugin {
return {
name: "ghost-listing",
transformIndexHtml: {
order: "pre",
handler(html, ctx) {
if (!ctx.filename.replace(/\\/g, "/").endsWith("/blog/index.html")) return html;
if (!html.includes(CARDS_MARKER)) {
console.warn(`[ghost] ${CARDS_MARKER} not found in blog/index.html; cards not injected.`);
return html;
}
// Empty when Ghost has no tagged posts, or (in dev) when it was
// unreachable. The grid has no static cards behind it, so show the
// empty state rather than a blank gap.
return html.replace(CARDS_MARKER, cardsHtml || renderEmptyState());
},
},
};
}
export default defineConfig(async ({ mode, command }) => {
// Third arg "" loads unprefixed vars too: these are read here in Node at build
// time and never inlined into the client bundle, so the Ghost key stays server-side.
const env = { ...process.env, ...loadEnv(mode, root, "") };
let cardsHtml = "";
try {
// Always fetch fresh, so a newly published post shows up on the next dev
// restart or deploy. In dev only, fall back to the last cached response if
// Ghost is unreachable.
({ cardsHtml } = await buildGhostBlog(env, { allowStaleFallback: command === "serve" }));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (command === "build") {
// Fail the build rather than publish a blog with posts silently missing.
// On Vercel a failed build leaves the previous deployment serving.
throw new Error(`[ghost] blog generation failed: ${message}`);
}
// In dev, keep the site usable — the hand-authored posts still render.
console.warn(`[ghost] ${message}\n[ghost] Continuing without Ghost posts (dev only).`);
}
return {
base: "./",
server: { port: 4190 },
plugins: [ghostListingPlugin(cardsHtml)],
build: {
rollupOptions: {
input: {
main: path.resolve(root, "index.html"),
faq: path.resolve(root, "faq.html"),
blog: path.resolve(root, "blog/index.html"),
terms: path.resolve(root, "terms/index.html"),
...blogPageInputs(),
},
output: {
manualChunks: { three: ["three"], gsap: ["gsap"] },
},
},
},
};
});