npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jclusso/ogshot

v0.1.0

Published

Open Graph images from your own HTML and CSS, rendered and cached on Cloudflare Workers.

Readme

ogshot

Open Graph images from your own HTML and CSS, rendered and cached on Cloudflare Workers.

Put a <template data-ogshot> on any page. ogshot loads the page in headless Chromium, swaps the body for your template at 1200x630, screenshots it, and caches the PNG at the edge. Your existing styles, fonts, and templating language all work because it's just your page.

Deploy to Cloudflare

How it works

  1. A crawler requests https://ogshot.example.com/render.png?url=https://example.com/posts/1&v=1725000000.
  2. The Worker fetches the page HTML, pulls out the template, and hashes it. That hash is the cache key.
  3. On a miss, it opens the page in Browser Rendering, replaces the body with the template, waits for images and fonts, and screenshots.
  4. The PNG is stored in the Workers Cache API and served with long cache headers.

Deploy

Click the button above, or:

git clone https://github.com/jclusso/ogshot
cd ogshot
npm install
npx wrangler deploy

Then set ALLOWED_HOSTS to the hosts you want to render. It's a secret, not a variable, so a deploy can never overwrite it. The Deploy button asks for it. Otherwise, in the dashboard under Settings, Variables and Secrets, add a secret named ALLOWED_HOSTS, or from the CLI:

npx wrangler secret put ALLOWED_HOSTS
# example.com,*.example.com

Exact hosts match exactly. *.example.com matches any subdomain but not the apex. Anything else gets a 403, so nobody can spend your browser quota screenshotting other sites.

Browser Rendering is included on the Workers Free plan with a daily time limit, and billed by browser time on the Paid plan. A cached OG image costs nothing to serve, so most sites stay well inside the free allotment.

Use it

1. Add a template to your page

<template data-ogshot>
  <div class="flex h-full w-full flex-col justify-between bg-slate-900 p-20 text-white">
    <h1 class="text-6xl font-bold leading-tight">How we cut build times in half</h1>
    <p class="text-2xl text-slate-400">example.com</p>
  </div>
</template>

Generate the contents with whatever renders the rest of your page. The template is rendered inside a 1200x630 wrapper with overflow: hidden. Use your normal CSS. Anything the page can load, the template can use.

2. Point og:image at the Worker

<meta property="og:image" content="https://ogshot.example.com/render.png?url=https://example.com/posts/1&v=1725000000">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

url is the page's canonical URL, URL-encoded. v is described below. A page without a template gets a 422.

The v parameter

Facebook, X, Slack, and iMessage cache og:image by URL, some effectively forever. The Worker can't do anything about that, so the URL has to change when the image should. Pass something that changes when the card content changes: the record's last-modified timestamp, a content hash, a cache key.

The Worker keys its cache on v together with the template's content. A new v always renders fresh, which also covers changes the template hash can't see, like an edited stylesheet or a replaced image at the same URL. If you forget to bump v, a template edit still triggers a render, but crawlers that already cached the old URL won't see it until v changes. With v present the response is immutable with a one-year max-age; without it, one day.

3. Add the client to every page

Install the package and call it once from your JavaScript entrypoint:

npm install @jclusso/ogshot
import { ogshot } from "@jclusso/ogshot";
ogshot();

Or, if you don't bundle JavaScript, load it from the Worker:

<script src="https://ogshot.example.com/ogshot.js" async fetchpriority="low"></script>

Both are the same code. The package version ships inside your own bundle, so there's no extra origin to connect to or allowlist in your Content Security Policy, and you control when it updates. The script tag is zero-setup and updates with the Worker.

This goes in production, on every page that has an og:image pointing at the Worker. Not just in development.

It does two things:

  • Warms the cache. Crawlers give up after a few seconds and a first render can take that long. The person sharing a link almost always loaded the page first, so the client finds the page's og:image tag and sends it a HEAD request. The Worker renders and caches the image and returns headers only. By the time anyone pastes the link into Slack or X, the image is a cache hit.
  • Previews the template. Open any page with ?ogshot-preview appended and the client swaps the body for your template at 1200x630 so you can tweak it in devtools. Same swap the renderer runs.

The client has no effect on page load. It's under a kilobyte gzipped, and the warm-up waits for the window load event and then an idle period before sending a low-priority request whose response has no body. It fires once per page URL per browser session. Nothing it does shows up in Core Web Vitals.

The Worker never depends on the client. Without it, warm the cache yourself by requesting the image URL when you publish. That's also the right move when one deploy changes many pages at once, since no visitor has seen the new pages yet.

Preview in development

With the client installed, append ?ogshot-preview to any local page.

To see a real PNG of a local page, expose it with cloudflared tunnel --url localhost:3000, add the tunnel host to ALLOWED_HOSTS, and request /render.png?url=<tunnel url>.

Fonts and images

Before the screenshot, the renderer waits for the page's stylesheets, for every <img> and CSS background image inside the template, and for document.fonts.ready, which resolves when every font the layout requested has loaded. Web fonts normally make it into the image. The only time they don't is when a font request takes longer than five seconds.

To keep renders fast, the browser is handed the HTML the Worker already fetched instead of downloading the page again, and requests that can't affect the image are blocked: third-party scripts, iframes, media, and websockets. First-party scripts still run. If your template depends on a script from another domain, inline it or serve it from your own.

If you want to remove even that possibility, preload the fonts the template uses:

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

Preloaded fonts are already in memory when the swap happens, so there's nothing to wait for.

Local development

npm install
cp .dev.vars.example .dev.vars
npm run dev

Edit .dev.vars to list the hosts you want to render locally. Browser Rendering has no local emulation. The binding is configured with remote: true, so wrangler dev runs the Worker locally but sends screenshots through your Cloudflare account. You need to be logged in with wrangler login.

npm test

The client in src/client.js is bundled by esbuild into dist/client before dev, deploy, and test, and published from there with npm publish.

Endpoints

| Path | Description | |---|---| | GET /render.png?url=<page>&v=<version> | The PNG. url must be on ALLOWED_HOSTS. HEAD renders and caches without returning the body. | | GET /ogshot.js | The client as a plain script, for sites that don't bundle. Same code as the npm package. | | GET / | A short description. |

Responses include x-ogshot-cache: HIT or MISS. Misses also carry a Server-Timing header with the time spent fetching the page, getting a browser, loading, swapping in the template, screenshotting, and re-encoding.

The PNG is re-encoded losslessly after the screenshot. Chromium's encoder favors speed, and the re-encode is typically 30 to 40 percent smaller with identical pixels. That matters for gradient-heavy cards, which otherwise land near the 300 KB limit some messaging apps apply to preview images.

Notes

  • The Cache API is per data center, so the first crawler to hit a given region triggers one render there. Expect a few misses per image, not one.
  • Templates should be static HTML and CSS. The renderer waits for stylesheets, images, and fonts, not for your JavaScript.
  • Each cache-warming HEAD costs the Worker one fetch of your page HTML to compute the key. Negligible for most sites; if yours is very high traffic, skip the client and warm on publish instead.