// Build a faithful, self-contained static mirror of the live site from
// landing-rip/ (rendered.html + styles.css + assets + manifest.json).
// Output: public/landing/{index.html, styles.css, assets/*} — an exact 1:1 copy
// with all URLs localized and tracking/ad scripts stripped.
import fs from "node:fs";
import path from "node:path";
const RIP = "landing-rip";
const OUT = "public/landing";
const manifest = JSON.parse(fs.readFileSync(path.join(RIP, "manifest.json"), "utf8"));
// 1) copy assets ----------------------------------------------------------
fs.mkdirSync(path.join(OUT, "assets"), { recursive: true });
let copied = 0;
for (const f of fs.readdirSync(path.join(RIP, "assets"))) {
const src = path.join(RIP, "assets", f);
if (fs.statSync(src).isDirectory()) continue; // handle the optics sequence separately
fs.copyFileSync(src, path.join(OUT, "assets", f));
copied++;
}
// 1b) copy the lens-reveal frame sequence (#scrollanim canvas) -------------
const OPTICS_SRC = path.join(RIP, "assets", "optics-webp");
const OPTICS_DST = path.join(OUT, "optics");
let opticsFrames = 0;
if (fs.existsSync(OPTICS_SRC)) {
fs.mkdirSync(OPTICS_DST, { recursive: true });
for (const f of fs.readdirSync(OPTICS_SRC)) {
fs.copyFileSync(path.join(OPTICS_SRC, f), path.join(OPTICS_DST, f));
opticsFrames++;
}
}
// 1c) Lenis (smooth scroll) UMD bundle for the static mirror ---------------
try {
fs.copyFileSync("node_modules/lenis/dist/lenis.min.js", path.join(OUT, "lenis.min.js"));
} catch {}
// 2) build URL -> local map from manifest --------------------------------
// Map the full URL, the URL without query, the pathname, and the basename so we
// catch absolute, root-relative, and srcset references.
const map = new Map();
function addMapping(url, local) {
if (!url) return;
const noQuery = url.split("?")[0];
map.set(url, local);
map.set(noQuery, local);
try {
const u = new URL(url);
map.set(u.pathname, local);
map.set(u.pathname + u.search, local);
} catch {}
}
for (const [url, info] of Object.entries(manifest)) {
if (!info?.file) continue;
addMapping(url, "/landing/" + info.file); // info.file like "assets/008-asset.webp"
}
// Replace any known URL occurrences in a text blob. Longest keys first so a
// query-string URL isn't partially clobbered by its no-query variant.
const keys = [...map.keys()].sort((a, b) => b.length - a.length);
function localizeUrls(text) {
let out = text;
for (const k of keys) {
if (!k || k.length < 6) continue;
out = out.split(k).join(map.get(k));
}
return out;
}
// 3) rewrite styles.css ---------------------------------------------------
let css = fs.readFileSync(path.join(RIP, "styles.css"), "utf8");
css = localizeUrls(css);
// Best-effort: map any remaining /static/media/
`); // 4a2) replace the broken footer with clean, semantic, on-brand markup ---- // The rip's footer layout depends on stripped JS (columns scatter, near-white // text on white). Swap it for a correct white footer matching the live design. html = replaceFooter(html); // 4b) accessibility / production-readiness harden ------------------------- // Durable wins that don't fight the rip's structure. Full per-image descriptive // alt and a complete heading hierarchy land during componentization. html = hardenDocument(html); fs.writeFileSync(path.join(OUT, "index.html"), html); function hardenDocument(doc) { const TITLE = "Bigscreen Beyond 2 | The world's smallest VR headset"; const DESC = "Bigscreen Beyond 2 is the world's smallest VR headset, with advanced pancake optics, dual micro-OLED displays, adjustable IPD, and optional eyetracking."; const OG_IMAGE = "/landing/assets/008-asset.webp"; // ---
: title, description, social, icons, manifest --- const headMeta = `
` + `` + `` + `` + `` + `` + `` + `` + `` + `` + ``; // normalize the viewport meta: keep zoom enabled (a11y), add notch-safe areas doc = doc.replace( /]*name=["']viewport["'][^>]*>/i, '', ); // remove any pre-existing (usually empty)
`); }