Skip to content

Commit 6d22fe4

Browse files
committed
Update with script to capture case studies as PDFs
1 parent 1248bcb commit 6d22fe4

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ yarn-error.log*
4141
.env.development.local
4242
.env.test.local
4343
.env.production.local
44+
45+
*.pdf

bin/exportPage.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env bun
2+
import { chromium } from "playwright";
3+
import { spawn } from "child_process";
4+
import { join } from "path";
5+
6+
const chromeExecutablePath =
7+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; // macOS path
8+
9+
const args = Bun.argv.slice(2);
10+
11+
let url: string | undefined;
12+
let nodeSelector: string | undefined;
13+
let outputFile = "output.pdf";
14+
let margin = "1in"; // default
15+
16+
for (let i = 0; i < args.length; i++) {
17+
if (args[i] === "--node" || args[i] === "-n") {
18+
nodeSelector = args[i + 1];
19+
i++;
20+
} else if (args[i] === "--output" || args[i] === "-o") {
21+
outputFile = args[i + 1];
22+
i++;
23+
} else if (args[i] === "--margin" || args[i] === "-m") {
24+
margin = args[i + 1];
25+
i++;
26+
} else if (!url) {
27+
url = args[i];
28+
}
29+
}
30+
31+
if (!url || !nodeSelector) {
32+
console.error(
33+
"Usage: bun exportPage.ts <url> --node <CSS selector> [--output file.pdf] [--margin 1in]"
34+
);
35+
process.exit(1);
36+
}
37+
38+
const browser = await chromium.launch({
39+
headless: true,
40+
executablePath: chromeExecutablePath,
41+
args: [
42+
"--remote-debugging-port=9222",
43+
"--headless=true",
44+
"--allow-file-access-from-files",
45+
"--autoplay-policy=user-gesture-required",
46+
"--disable-background-networking",
47+
"--disable-background-timer-throttling",
48+
"--disable-backgrounding-occluded-windows",
49+
"--disable-breakpad",
50+
"--disable-client-side-phishing-detection",
51+
"--disable-component-update",
52+
"--disable-default-apps",
53+
"--disable-dev-shm-usage",
54+
"--disable-domain-reliability",
55+
"--disable-extensions",
56+
"--disable-features=AudioServiceOutOfProcess",
57+
"--disable-hang-monitor",
58+
"--disable-ipc-flooding-protection",
59+
"--disable-notifications",
60+
"--disable-offer-store-unmasked-wallet-cards",
61+
"--disable-popup-blocking",
62+
"--disable-print-preview",
63+
"--disable-prompt-on-repost",
64+
"--disable-renderer-backgrounding",
65+
"--disable-setuid-sandbox",
66+
"--disable-speech-api",
67+
"--disable-sync",
68+
"--hide-scrollbars",
69+
"--ignore-gpu-blacklist",
70+
"--metrics-recording-only",
71+
"--mute-audio",
72+
"--no-default-browser-check",
73+
"--no-first-run",
74+
"--no-pings",
75+
"--no-sandbox",
76+
"--no-zygote",
77+
"--password-store=basic",
78+
"--use-gl=swiftshader",
79+
"--use-mock-keychain",
80+
],
81+
});
82+
83+
const page = await browser.newPage();
84+
85+
await page.goto(url, { waitUntil: "networkidle" });
86+
87+
const success = await page.evaluate((selector) => {
88+
const target = document.querySelector(selector);
89+
if (!target) return false;
90+
91+
const clone = target.cloneNode(true) as HTMLElement;
92+
93+
const styles = [...document.styleSheets]
94+
.map((sheet) => {
95+
try {
96+
return [...(sheet.cssRules ?? [])]
97+
.map((rule) => rule.cssText)
98+
.join("\n");
99+
} catch {
100+
return "";
101+
}
102+
})
103+
.join("\n");
104+
105+
document.head.innerHTML = `<style>
106+
${styles}
107+
html, body {
108+
margin: 0 !important;
109+
padding: 0 !important;
110+
border: 0 !important;
111+
}
112+
</style>`;
113+
114+
document.body.innerHTML = "";
115+
document.body.appendChild(clone);
116+
117+
return true;
118+
}, nodeSelector);
119+
120+
if (!success) {
121+
console.error(`Could not find node matching selector: ${nodeSelector}`);
122+
await browser.close();
123+
process.exit(1);
124+
}
125+
126+
await page.pdf({
127+
path: outputFile,
128+
format: "A4",
129+
printBackground: false,
130+
displayHeaderFooter: true,
131+
footerTemplate: `
132+
<div style="
133+
font-size: 11px;
134+
font-family: Arial, Helvetica, sans-serif;
135+
width: 100%;
136+
text-align: center;
137+
color: #ccc;
138+
margin: 0 auto;
139+
">
140+
<span class="pageNumber"></span>/<span class="totalPages"></span>
141+
</div>
142+
`,
143+
headerTemplate: `<div></div>`, // optional: empty header
144+
margin: {
145+
top: margin,
146+
right: margin,
147+
bottom: margin,
148+
left: margin,
149+
},
150+
});
151+
152+
await browser.close();
153+
console.log(`Saved as ${outputFile}`);
154+
155+
// Open PDF in system viewer
156+
function openPDF(path: string) {
157+
const platform = process.platform;
158+
if (platform === "darwin") {
159+
spawn("open", [path], { stdio: "inherit" });
160+
} else if (platform === "win32") {
161+
spawn("start", [path], { shell: true, stdio: "inherit" });
162+
} else {
163+
spawn("xdg-open", [path], { stdio: "inherit" });
164+
}
165+
}
166+
167+
openPDF(join(process.cwd(), outputFile));

bun.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@
4646
"@next/bundle-analyzer": "^15.3.3",
4747
"@svgr/webpack": "^8.1.0",
4848
"@types/axios": "^0.14.4",
49+
"@types/bun": "^1.2.16",
4950
"@types/node": "^22.15.19",
5051
"@types/nprogress": "^0.2.3",
5152
"@types/react": "^19.1.4",
5253
"next-sitemap": "^4.2.3",
54+
"playwright": "^1.53.0",
5355
"svgo": "^3.3.2",
5456
"typescript": "^5.8.3",
5557
"xmldom": "^0.6.0",
@@ -495,6 +497,8 @@
495497

496498
"@types/axios": ["@types/[email protected]", "", { "dependencies": { "axios": "*" } }, "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g=="],
497499

500+
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
501+
498502
"@types/lodash": ["@types/[email protected]", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="],
499503

500504
"@types/lodash-es": ["@types/[email protected]", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
@@ -539,6 +543,8 @@
539543

540544
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
541545

546+
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
547+
542548
"busboy": ["[email protected]", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
543549

544550
"call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -675,6 +681,8 @@
675681

676682
"form-data": ["[email protected]", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
677683

684+
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
685+
678686
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
679687

680688
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
@@ -909,6 +917,10 @@
909917

910918
"picomatch": ["[email protected]", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
911919

920+
"playwright": ["[email protected]", "", { "dependencies": { "playwright-core": "1.53.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q=="],
921+
922+
"playwright-core": ["[email protected]", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw=="],
923+
912924
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
913925

914926
"postcss-media-query-parser": ["[email protected]", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="],

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@
6363
"@next/bundle-analyzer": "^15.3.3",
6464
"@svgr/webpack": "^8.1.0",
6565
"@types/axios": "^0.14.4",
66+
"@types/bun": "^1.2.16",
6667
"@types/node": "^22.15.19",
6768
"@types/nprogress": "^0.2.3",
6869
"@types/react": "^19.1.4",
6970
"next-sitemap": "^4.2.3",
71+
"playwright": "^1.53.0",
7072
"svgo": "^3.3.2",
7173
"typescript": "^5.8.3",
7274
"xmldom": "^0.6.0"

0 commit comments

Comments
 (0)