Auto-Screenshot Portfolio dengan Puppeteer: Capture Website Tanpa Manual
Tiap nambah project baru di portfolio, harus screenshot manual — buka browser, fullscreen, capture, crop, optimize. Capek. Ini tutorial lengkap cara otomatisasi proses itu dengan Puppeteer, termasuk trick biar hasilnya konsisten.
Masalahnya simpel tapi menyebalkan: setiap kali nambah proyek baru di website portfolio, harus screenshot website secara manual. Buka Chrome, fullscreen, screenshot, crop, optimize, taruh di folder yang benar, update path di database.
Satu proyek mungkin cuma 5 menit. Tapi waktu proyeknya 10, 15, 20 — itu waktu yang bisa dipakai untuk hal lebih berguna.
Gue bakal share script Puppeteer yang gue pakai buat otomatisasi proses ini, termasuk trick-trick yang gue pelajarin dari pengalaman capture puluhan website.
Pendekatan: Build-Time vs Runtime
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
| Screenshot API (thum.io) | Zero setup, langsung jalan | Lambat first load, bergantung service luar |
| Runtime Puppeteer | Fresh screenshot tiap waktu | Berat di server, butuh browser di production |
| Build-time Puppeteer | Static image, loading instan | Butuh run manual, image bisa outdated |
Gue pilih build-time. Loading instan karena gambar static file. Nggak membebani server. Nggak bergantung ke service pihak ketiga.
Struktur File
scripts/ │ └── capture.mjs data/ │ └── database.json public/ │ └── projects/ │ ├── project-a.png │ └── project-b.png package.json
Step 1: Install
npm install --save-dev puppeteerIni download Chromium (~300MB) sebagai dev dependency. Nggak masuk production bundle.
Di WSL2 atau Linux, kadang butuh system dependencies:
sudo apt-get install -y libgbm1 libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0Step 2: Script Capture
// scripts/capture.mjs
import puppeteer from 'puppeteer'
import { readFileSync, existsSync, mkdirSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'const __dirname = dirname(fileURLToPath(import.meta.url)) const root = join(__dirname, '..')
const db = JSON.parse(readFileSync(join(root, 'data/database.json'), 'utf-8')) const outputDir = join(root, 'public/projects')
// Parse --slug argument const slugIdx = process.argv.indexOf('--slug') const filterSlug = slugIdx !== -1 ? process.argv[slugIdx + 1] : undefined
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true })
const projects = db.featuredProjects.filter(p => {
if (filterSlug && p.slug !== filterSlug) return false
if (!p.projectUrl) {
console.log(skip ${p.slug} — no URL)
return false
}
return true
})
if (!projects.length) { console.log('No projects to capture.') process.exit(0) }
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], })
let ok = 0, fail = 0
for (const project of projects) {
const outPath = join(outputDir, ${project.slug}.png)
console.log(capturing ${project.slug}...)
const page = await browser.newPage() await page.setViewport({ width: 1280, height: 800 })
try { await page.goto(project.projectUrl, { waitUntil: 'networkidle2', timeout: 30_000, })
// Tunggu lazy images dan fonts selesai render await new Promise(r => setTimeout(r, 1500))
// Hide scrollbar await page.addStyleTag({ content: '::-webkit-scrollbar { display: none; }' })
await page.screenshot({ path: outPath, type: 'png' })
console.log( saved)
ok++
} catch (err) {
console.error( failed: ${err.message})
fail++
} finally {
await page.close()
}
}
await browser.close()
console.log(Done — ${ok} captured, ${fail} failed.)
```
Detail penting:
waitUntil: 'networkidle2' — tunggu sampai nggak ada request network lebih dari 2 dalam 500ms. Penting buat SPA.
setTimeout 1500ms setelah goto — beri waktu buat lazy images, web fonts, dan animasi CSS. Tanpa ini, screenshot bisa half-rendered.
--disable-dev-shm-usage — critical buat Docker/CI environment yang punya shared memory kecil.
Step 3: Integrasi NPM Scripts
{
"scripts": {
"capture": "node scripts/capture.mjs",
"capture:one": "node scripts/capture.mjs --slug"
}
}npm run capture # semua proyek
npm run capture:one project-slug # satu proyekStep 4: Pakai di Next.js Image
import Image from 'next/image'export function ProjectCard({ project }) {
return (
<Image
src={/projects/${project.slug}.png}
alt={Screenshot ${project.title}}
fill
sizes='(max-width: 768px) 100vw, 33vw'
className='object-cover'
/>
)
}
```
Next.js otomatis optimize — WebP/AVIF, responsive sizing, lazy loading. Tanpa konfigurasi tambahan.
Advanced: Multiple Viewports
Kalau mau capture desktop dan mobile:
const viewports = [
{ name: 'desktop', width: 1280, height: 800 },
{ name: 'mobile', width: 390, height: 844 },
]for (const vp of viewports) {
await page.setViewport({ width: vp.width, height: vp.height })
await page.screenshot({
path: join(outputDir, ${project.slug}-${vp.name}.png),
})
}
```
Tips dari Pengalaman
Cookie banner. Banyak website nunjukin cookie consent yang nutupin konten. Dismiss otomatis:
const btn = await page.$('[class*="accept"], [class*="consent"]')
if (btn) await btn.click()Dark mode. Kalau mau consistent light mode:
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'light' },
])Full page vs viewport. Default cuma capture viewport. Full page:
await page.screenshot({ path: outPath, fullPage: true })Hati-hati — full page untuk website panjang bisa menghasilkan image sangat besar.