fix(ingestion): trigger deterministic Eleventy builds on publish and unpublish
This commit is contained in:
@@ -20,7 +20,7 @@ services:
|
||||
- "WATCH=${WATCH:-false}"
|
||||
volumes:
|
||||
# Mount external markdown posts directly into the container
|
||||
- "./posts:/app/src/posts:ro"
|
||||
- "./posts:/app/src/posts"
|
||||
ports:
|
||||
- "${HOST_PORT:-${PORT:-8080}}:${PORT:-8080}"
|
||||
networks:
|
||||
|
||||
@@ -74,6 +74,49 @@ function slugify(text) {
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
let isBuilding = false;
|
||||
let buildQueued = false;
|
||||
|
||||
/**
|
||||
* Rebuilds the static site using Eleventy.
|
||||
* Uses a build queue to prevent concurrent build races.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function triggerEleventyBuild() {
|
||||
if (isBuilding) {
|
||||
buildQueued = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const root = process.cwd();
|
||||
const hasConfig = fs.existsSync(path.join(root, "eleventy.config.js")) ||
|
||||
fs.existsSync(path.join(root, ".eleventy.js")) ||
|
||||
fs.existsSync(path.join(root, "node_modules/@11ty/eleventy"));
|
||||
|
||||
if (!hasConfig) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
isBuilding = true;
|
||||
return new Promise((resolve) => {
|
||||
exec("npx @11ty/eleventy", (err) => {
|
||||
isBuilding = false;
|
||||
if (err) {
|
||||
console.error("[Ingestion API] Eleventy build error:", err);
|
||||
} else {
|
||||
console.log("[Ingestion API] Eleventy build completed successfully.");
|
||||
}
|
||||
|
||||
if (buildQueued) {
|
||||
buildQueued = false;
|
||||
triggerEleventyBuild().then(resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTTP server instance for serving static files and handling ingestion API
|
||||
* @param {string} [customSiteDir] Optional custom directory for _site
|
||||
@@ -85,8 +128,9 @@ export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
|
||||
const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site"));
|
||||
const postsDir = path.resolve(customPostsDir || path.join(process.cwd(), "src/posts"));
|
||||
const assetsDir = path.resolve(customAssetsDir || path.join(process.cwd(), "src/assets/images/posts"));
|
||||
const isCustomEnv = Boolean(customSiteDir || customPostsDir || customAssetsDir);
|
||||
|
||||
return http.createServer((req, res) => {
|
||||
return http.createServer(async (req, res) => {
|
||||
// Standard security headers
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.setHeader("X-Frame-Options", "DENY");
|
||||
@@ -130,7 +174,7 @@ export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
|
||||
}
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
req.on("end", async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
@@ -182,6 +226,11 @@ export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild static site with Eleventy if in standard environment
|
||||
if (!isCustomEnv) {
|
||||
await triggerEleventyBuild();
|
||||
}
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
status: "published",
|
||||
@@ -228,19 +277,9 @@ export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
|
||||
fs.unlinkSync(compiledHtml);
|
||||
}
|
||||
|
||||
// 3. Rebuild static collections (index.html, dispatches, tags) if in full Eleventy environment
|
||||
try {
|
||||
if (fs.existsSync(path.join(process.cwd(), ".eleventy.js"))) {
|
||||
exec("npx @11ty/eleventy", (err) => {
|
||||
if (err) {
|
||||
console.error("[Ingestion API] Error rebuilding site after delete:", err);
|
||||
} else {
|
||||
console.log(`[Ingestion API] Site rebuild complete after unpublishing '${cleanSlug}'.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (rebuildErr) {
|
||||
console.error("[Ingestion API] Failed to trigger Eleventy rebuild:", rebuildErr);
|
||||
// 3. Rebuild static collections (index.html, dispatches, tags) with Eleventy
|
||||
if (!isCustomEnv) {
|
||||
await triggerEleventyBuild();
|
||||
}
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import http from "node:http";
|
||||
import { createServer } from "../server.js";
|
||||
import { createServer, triggerEleventyBuild } from "../server.js";
|
||||
|
||||
const TEST_SECRET = "super-secret-test-token";
|
||||
|
||||
@@ -274,3 +274,12 @@ test("Ingestion API: adversarial protection against path traversal in slug and D
|
||||
fs.rmSync(tmpPosts, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Ingestion API: triggerEleventyBuild builds site and handles concurrency", async () => {
|
||||
assert.strictEqual(typeof triggerEleventyBuild, "function");
|
||||
// Trigger two concurrent calls to assert queuing does not crash or throw
|
||||
const [b1, b2] = await Promise.all([triggerEleventyBuild(), triggerEleventyBuild()]);
|
||||
assert.strictEqual(b1, undefined);
|
||||
assert.strictEqual(b2, undefined);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user