// 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/ by basename to an asset. fs.writeFileSync(path.join(OUT, "styles.css"), css); // 4) rewrite rendered.html ----------------------------------------------- let html = fs.readFileSync(path.join(RIP, "rendered.html"), "utf8"); // strip all `; // Mobile nav: the live site's hamburger uses the custom "TOG" toggle framework, // whose JS we strip — so on mobile the menu can't open. Inject a clean, on-brand, // accessible overlay (real nav destinations) and wire it to the hamburger. const mobileNav = ` `; // Smooth scroll (Lenis) + eased anchor scrolling + soft scroll reveals. // Reveals are visible-by-default (only hidden via JS, then animated in) so the // page never ships blank without JS, and they honor prefers-reduced-motion. const motionScript = ` `; // Perf: load + play each background video only when it nears the viewport, and // pause it when it leaves. Cuts initial load from ~3.8 MB to ~the hero video. const lazyVideoScript = ` `; html = html.replace(/<\/body>/i, `${lensScript}${mobileNav}${motionScript}${lazyVideoScript}`); // 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 = `${TITLE}` + `` + `` + `` + `` + `` + `` + `` + `` + `` + ``; // normalize the viewport meta: keep zoom enabled (a11y), add notch-safe areas doc = doc.replace( /]*name=["']viewport["'][^>]*>/i, '', ); // remove any pre-existing (usually empty) and manifest link, then inject ours doc = doc.replace(/<title>[\s\S]*?<\/title>/i, ""); doc = doc.replace(/<link\b[^>]*rel=["']?manifest["']?[^>]*>/gi, ""); doc = doc.replace(/<\/head>/i, `${headMeta}</head>`); // --- skip link + visually-hidden H1 right after <body ...> --- const a11yTop = `<a class="skip-link" href="#main-content">Skip to content</a>` + `<h1 class="vh">Bigscreen Beyond 2, the world's smallest VR headset</h1>`; doc = doc.replace(/(<body\b[^>]*>)/i, `$1${a11yTop}`); // --- <main> landmark wrapping the content (between header and footer) --- let wrappedMain = false; doc = doc.replace(/<section\b/i, (m) => { wrappedMain = true; return `<main id="main-content">${m}`; }); if (wrappedMain) { doc = doc.replace(/(<footer\b)/i, `</main>$1`); } // --- default alt="" on decorative images that lack alt (stops screen // readers announcing filenames; descriptive alt comes with components) --- doc = doc.replace(/<img\b(?![^>]*\salt=)([^>]*?)(\/?)>/gi, '<img alt=""$1$2>'); return doc; } // 5) report --------------------------------------------------------------- const remainingRemote = (html.match(/https?:\/\/cdn\.(?:builder|shopify)\.[a-z]+/gi) || []).length; console.log(`mirror built → ${OUT}/`); console.log(` assets copied: ${copied}`); console.log(` manifest mappings: ${Object.keys(manifest).length}`); console.log(` html bytes: ${html.length} | css bytes: ${css.length}`); console.log(` remaining remote cdn refs in html: ${remainingRemote}`); function replaceFooter(doc) { const STORE = "https://store.bigscreenvr.com"; const cols = [ { title: "Resources", links: [ ["Remote Desktop Client", "/remotedesktop"], ["Beyond Setup Guide", "/mybeyond"], ["Audio Strap Setup Guide", "/myaudiostrap"], ["Hardware Warranty & Terms", "/hardwareterms"], ], }, { title: "Learn more", links: [ ["Blog", `${STORE}/blogs/beyond`], ["Contact", "mailto:support@bigscreenvr.com"], ["Careers", "/about#openings"], ["Affiliate", "/affiliate"], ["Displays", "/displays"], ], }, { title: "About", links: [ ["Company", "/about#story"], ["Press", "/about#press"], ], }, { title: "Follow us", links: [ ["X", "https://x.com/bigscreenvr"], ["YouTube", "https://youtube.com/bigscreenvr"], ["Reddit", "https://www.reddit.com/r/bigscreenbeyond"], ["Facebook", "https://www.facebook.com/bigscreenvr"], ], }, ]; const ext = (h) => /^https?:|^mailto:/.test(h); const linkList = (links) => links .map( ([t, h]) => `<li><a href="${h}"${ext(h) ? ' target="_blank" rel="noreferrer"' : ""}>${t}</a></li>`, ) .join(""); const colHtml = (c) => `<div class="bs-foot-col"><h2 class="bs-foot-head">${c.title}</h2><ul>${linkList(c.links)}</ul></div>`; const discordIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.3 4.4A19.8 19.8 0 0 0 15.4 3l-.25.5a18 18 0 0 1 4.3 1.4 13 13 0 0 0-15-.0 18 18 0 0 1 4.3-1.4L8.6 3A19.8 19.8 0 0 0 3.7 4.4 20.6 20.6 0 0 0 .2 18.6a20 20 0 0 0 6 3l.8-1.3a13 13 0 0 1-2-1l.5-.4a14.3 14.3 0 0 0 12.2 0l.5.4a13 13 0 0 1-2 1l.8 1.3a20 20 0 0 0 6-3 20.6 20.6 0 0 0-3.5-14.2ZM8.3 15.3c-1.2 0-2.1-1.1-2.1-2.4s.9-2.4 2.1-2.4 2.1 1.1 2.1 2.4-.9 2.4-2.1 2.4Zm7.4 0c-1.2 0-2.1-1.1-2.1-2.4s.9-2.4 2.1-2.4 2.1 1.1 2.1 2.4-.9 2.4-2.1 2.4Z"/></svg>'; const footer = `<footer class="bs-footer" data-reveal-skip> <div class="bs-footer-inner"> <div class="bs-foot-brand"> <p class="bs-foot-corp">Bigscreen, Inc.</p> <nav class="bs-foot-primary" aria-label="Footer primary"> <a href="/">Hardware</a> <a href="/experiences">Experiences</a> <a href="/about">About Us</a> <a href="${STORE}" target="_blank" rel="noreferrer">Store</a> </nav> <a class="bs-foot-discord" href="https://discord.gg/bigscreen" target="_blank" rel="noreferrer">${discordIcon}<span>Join us on Discord</span></a> </div> <div class="bs-foot-col bs-foot-resources"> <h2 class="bs-foot-head">Resources</h2> <ul>${linkList(cols[0].links)}</ul> <h2 class="bs-foot-head bs-foot-news-head">Newsletter</h2> <form class="bs-foot-news" action="${STORE}" method="get" target="_blank" rel="noreferrer"> <label class="vh" for="bs-foot-email">Email address</label> <input id="bs-foot-email" name="email" type="email" placeholder="Your email" autocomplete="email" /> <button type="submit" aria-label="Subscribe">→</button> </form> </div> ${colHtml(cols[1])} ${colHtml(cols[2])} ${colHtml(cols[3])} </div> <div class="bs-footer-legal"> <p>Copyright © 2025 Bigscreen, Inc. All rights reserved</p> <nav aria-label="Legal"><a href="/terms">Terms of Service</a><a href="/privacy">Privacy Policy</a></nav> </div> </footer>`; const style = `<style id="bs-footer-style"> .bs-footer { background: #fff; color: #0a0a0c; font-family: "Neue Haas Grotesk Display Pro - 55 Roman", "Zen Kaku Gothic New - Regular", sans-serif; padding: clamp(56px, 7vw, 96px) clamp(20px, 5vw, 56px) 0; position: relative; z-index: 10; } .bs-footer-inner { max-width: 1648px; margin: 0 auto; display: grid; grid-template-columns: 1.5fr 1.2fr 1fr 1fr 1fr; gap: 40px 32px; } .bs-foot-corp { color: #646A6E; font-size: 15px; margin: 0 0 24px; } .bs-foot-primary { display: flex; flex-direction: column; gap: 2px; } .bs-foot-primary a { font-family: "Neue Haas Grotesk Display Pro - 55 Roman", "Zen Kaku Gothic New - Regular", sans-serif; font-size: clamp(28px, 3vw, 40px); font-weight: 500; line-height: 1.18; color: #0a0a0c; text-decoration: none; letter-spacing: -0.01em; transition: color .18s cubic-bezier(.22,1,.36,1); } .bs-foot-primary a:hover { color: #4D1EF7; } .bs-foot-discord { display: inline-flex; align-items: center; gap: 10px; margin-top: 28px; height: 46px; padding: 0 22px; border-radius: 40px; background: #000; color: #fff; font-size: 14px; font-weight: 500; text-decoration: none; width: fit-content; transition: transform .18s cubic-bezier(.22,1,.36,1); } .bs-foot-discord:hover { transform: translateY(-1px); } .bs-foot-head { color: #646A6E; font-size: 15px; font-weight: 400; margin: 0 0 18px; } .bs-foot-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; } .bs-foot-col a { color: #0a0a0c; font-size: 15px; text-decoration: none; transition: color .18s cubic-bezier(.22,1,.36,1); } .bs-foot-col a:hover { color: #4D1EF7; } .bs-foot-news-head { margin-top: 40px; } .bs-foot-news { display: flex; align-items: center; gap: 8px; max-width: 240px; border-bottom: 1px solid #DFDFDF; padding-bottom: 8px; } .bs-foot-news input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: "Neue Haas Grotesk Display Pro - 55 Roman", "Zen Kaku Gothic New - Regular", sans-serif; font-size: 15px; color: #0a0a0c; } .bs-foot-news input::placeholder { color: #646A6E; } .bs-foot-news button { border: none; background: transparent; color: #0a0a0c; font-size: 18px; cursor: pointer; line-height: 1; padding: 2px 4px; } .bs-foot-news button:focus-visible, .bs-footer a:focus-visible, .bs-foot-news input:focus-visible { outline: 2px solid #4D1EF7; outline-offset: 3px; border-radius: 2px; } .bs-footer-legal { max-width: 1648px; margin: clamp(48px, 6vw, 80px) auto 0; padding: 24px 0 32px; border-top: 1px solid #DFDFDF; display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; color: #646A6E; font-size: 13px; } .bs-footer-legal nav { display: flex; gap: 32px; } .bs-footer-legal a { color: #646A6E; text-decoration: none; transition: color .18s; } .bs-footer-legal a:hover { color: #0a0a0c; } @media (max-width: 991px) { .bs-footer-inner { grid-template-columns: 1fr 1fr; gap: 40px 24px; } .bs-foot-brand { grid-column: 1 / -1; } } @media (max-width: 560px) { .bs-footer-inner { grid-template-columns: 1fr; } .bs-footer-legal { flex-direction: column; align-items: flex-start; } } </style>`; // Replace the rip footer (first <footer>…</footer>) with the clean one. if (/<footer\b[\s\S]*?<\/footer>/i.test(doc)) { return doc.replace(/<footer\b[\s\S]*?<\/footer>/i, style + footer); } // Fallback: append before </body> if no footer matched. return doc.replace(/<\/body>/i, `${style}${footer}</body>`); }