ghost integrated
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user