// Pure helpers for baking Open Graph / Twitter Card tags into prerendered HTML.
// Kept separate from vite.config so the logic is unit-testable without a full build.
// Used by the `prerender-og` Vite plugin (see vite.config.ts).
import fs from "node:fs/promises";
import path from "node:path";
export const escapeHtml = (value) =>
String(value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
const absolute = (urlPath, ogBase) => (ogBase ? ogBase + urlPath : urlPath);
/**
* Build the OG/Twitter block for one route.
* @param {{image:string,title:string,description:string}} entry
* @param {{ogBase?:string, pageUrlPath?:string|null}} opts
*/
export function buildOgTags(entry, { ogBase = "", pageUrlPath = null } = {}) {
const title = escapeHtml(entry.title);
const description = escapeHtml(entry.description);
const imageUrl = absolute(entry.image, ogBase);
const image = escapeHtml(imageUrl);
const pageUrl = pageUrlPath
? escapeHtml(absolute(pageUrlPath, ogBase))
: null;
const lines = [
"",
'',
'',
``,
``,
pageUrl ? `` : null,
``,
imageUrl.startsWith("https")
? ``
: null,
'',
'',
'',
'',
``,
``,
``,
"",
].filter(Boolean);
return lines.join("\n ") + "\n ";
}
/** Inject route-specific
, description and OG/Twitter tags into an HTML shell. */
export function injectOg(html, entry, opts = {}) {
return html
.replace(
/[\s\S]*?<\/title>/i,
() => `${escapeHtml(entry.title)}`,
)
.replace(
//i,
() =>
``,
)
.replace("", ` ${buildOgTags(entry, opts)}`);
}
const BASE_HREF_RE = //i;
/**
* Write the root index.html (home preview) plus one file per route in the
* manifest: flat for single-segment routes (e.g. dist/compress.html) and nested
* for multi-segment ones (e.g. dist/settings/people.html). Returns the count of
* route pages written.
*
* `baseHref` is the absolute deploy base ("/" for a root deploy). Nested files
* need it because a relative `` would resolve their assets
* against the sub-path (e.g. /settings/) and 404; flat files and the root keep
* the build's relative base.
* @returns {Promise}
*/
export async function prerenderOg({
distDir,
manifest,
ogBase = "",
baseHref = "/",
}) {
const template = await fs.readFile(path.join(distDir, "index.html"), "utf8");
await fs.writeFile(
path.join(distDir, "index.html"),
injectOg(template, manifest.default, {
ogBase,
pageUrlPath: ogBase ? "/" : null,
}),
);
let count = 0;
for (const [routePath, id] of Object.entries(manifest.byPath || {})) {
const segments = routePath.replace(/^\//, "").split("/");
// Clean segments only - no traversal, no dots (e.g. /compress, /settings/people).
if (!segments.length || !segments.every((s) => /^[A-Za-z0-9_-]+$/.test(s)))
continue;
const entry = manifest.byTool[id] ?? manifest.default;
let html = injectOg(template, entry, {
ogBase,
pageUrlPath: ogBase ? routePath : null,
});
const nested = segments.length > 1;
if (nested)
html = html.replace(BASE_HREF_RE, ``);
const outFile = path.join(distDir, ...segments) + ".html";
if (nested) await fs.mkdir(path.dirname(outFile), { recursive: true });
await fs.writeFile(outFile, html);
count++;
}
return count;
}