DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Vue.js Screenshot API: Capture Pages from Your Nuxt and Vue Backend

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')
  }
})
Enter fullscreen mode Exit fullscreen mode

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}`
    })
  }
})
Enter fullscreen mode Exit fullscreen mode
// 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}`
    })
  }
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
  }
})
Enter fullscreen mode Exit fullscreen mode

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'
    })
  }
})
Enter fullscreen mode Exit fullscreen mode

Next Steps

  1. Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
  2. Add to your Nuxt app: Copy the server route examples above
  3. Create a composable wrapper for cleaner component code
  4. 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)