Screenshot API for Remix

Generate screenshots, PDFs, and extract web content from your Remix app — one HTTP call, no Puppeteer, no browser infrastructure to manage.

Get Free API Key

Remix Resource Routes for Screenshots

Remix resource routes are the cleanest way to expose a screenshot endpoint. Create a file with no default export and export a loader that streams the image:

// app/routes/api.screenshot.tsx
import { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const target = url.searchParams.get("url");
  if (!target) return new Response("Missing url", { status: 400 });

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url: target,
    format: "png",
    width: "1280",
    full_page: "false",
  });

  const res = await fetch(
    `https://api.snapapi.pics/v1/screenshot?${params}`
  );

  if (!res.ok) return new Response("Screenshot failed", { status: 502 });

  return new Response(res.body, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=3600",
    },
  });
}

PDF Download Resource Route

// app/routes/api.pdf.tsx
import { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  const { searchParams } = new URL(request.url);
  const target = searchParams.get("url");

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url: target!,
    format: "pdf",
    pdf_format: "A4",
    full_page: "true",
    delay: "1000",
  });

  const res = await fetch(
    `https://api.snapapi.pics/v1/screenshot?${params}`
  );

  return new Response(res.body, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": 'attachment; filename="page.pdf"',
    },
  });
}

Using in a Remix Action

Generate a screenshot when a form is submitted — for example, saving a website preview on a user's saved links feature:

// app/routes/links.new.tsx
import { ActionFunctionArgs, redirect } from "@remix-run/node";

export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData();
  const linkUrl = form.get("url") as string;

  // Capture screenshot in background
  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url: linkUrl,
    format: "png",
    width: "1280",
  });

  const snap = await fetch(
    `https://api.snapapi.pics/v1/screenshot?${params}`
  );
  const pngBuffer = Buffer.from(await snap.arrayBuffer());

  // Save to your storage (S3, Cloudflare R2, etc.)
  const previewKey = await uploadToStorage(pngBuffer, "image/png");

  // Save link with preview to DB
  await db.link.create({
    data: { url: linkUrl, previewKey, userId: getUserId(request) },
  });

  return redirect("/links");
}

OG Image Route for Remix Blogs

// app/routes/og.$slug.tsx
import { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const slug = params.slug;
  const templateUrl = encodeURIComponent(
    `https://yoursite.com/og-template/${slug}`
  );

  const snapParams = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url: templateUrl,
    format: "png",
    width: "1200",
    height: "630",
  });

  const res = await fetch(
    `https://api.snapapi.pics/v1/screenshot?${snapParams}`
  );

  return new Response(res.body, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=86400",
    },
  });
}

Environment Setup

Add SNAPAPI_KEY to your .env file for local development. In production, set it as an environment variable in Fly.io, Railway, Render, or Vercel. Never expose it in the browser — resource routes and loaders run server-side only, so your key is always safe inside Remix server code.

Why Remix + SnapAPI Is a Great Combination

Remix's server-first philosophy pairs perfectly with SnapAPI's API model. Resource routes give you clean, cacheable endpoints for screenshots that integrate naturally with Remix's data loading patterns. No special middleware, no global state, no browser workarounds — just a loader function and one fetch call. Streaming response support means you can pipe the screenshot binary directly to the client with minimal memory overhead on your server.

Pricing

Free: 200 captures/month. Starter: $19/month for 5K. Pro: $79/month for 50K. All plans include screenshots, PDFs, scraping, and content extraction. Sign up free at snapapi.pics.

Remix + SnapAPI: Advanced Patterns and Production Tips

Remix's progressive enhancement philosophy and server-first data model make it uniquely well-suited for screenshot API integration. Here's how to use SnapAPI across the full range of Remix features.

Nested Routes and Screenshot Previews

Use Remix's nested route system to show screenshot previews in sidebars or detail panels without full page reloads. A parent route fetches the list; a child route fetches the screenshot for the selected item:

// app/routes/links.$id.preview.tsx
import { LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ params }: LoaderFunctionArgs) {
  const link = await db.link.findUnique({ where: { id: params.id } });
  const params2 = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url: link!.url,
    format: "png",
    width: "800",
    full_page: "false",
  });
  const res = await fetch(`https://api.snapapi.pics/v1/screenshot?${params2}`);
  const buf = Buffer.from(await res.arrayBuffer());
  return { base64: buf.toString("base64") };
}

export default function LinkPreview() {
  const { base64 } = useLoaderData<typeof loader>();
  return <img src={`data:image/png;base64,${base64}`} alt="Preview" className="rounded-lg shadow" />;
}

Fetcher-Based Screenshot on Hover

Use Remix's useFetcher to capture screenshots lazily — only when the user hovers over a link, avoiding unnecessary API calls:

import { useFetcher } from "@remix-run/react";

function LinkCard({ url }: { url: string }) {
  const fetcher = useFetcher<{ base64: string }>();

  function handleMouseEnter() {
    if (!fetcher.data) {
      fetcher.load(`/api/preview?url=${encodeURIComponent(url)}`);
    }
  }

  return (
    <div onMouseEnter={handleMouseEnter} className="relative group">
      <a href={url}>{url}</a>
      {fetcher.data && (
        <div className="absolute z-10 top-full left-0 mt-1 shadow-xl">
          <img src={`data:image/png;base64,${fetcher.data.base64}`} alt="Preview" width={320} />
        </div>
      )}
    </div>
  );
}

Caching Screenshots with Remix + Redis

// app/utils/screenshot-cache.server.ts
import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

export async function getCachedScreenshot(url: string): Promise<Buffer | null> {
  const cached = await redis.get(`snap:${Buffer.from(url).toString('base64')}`);
  return cached ? Buffer.from(cached, 'base64') : null;
}

export async function cacheScreenshot(url: string, buf: Buffer, ttl = 3600) {
  await redis.setEx(
    `snap:${Buffer.from(url).toString('base64')}`,
    ttl,
    buf.toString('base64')
  );
}

Error Boundaries for Screenshot Failures

Remix's ErrorBoundary system handles screenshot failures gracefully, showing a placeholder instead of crashing the page:

export function ErrorBoundary() {
  return (
    <div className="w-full h-48 bg-gray-100 rounded-lg flex items-center justify-center text-gray-500">
      Preview unavailable
    </div>
  );
}

Remix + SnapAPI on Fly.io

Fly.io is a popular Remix deployment target. Set your API key as a Fly secret: fly secrets set SNAPAPI_KEY=your_key_here. The key will be available as process.env.SNAPAPI_KEY in all resource routes and loaders. Fly's global edge network means your Remix server is geographically close to users, reducing latency for screenshot requests that need to round-trip to SnapAPI's servers.

Comparing Remix Screenshot Approaches

For simple cases, the resource route approach (one fetch to SnapAPI, stream back the result) is all you need. For repeat views of the same URLs, add Redis caching. For high-traffic apps with many unique URLs, use a background job queue (BullMQ, Inngest, Trigger.dev) to pre-generate screenshots asynchronously and store them in R2 or S3, serving them as static assets. SnapAPI's median response time of under 2 seconds makes synchronous generation viable for most use cases without a queue.

Free to Get Started

SnapAPI's free tier gives 200 captures per month — enough to prototype your Remix app and validate the integration. Sign up at snapapi.pics, no credit card required.

Remix + SnapAPI: Common Patterns Summary

The three patterns that cover 90% of Remix screenshot use cases are: a resource route that proxies the SnapAPI response directly to the browser (for one-off captures), a loader that generates a base64 data URI embedded in server-rendered HTML (for SSR-optimized previews), and a useFetcher hook that triggers lazy capture on user interaction (for hover previews and on-demand generation).

For production apps handling significant volume, add a simple cache layer using Remix's session storage or an external Redis instance. Cache keys should be derived from the target URL plus any relevant parameters (viewport size, format). A 1-hour TTL works well for most use cases — set longer for evergreen content like landing pages, shorter for live dashboards or frequently updated sites.

Error handling deserves attention: SnapAPI returns non-200 status codes for timeout, invalid URLs, and server errors. In Remix loaders, convert these to appropriate HTTP responses (502, 400) rather than letting them bubble up as 500 errors. Use Remix's built-in ErrorBoundary on routes that display screenshots so a failed capture degrades gracefully to a placeholder image rather than crashing the entire page.

For teams building multi-tenant SaaS on Remix, track screenshot usage per account in your database before each SnapAPI call. This lets you enforce per-plan quotas, bill for overages, and display usage stats in your dashboard without relying on SnapAPI as your source of truth for billing data.

Sign up free at snapapi.pics — 200 captures per month, all endpoints included, no credit card required.

Questions about integrating SnapAPI with Nuxt or Remix? The team is available at hello@snapapi.pics and typically responds within one business day. We also offer custom onboarding calls for teams on the Pro or Business plan to walk through architecture decisions, caching strategies, and deployment-specific configuration for your stack.

Start building today at snapapi.pics. Free tier, no credit card required, instant API key.