Vue.js Screenshot API: Capture Pages from Your Nuxt and Vue Backend
Nuxt and Vue developers face a familiar problem: your fullstack app needs to capture web pages as screenshots or PDFs, but you don't want to manage Puppeteer, Chromium, or wkhtmltopdf on your server.
This is exactly the problem PageBolt solves. Instead of spinning up a headless browser or managing complex server dependencies, you just call a REST API from your Nuxt server route or Vue backend. Your screenshot is ready in under a second.
The Problem: Why Puppeteer Doesn't Work for Nuxt
If you've tried Puppeteer in Nuxt, you know the friction:
- Bloated dependencies: Puppeteer adds 200MB+ to your server. If you're deploying to Vercel or Netlify, that's a cold start penalty
- Memory hungry: Each screenshot spins up a browser process. Your serverless function hits memory limits fast
- Breaks serverless: Vercel Edge Functions, Netlify Functions, and AWS Lambda can't run Puppeteer. You need a separate server just for screenshots
- Maintenance burden: Puppeteer version conflicts with Node.js versions, browser crashes in production, timeouts eating your CPU quota
- Not aligned with Nuxt philosophy: Nuxt is about simple, elegant fullstack development. Puppeteer adds complexity that breaks that model
PageBolt removes all of this. Your Nuxt server route just makes one API call. No dependencies. Works everywhere — from local dev to serverless.
The Solution: REST API That Works With Nuxt Server Routes
Here's the whole idea in one example:
// server/api/capture-screenshot.post.ts
export default defineEventHandler(async (event) => {
const { url } = await readBody(event)
const response = await $fetch('https://api.pagebolt.dev/screenshot', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAGEBOLT_API_KEY}`
},
body: {
url,
format: 'png',
width: 1280,
height: 720,
fullPage: true
}
})
return {
status: 'success',
imageData: Buffer.from(response).toString('base64')
}
})
Done. One API call. No browsers. No Puppeteer. Your Nuxt app captures screenshots.
Complete Nuxt Example 1: Server Route with Screenshot Endpoint
Let's build a complete Nuxt app with a server route that captures screenshots:
// server/api/screenshot.post.ts
export default defineEventHandler(async (event) => {
const { url } = await readBody(event)
if (!url) {
throw createError({
statusCode: 400,
statusMessage: 'url is required'
})
}
try {
const response = await $fetch(
'https://api.pagebolt.dev/screenshot',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
'Content-Type': 'application/json'
},
body: {
url,
format: 'png',
width: 1280,
height: 720,
fullPage: true
}
}
)
return {
status: 'success',
image: Buffer.from(response).toString('base64'),
mimeType: 'image/png'
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: `PageBolt error: ${error.message}`
})
}
})
// server/api/pdf.post.ts
export default defineEventHandler(async (event) => {
const { url } = await readBody(event)
if (!url) {
throw createError({
statusCode: 400,
statusMessage: 'url is required'
})
}
try {
const response = await $fetch(
'https://api.pagebolt.dev/pdf',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
'Content-Type': 'application/json'
},
body: {
url,
format: 'A4',
margin: '1cm'
}
}
)
return {
status: 'success',
pdf: Buffer.from(response).toString('base64'),
mimeType: 'application/pdf'
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: `PDF generation failed: ${error.message}`
})
}
})
Now create a Vue component to use these routes:
<!-- components/ScreenshotCapture.vue -->
<template>
<div class="screenshot-capture">
<input
v-model="url"
type="text"
placeholder="Enter URL to capture"
class="input"
/>
<button @click="captureScreenshot" :disabled="loading">
{{ loading ? 'Capturing...' : 'Capture Screenshot' }}
</button>
<div v-if="screenshotBase64" class="result">
<h3>Screenshot:</h3>
<img :src="`data:image/png;base64,${screenshotBase64}`" alt="screenshot" />
<button @click="downloadScreenshot">Download PNG</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
const url = ref('')
const loading = ref(false)
const screenshotBase64 = ref('')
const error = ref('')
const captureScreenshot = async () => {
error.value = ''
loading.value = true
try {
const response = await $fetch('/api/screenshot', {
method: 'POST',
body: { url: url.value }
})
screenshotBase64.value = response.image
} catch (err: any) {
error.value = err.data?.statusMessage || 'Failed to capture screenshot'
} finally {
loading.value = false
}
}
const downloadScreenshot = () => {
const link = document.createElement('a')
link.href = `data:image/png;base64,${screenshotBase64.value}`
link.download = 'screenshot.png'
link.click()
}
</script>
<style scoped>
.input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
margin-bottom: 1rem;
}
button {
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.result {
margin-top: 2rem;
}
.result img {
max-width: 100%;
border: 1px solid #ddd;
margin: 1rem 0;
}
.error {
color: red;
margin-top: 1rem;
}
</style>
Complete Nuxt Example 2: Reusable Composable for Server Calls
For cleaner code, create a composable wrapper:
// composables/usePageBoltScreenshot.ts
interface CaptureOptions {
url: string
format?: 'png' | 'jpeg' | 'webp'
fullPage?: boolean
}
interface CaptureResult {
image: string
mimeType: string
}
export const usePageBoltScreenshot = () => {
const loading = ref(false)
const error = ref('')
const captureScreenshot = async (options: CaptureOptions): Promise<CaptureResult | null> => {
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/screenshot', {
method: 'POST',
body: {
url: options.url,
format: options.format || 'png',
fullPage: options.fullPage !== false
}
})
return {
image: response.image,
mimeType: response.mimeType
}
} catch (err: any) {
error.value = err.data?.statusMessage || 'Failed to capture screenshot'
return null
} finally {
loading.value = false
}
}
const capturePdf = async (url: string) => {
loading.value = true
error.value = ''
try {
const response = await $fetch('/api/pdf', {
method: 'POST',
body: { url }
})
return {
pdf: response.pdf,
mimeType: response.mimeType
}
} catch (err: any) {
error.value = err.data?.statusMessage || 'Failed to generate PDF'
return null
} finally {
loading.value = false
}
}
return {
loading: readonly(loading),
error: readonly(error),
captureScreenshot,
capturePdf
}
}
Use it in your component:
<template>
<div>
<input v-model="url" placeholder="Enter URL" />
<button @click="capture" :disabled="loading">
{{ loading ? 'Capturing...' : 'Capture' }}
</button>
<img v-if="screenshotData" :src="`data:image/png;base64,${screenshotData}`" />
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
const url = ref('')
const screenshotData = ref('')
const { captureScreenshot, loading, error } = usePageBoltScreenshot()
const capture = async () => {
const result = await captureScreenshot({ url: url.value })
if (result) {
screenshotData.value = result.image
}
}
</script>
Complete Nitro Example: Standalone Backend Handler
If you're using Nitro directly (not Nuxt), here's how to add PageBolt:
// routes/api/capture.ts (Nitro standalone)
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
const { url } = await readBody(event)
const response = await fetch('https://api.pagebolt.dev/screenshot', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url,
format: 'png',
width: 1280,
height: 720,
fullPage: true
})
})
if (!response.ok) {
throw createError({
statusCode: response.status,
statusMessage: 'PageBolt error'
})
}
const buffer = await response.arrayBuffer()
return {
image: Buffer.from(buffer).toString('base64'),
mimeType: 'image/png'
}
})
Comparison: PageBolt vs Puppeteer vs Self-Hosted
| Feature | PageBolt | Puppeteer | wkhtmltopdf |
|---|---|---|---|
| Vercel/Netlify compatible | Yes | No | No |
| Bundle size impact | 0 bytes | 200MB+ | System package |
| Cold start delay | None | 1-3 seconds | None |
| Serverless ready | Yes (REST API) | Requires separate server | No |
| Memory per request | Hosted (not your problem) | 50-100MB | Minimal |
| Browser management | None | Manual | Unmaintained |
| JavaScript rendering | Full | Full | Basic |
| Cost at scale | $29/mo for 10k requests | $0 (but hosting costs) | $0 (but hosting costs) |
Winner for Nuxt: PageBolt. Zero dependencies, works on Vercel, no cold start penalty.
Cost Analysis
PageBolt pricing:
- Free tier: 100 requests/month
- Paid: $29/month for 10,000 requests (~$0.003 per screenshot)
Self-hosted Puppeteer in Nuxt:
- Additional server: $50–$150/month
- Deployment complexity: Hours of setup and debugging
- Maintenance: 4+ hours/month for updates and crashes
- Real cost: $100–$200/month + your time
At 1,000 requests/month, PageBolt saves you on infrastructure costs alone.
Real-World Example: Export Reports as PDFs
Here's a practical example — a Nuxt app that generates reports and exports them as PDFs:
// server/api/reports/[id]/export.get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// Your report data is already rendered as HTML at this URL
const reportUrl = `${getHeader(event, 'origin')}/reports/${id}/view`
try {
const response = await $fetch(
'https://api.pagebolt.dev/pdf',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`
},
body: {
url: reportUrl,
format: 'A4',
margin: '1cm'
}
}
)
setHeader(event, 'Content-Type', 'application/pdf')
setHeader(event, 'Content-Disposition', `attachment; filename="report-${id}.pdf"`)
return response
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'PDF generation failed'
})
}
})
Next Steps
- Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
- Add to your Nuxt app: Copy the server route examples above
- Create a composable wrapper for cleaner component code
- Scale as needed: If you exceed 100 requests/month, upgrade to a paid plan ($29/month, cancel anytime)
Vue and Nuxt developers shouldn't manage headless browsers. With PageBolt, your fullstack app gets web capture in minutes, not weeks.
Try it free — 100 requests/month, no credit card. Start capturing screenshots now.
Top comments (0)