fix(ingestion): trigger deterministic Eleventy builds on publish and unpublish

This commit is contained in:
venus
2026-09-14 01:24:07 -05:00
parent 417ec30bfd
commit 22f78da4cb
3 changed files with 65 additions and 17 deletions
+1 -1
View File
@@ -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:
+54 -15
View File
@@ -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" });
+10 -1
View File
@@ -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);
});