feat: launch public product website
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// Capture the long-form homepage in both system color schemes.
|
||||
//
|
||||
// bun scripts/capture-homepage.mjs [baseUrl] [outputDirectory]
|
||||
//
|
||||
// Requires a running local server and Google Chrome. If SITE_PASSWORD is set,
|
||||
// the script uses the same signed access cookie as the login route.
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const baseUrl = process.argv[2] ?? "http://localhost:3000";
|
||||
const outputDirectory = path.resolve(process.argv[3] ?? "output");
|
||||
const chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||
const password = process.env.SITE_PASSWORD;
|
||||
|
||||
await fs.mkdir(outputDirectory, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: chrome,
|
||||
headless: true,
|
||||
});
|
||||
|
||||
async function capture({ name, width, height, colorScheme }) {
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
||||
await page.emulateMediaFeatures([
|
||||
{ name: "prefers-color-scheme", value: colorScheme },
|
||||
{ name: "prefers-reduced-motion", value: "reduce" },
|
||||
]);
|
||||
|
||||
if (password) {
|
||||
const value = crypto
|
||||
.createHmac("sha256", password)
|
||||
.update("medscribe-site-access-v1")
|
||||
.digest("base64");
|
||||
await browser.setCookie({
|
||||
name: "medscribe_site_access",
|
||||
value,
|
||||
url: baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
await page.goto(baseUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
if (page.url().includes("/login")) {
|
||||
throw new Error("Homepage capture was redirected to /login.");
|
||||
}
|
||||
|
||||
await page.addStyleTag({
|
||||
content:
|
||||
".pitch-lazy{content-visibility:visible!important}*{animation:none!important;transition:none!important}",
|
||||
});
|
||||
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
for (let y = 0; y < document.documentElement.scrollHeight; y += 700) {
|
||||
window.scrollTo(0, y);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => Array.from(document.images).every((image) => image.complete),
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
const layout = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
}));
|
||||
if (layout.scrollWidth > layout.clientWidth) {
|
||||
throw new Error(
|
||||
`${name} has horizontal overflow: ${layout.scrollWidth}px > ${layout.clientWidth}px`,
|
||||
);
|
||||
}
|
||||
|
||||
const destination = path.join(outputDirectory, `${name}.png`);
|
||||
await page.screenshot({ path: destination, fullPage: true });
|
||||
console.log(`${name}: ${layout.clientWidth} × ${layout.scrollHeight}`);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const colorScheme of ["light", "dark"]) {
|
||||
await capture({
|
||||
name: `homepage-${colorScheme}`,
|
||||
width: 1440,
|
||||
height: 1000,
|
||||
colorScheme,
|
||||
});
|
||||
await capture({
|
||||
name: `homepage-${colorScheme}-mobile`,
|
||||
width: 390,
|
||||
height: 844,
|
||||
colorScheme,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Render a marketing route to a print-styled PDF, the same way the browser's
|
||||
// "Print PDF" button does (print CSS + designed print-only components).
|
||||
//
|
||||
// bun scripts/render-pdf.mjs <route> <output.pdf> [baseUrl]
|
||||
// bun scripts/render-pdf.mjs /cvs ../docs/business/CVS_SUMMARY.pdf
|
||||
//
|
||||
// Self-authenticates past the SITE_PASSWORD gate by computing the same signed
|
||||
// access cookie the login route issues (see src/lib/site-auth.ts). Requires a
|
||||
// running dev/preview server and a local Google Chrome.
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const route = process.argv[2] ?? "/cvs";
|
||||
const outArg = process.argv[3] ?? "../docs/business/CVS_SUMMARY.pdf";
|
||||
const baseUrl = process.argv[4] ?? "http://localhost:3000";
|
||||
const outPath = path.resolve(__dirname, "..", outArg);
|
||||
|
||||
const CHROME =
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||
const AUTH_COOKIE_NAME = "medscribe_site_access";
|
||||
const ACCESS_PAYLOAD = "medscribe-site-access-v1";
|
||||
|
||||
const password = process.env.SITE_PASSWORD;
|
||||
const cookies = [];
|
||||
if (password) {
|
||||
const token = crypto
|
||||
.createHmac("sha256", password)
|
||||
.update(ACCESS_PAYLOAD)
|
||||
.digest("base64");
|
||||
cookies.push({
|
||||
name: AUTH_COOKIE_NAME,
|
||||
value: token,
|
||||
url: baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME,
|
||||
headless: true,
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
if (cookies.length) await browser.setCookie(...cookies);
|
||||
const res = await page.goto(`${baseUrl}${route}`, {
|
||||
waitUntil: "networkidle0",
|
||||
timeout: 60_000,
|
||||
});
|
||||
const finalUrl = page.url();
|
||||
if (finalUrl.includes("/login")) {
|
||||
throw new Error(
|
||||
`Redirected to login (${res?.status()}). Check SITE_PASSWORD is set in the env used to run this script.`,
|
||||
);
|
||||
}
|
||||
await page.pdf({
|
||||
path: outPath,
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
});
|
||||
console.log(`Wrote ${outPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user