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 https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/small/9:16/bigger/
.
You can see this live in production now in a few different places on the Eleventy docs.
Decisions, decisions
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:
- Sizing options are limited to improve cache hits. Currently we only offer 11 different image combinations for each URL. This will likely increase over time as we add additional options, like sizes or aspect ratios or maybe even a no-JavaScript mode. We want cache hits to make these things fast and reduce the request count to external web sites.
- 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
sharp
andeleventy-img
too. - 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 tov2
and just leave the old branch as-is. Of particular note is that https://screenshot.11ty.dev (without the version) redirects via an HTTP 301 tov1
and 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 width
and height
attributes).
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.
Demos
Small (375px viewport width)
https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/small/9:16/larger/
Medium (650px viewport width)
https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/medium/9:16/larger/
Large (1024px viewport width)
https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/large/1:1/larger/
Open Graph (1200×630)
https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/opengraph/
9 Comments
@TheGreenGreek
Very cool! Typo: Words great with <img loading="lazy">
@zachleat
Ack, thank you! deploying
@TheGreenGreek
This is really cool. Another option could be to generate 1 image then use a cloudinary proxy to provide different sizes and formats
@TheGreenGreek
Like a fancier version of this timkadlec.com/remembers/2020…
@zachleat
Ooh, yeah, that’s a good idea 🙌🏻
@TheGreenGreek
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! 😅
@DavidWells
That’s pretty nifty! Where is the builder handler wrapper saving the images to?
@davatron5000
❤️ it! How do you invalidate or update the screenshot when changes happen?
@zachleat
> automatically cached on Netlify’s Edge CDN via docs.netlify.com/configure-buil…