HTML Pipeline (generate-static-pages.js)
Understand static HTML page generation at server startup for multiple routes, stream piping, and metadata serialization.
Key File Location: ./dinou/core/generate-static-pages.js💡 Overview
During the production server startup phase, we need to output complete physical HTML pages for all crawled static directories. The generate-static-pages.js module manages this batch execution, piping rendering components to index.html inside dist2/.
📊 Pipeline Flow
The flowchart below shows how routes are processed through the bulk HTML generation pipeline:
🔄 Differences: Bulk vs. Single Page Compilation
Dinou has two modules for rendering HTML: generate-static-pages.js (for batch rendering at startup) and generate-static-page.js (for single pages during active traffic). They operate differently to optimize speed and prevent downtime:
| Feature | Bulk Pipeline (generate-static-pages.js) | Single Page Compiler (generate-static-page.js) |
|---|---|---|
| When does it run? | Runs once in the background when the production server starts up. | Runs on-demand when a user visits a page (ISR / ISG). |
| How does it write files? | Direct Writes: Writes directly to the final index.html file path. Safe because no public traffic is hitting the server yet during startup. | Double-Buffered: Writes to a temporary .tmp file first, then renames it atomically to prevent serving a half-written file to an active user. |
| Status Manifest updates | Updates the in-memory status-manifest.js map at the end of the batch run to synchronize routing states for all compiled paths at once. | Updates only the status map metadata key for the specific path that was revalidated. |
⚙️ Complete Code Walkthrough
Below is the full code of generate-static-pages.js:
// generate-static-pages.js
const path = require("path");
const { mkdirSync, createWriteStream } = require("fs");
const fs = require("fs").promises;
const renderAppToHtml = require("./render-app-to-html.js");
const { getStaticMetadata } = require("./build-static-pages.js");
const { processMetadata } = require("./get-ssg-metadata.js");
const { updateStatus } = require("./status-manifest.js");
const OUT_DIR = path.resolve("dist2");
async function generateStaticPages(routes) {
// 1. Loop through all crawled route paths
for (const route of routes) {
const reqPath = route.endsWith("/") ? route : route + "/";
const htmlPath = path.join(OUT_DIR, reqPath, "index.html");
const query = {};
const paramsString = JSON.stringify(query);
const capturedStatus = {};
const contextForChild = {
req: {
query,
cookies: {},
headers: {
"user-agent": "Dinou-SSG-Builder",
host: "localhost",
"x-forwarded-proto": "http",
},
path: reqPath,
method: "GET",
},
};
try {
mkdirSync(path.dirname(htmlPath), { recursive: true });
const fileStream = createWriteStream(htmlPath);
let htmlStream = null;
// 2. Mock Response fulfilling Server-Side rendering contracts
const mockRes = {
headersSent: true,
_cookies: [],
cookie(name, value, options) {
this._cookies.push({ name, value, options });
},
write: (chunk) => {
if (!fileStream.writableEnded) fileStream.write(chunk);
},
end: (chunk) => {
if (chunk && !fileStream.writableEnded) fileStream.write(chunk);
if (htmlStream) htmlStream.unpipe(fileStream);
if (!fileStream.writableEnded) fileStream.end();
},
status: (code) => {
if (code !== 200) console.warn(`[SSG] Status ${code} ignored for ${reqPath}`);
capturedStatus.value = code;
},
setHeader: () => {},
clearCookie: () => {},
redirect: () => {},
};
// 3. Render Component Tree to HTML readable Stream
htmlStream = renderAppToHtml(
reqPath,
paramsString,
contextForChild,
mockRes,
capturedStatus
);
// 4. Retrieve static compile metadata and extract cookie/redirect side-effects
const metadata = getStaticMetadata(reqPath);
let sideEffectScripts = "";
if (metadata && metadata.effects) {
sideEffectScripts = processMetadata(metadata.effects);
}
// 5. Pipe HTML components directly into output directory
await new Promise((resolve, reject) => {
if (sideEffectScripts) fileStream.write(sideEffectScripts);
htmlStream.pipe(fileStream, { end: false });
htmlStream.on("end", () => {
if (!fileStream.writableEnded) fileStream.end();
updateStatus(reqPath, capturedStatus.value);
resolve();
});
htmlStream.on("error", (err) => {
if (err.code === "ERR_STREAM_WRITE_AFTER_END") resolve();
else reject(err);
});
fileStream.on("error", reject);
});
// 6. Write cache verification metadata file
if (metadata) {
const metadataPath = path.join(OUT_DIR, reqPath, "metadata.json");
await fs.writeFile(
metadataPath,
JSON.stringify({
revalidate: metadata.revalidate,
generatedAt: Date.now(),
effects: metadata.effects,
tags: metadata.tags || [],
}, null, 2),
"utf8"
);
}
console.log("✅ Generated HTML:", reqPath);
} catch (error) {
if (error.code === "ERR_STREAM_WRITE_AFTER_END") {
console.log("⚠️ Ignored write-after-end race condition for:", reqPath);
} else {
console.error("❌ Error rendering:", reqPath);
}
}
}
console.log("🟢 Static page generation complete.");
}
module.exports = generateStaticPages;