DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Screenshot API for Deno: Capture Pages Without node_modules

Screenshot API for Deno: Capture Pages Without node_modules

Deno is TypeScript-first, has built-in fetch, and deliberately avoids node_modules. That philosophy fits perfectly with a screenshot API: one HTTP call, binary response, done. No package to install, no compatibility shim, no headless Chrome binary to manage.

This guide shows how to integrate screenshot and PDF generation into Deno scripts, Fresh routes, and Oak handlers using PageBolt — a hosted browser capture API with 100 free requests per month, no credit card required.

Why Not Puppeteer in Deno?

Puppeteer works in Deno, but it fights the runtime's design:

  • node_modules compatibility flag required — you need --allow-env, --allow-net, --allow-run, --allow-read, --allow-write, plus --node-modules-dir or an npm specifier. The ergonomics are rough.
  • Chromium download on first run — adds several hundred MB to your deploy or container image.
  • Process management — you're responsible for browser lifecycle, crash recovery, and memory leaks in long-running servers.
  • Deno Deploy incompatible — you cannot run headless Chrome on Deno Deploy at all.

A screenshot API sidesteps every one of these. Your Deno code stays pure TypeScript with zero native dependencies.

The Simplest Screenshot in Deno

const response = await fetch("https://pagebolt.dev/api/v1/screenshot", {
  method: "POST",
  headers: {
    "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ url: "https://example.com", fullPage: true }),
});

const buffer = await response.arrayBuffer();
await Deno.writeFile("screenshot.png", new Uint8Array(buffer));
console.log("Saved screenshot.png");
Enter fullscreen mode Exit fullscreen mode

Run it:

PAGEBOLT_API_KEY=your_key deno run --allow-net --allow-env --allow-write screenshot.ts
Enter fullscreen mode Exit fullscreen mode

No --unstable. No --node-modules-dir. Just the permissions your script actually needs.

PDF Generation

Same pattern, different endpoint:

async function generatePDF(url: string, outputPath: string): Promise<void> {
  const response = await fetch("https://pagebolt.dev/api/v1/pdf", {
    method: "POST",
    headers: {
      "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url,
      pdfOptions: { format: "A4", printBackground: true },
    }),
  });

  if (!response.ok) {
    throw new Error(`PDF generation failed: ${response.status}`);
  }

  const buffer = await response.arrayBuffer();
  await Deno.writeFile(outputPath, new Uint8Array(buffer));
}

await generatePDF("https://example.com/invoice/123", "invoice-123.pdf");
Enter fullscreen mode Exit fullscreen mode

Pass raw HTML instead of a URL if you're rendering from a template:

body: JSON.stringify({
  html: `<!DOCTYPE html><html><body><h1>Invoice #123</h1></body></html>`,
  pdfOptions: { format: "A4" },
}),
Enter fullscreen mode Exit fullscreen mode

This is the cleanest invoice generation pattern in Deno — render your HTML string, POST it, get a PDF back.

Screenshot Monitoring Cron Job

Deno has a built-in cron API (Deno.cron) in Deno Deploy and recent runtimes. Here's a daily monitoring job that captures a screenshot and saves it with a datestamped filename:

Deno.cron("daily-screenshot", "0 8 * * *", async () => {
  const date = new Date().toISOString().split("T")[0];
  const outputPath = `./snapshots/${date}.png`;

  const response = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: {
      "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: "https://yourapp.com",
      fullPage: true,
      blockBanners: true,
      blockAds: true,
    }),
  });

  const buffer = await response.arrayBuffer();
  await Deno.writeFile(outputPath, new Uint8Array(buffer));
  console.log(`[${date}] Snapshot saved to ${outputPath}`);
});
Enter fullscreen mode Exit fullscreen mode

Compare snapshots over time with any image diff library to catch visual regressions or outages without running a full Playwright test suite.

Caching with Deno KV

If you're generating screenshots on-demand (OG images, previews), cache results in Deno KV to avoid burning API quota on repeated requests:

const kv = await Deno.openKv();

async function cachedScreenshot(url: string): Promise<Uint8Array> {
  const cacheKey = ["screenshots", url];
  const cached = await kv.get<Uint8Array>(cacheKey);

  if (cached.value) {
    console.log("Cache hit");
    return cached.value;
  }

  const response = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: {
      "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url, viewportWidth: 1200, viewportHeight: 630 }),
  });

  const buffer = new Uint8Array(await response.arrayBuffer());

  // Cache for 1 hour
  await kv.set(cacheKey, buffer, { expireIn: 60 * 60 * 1000 });
  return buffer;
}
Enter fullscreen mode Exit fullscreen mode

Deno KV is zero-config locally and built into Deno Deploy — no Redis, no external state.

Fresh Route: OG Image Generation

In a Fresh project, generate Open Graph images on the fly without any server-side image library:

// routes/og/[slug].ts
import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    const { slug } = ctx.params;
    const templateUrl = `https://yoursite.com/og-template?slug=${slug}`;

    const response = await fetch("https://pagebolt.dev/api/v1/screenshot", {
      method: "POST",
      headers: {
        "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: templateUrl,
        viewportWidth: 1200,
        viewportHeight: 630,
        format: "jpeg",
        quality: 90,
        blockBanners: true,
      }),
    });

    const buffer = await response.arrayBuffer();
    return new Response(buffer, {
      headers: { "Content-Type": "image/jpeg", "Cache-Control": "max-age=86400" },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Every blog post gets a unique, properly sized OG image without installing Sharp, Canvas, or any native module.

Oak Handler: PDF Download Endpoint

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const router = new Router();

router.get("/invoice/:id/pdf", async (ctx) => {
  const id = ctx.params.id;

  const response = await fetch("https://pagebolt.dev/api/v1/pdf", {
    method: "POST",
    headers: {
      "x-api-key": Deno.env.get("PAGEBOLT_API_KEY")!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: `https://yourapp.com/invoice/${id}?print=true`,
      pdfOptions: { format: "A4", printBackground: true },
    }),
  });

  const buffer = await response.arrayBuffer();
  ctx.response.headers.set("Content-Type", "application/pdf");
  ctx.response.headers.set("Content-Disposition", `attachment; filename="invoice-${id}.pdf"`);
  ctx.response.body = new Uint8Array(buffer);
});
Enter fullscreen mode Exit fullscreen mode

deno.json Import Map

If you're calling PageBolt from multiple files, define a thin wrapper and reference it via import map:

// deno.json
{
  "imports": {
    "@/capture": "./lib/capture.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode
// lib/capture.ts
const BASE = "https://pagebolt.dev/api/v1";
const KEY = () => Deno.env.get("PAGEBOLT_API_KEY")!;

export async function screenshot(url: string, opts = {}): Promise<Uint8Array> {
  const r = await fetch(`${BASE}/screenshot`, {
    method: "POST",
    headers: { "x-api-key": KEY(), "Content-Type": "application/json" },
    body: JSON.stringify({ url, ...opts }),
  });
  return new Uint8Array(await r.arrayBuffer());
}

export async function pdf(url: string, opts = {}): Promise<Uint8Array> {
  const r = await fetch(`${BASE}/pdf`, {
    method: "POST",
    headers: { "x-api-key": KEY(), "Content-Type": "application/json" },
    body: JSON.stringify({ url, ...opts }),
  });
  return new Uint8Array(await r.arrayBuffer());
}
Enter fullscreen mode Exit fullscreen mode

Now every file in your project does import { screenshot, pdf } from "@/capture" — no npm, no lock file churn.

Free Tier

PageBolt's free tier includes 100 requests per month — no credit card required. That covers most development and light-production workloads. Paid plans start at $29/mo for 5,000 requests.

Get started: pagebolt.dev

Top comments (0)