93 lines
3.4 KiB
TypeScript
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"] },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|