Building an Automated Screenshot Service on Netlify in ~140 Lines of Code
This post is a continuation of the ideas first presented in How and Why I Removed 3000 Images from the Eleventy Docs Build.
The idea is pretty simple: a service that will accept a URL as input and return a static screenshot image of that URL to embed and use on other web sites. The code is pretty simple too, about 140 lines.
Having a service for these images is important as the Eleventy docs use a lot of visuals from Built With Eleventy sites around the web—it wouldn’t be feasible to generate these manually.
The end result looks something like this (
11ty.dev/docs is shown):
And the URL for the above image is
You can see this live in production now in a few different places on the Eleventy docs.
I think there were a few architecture decisions that went into this service that are worth documenting, so here goes:
- This is now a separate repo and project from the main 11ty.dev site. This is important as it decouples our On-demand Builder cache for this service away from the main web site, which deploys with a much higher frequency.
- This is best used with lower priority images, things that live further down the page (dare I say, below the imaginary fold). Works great with
<img loading="lazy">. ⚠️ ABSOLUTELY not for use with HERO IMAGES or on something that might be eligible for your LCP!!! (I warned you with three exclamation marks.)
- Best paired with preconnect:
<link href="https://v1.screenshot.11ty.dev" rel="preconnect" crossorigin>.
- Best paired with preconnect:
- I added an Open Graph size (you know, for those cards that show up on social media posts). I’m currently playing around with this as a way to do super-lazy custom Open Graph images for every page. Each page can have an Open Graph image that’s a screenshot of itself!
- One negative of generating these in a serverless function is that image formats are a bit harder to manage. This means that only JPEG is supported for now. Especially with the version of Puppeteer that barely fits in a serverless bundle, I’m still trying to figure out how to bundle it with
- The entire thing is versioned using Netlify Branch subdomains: e.g.
https://v1.screenshot.11ty.dev. If I want to change the API later I’ll bump it to
v2and just leave the old branch as-is. Of particular note is that https://screenshot.11ty.dev (without the version) redirects via an HTTP 301 to
v1and will do so permanently. Don’t rely on this redirect (for performance reasons).
- Update (July 30, 2021): The other issue I noticed with using Puppeteer in a Lambda is that emoji are not available to the rendered content. So if a site is using Emoji they do not render. It looks like Matic Jurglič may have a workaround to solve this.
What happens if a site is super slow or is currently down?
Netlify Functions have a 10 second execution limit. If the site doesn’t render in 10 seconds, we show a fallback image by default. Currently this is a low-contrast 11ty logo using the same image size as the requested screenshot (via SVG
We don’t use a HTTP 500 status code on errors. In Firefox, the fallback image didn’t render when an error code was used. Because we aren’t using a HTTP 500 status code, the On-demand Builder will cache the fallback image for this request. This is good to prevent a bunch of re-requests to slow sites that don’t make the cutoff (or have a different error) but also means if a request had an outlier response time then the fallback image will continue to be used until the On-demand Builder cache is invalidated with a new build.
We include the real error message in a custom
x-error-message HTTP Header, if you want more insight into why a screenshot failed.
Can I Use Your Instance For My Site?
Um… I’m not sure yet. For now I’d recommend just self hosting it. You can click this button to do it:
The full source code is available on GitHub.
Small (375px viewport width)
Medium (650px viewport width)
Large (1024px viewport width)
Open Graph (1200×630)
Very cool! Typo: Words great with
Ack, thank you! deploying
This is really cool. Another option could be to generate 1 image then use a cloudinary proxy to provide different sizes and formats
Like a fancier version of this timkadlec.com/remembers/2020…
Ooh, yeah, that’s a good idea 🙌🏻
I'd probably turn it into an api that only accepts requests from a specific domain so you don't go over your limits lol. It looks like a fun thing to try to code. I'd play with it but have too many things I'm fiddling with right now! 😅
That’s pretty nifty! Where is the builder handler wrapper saving the images to?
❤️ it! How do you invalidate or update the screenshot when changes happen?
> automatically cached on Netlify’s Edge CDN via docs.netlify.com/configure-buil…