Zach’s ugly mug (his face) Zach Leatherman

Building an Automated Screenshot Service on Netlify in ~140 Lines of Code

July 30, 2021

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):

Screenshot of 11ty.dev

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:

  1. 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.
  2. 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>.
  3. 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!
  4. 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 and eleventy-img too.
  5. 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 v2 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 to v1 and will do so permanently. Don’t rely on this redirect (for performance reasons).
  6. 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:

Deploy to Netlify

The full source code is available on GitHub.

Demos

Small (375px viewport width)

Screenshot of 11ty.dev

https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/small/9:16/larger/

Medium (650px viewport width)

Screenshot of 11ty.dev

https://v1.screenshot.11ty.dev/https%3A%2F%2Fwww.11ty.dev%2Fdocs%2F/medium/9:16/larger/

Large (1024px viewport width)

Screenshot of 11ty.dev

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/


< Newer
If I work really hard on my Open Graph Images, People will share my Blog Posts
Older >
Uniclode: yet another demo of Eleventy Serverless

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at IndieWeb Avatar for https://cloudcannon.com/CloudCannon. He is the creator and maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 79 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

11 Reposts

EleventyEliad MoosaviWill MendesM157q News RSSMatt BiilmannIain FreestoneChristophe JeanFrontend Daily 🚀Fernando - Código FaladoIndieWeb Avatar for https://www.pixellyft.comIndieWeb Avatar for https://www.zachleat.com

66 Likes

Prince WilsonTomek SułkowskiZacabk SeuberlingJames BatesonViniciusDallacqua 🌐Adam GreenoughTristan RemyPaul J Stales 🧑🏻‍💻Iain Beankeith h.Gaël PoupardargelKen DomanEric Wallace𝒟𝑜𝑔𝓊𝓀𝒶𝓃elvendrimMathias RechtzigelSia KaramalegosTanner DolbyDave Pedu𝕕𝔾𝕣𝕒𝕞𝕞𝕒𝕥𝕚𝕜𝕠Yannis Spyrou˗ˏˋ abhinav.co ˊˎ˗TRST_BlogMatt BiilmannCarles MuiñosEugene ChulkovWill MendesTRST_BlogNick SollecitoMike AparicioNaomi See 🤖👩🏻‍🎤❤️🤦‍♀️🤷🏻‍♀️John MeyerhoferWinston FassettAlex PageJakeBryce Wray Brian RinaldiMatt BiilmannMatthias OttJens GrochtdreisTrent WaltonAravind Reddy V (vi/vim)Mohammed Zeeshan🌸🌕⭐🌕🌸olivMNate van der VisPatrick HaugPhil HawksworthEleventyGeorge GriffithsDavid O'HalloranSia KaramalegosChris McMahonNikhil MehtaMatt Strömchristina 🥚Steve LeeStephanie Eckles🧚🏽‍♀️ 𝔭𝔞𝔯𝔢𝔢𝔫𝔞 🧚🏽‍♀️Andrew MasonAshiateyMaxDave RupertFilipi
9 Comments
  1. Sia Karamalegos

    @TheGreenGreek

    Very cool! Typo: Words great with <img loading="lazy">

  2. Zach Leatherman

    @zachleat

    Ack, thank you! deploying

  3. Sia Karamalegos

    @TheGreenGreek

    This is really cool. Another option could be to generate 1 image then use a cloudinary proxy to provide different sizes and formats

  4. Sia Karamalegos

    @TheGreenGreek

    Like a fancier version of this timkadlec.com/remembers/2020…

  5. Zach Leatherman

    @zachleat

    Ooh, yeah, that’s a good idea 🙌🏻

  6. Sia Karamalegos

    @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! 😅

  7. David Wells

    @DavidWells

    That’s pretty nifty! Where is the builder handler wrapper saving the images to?

  8. Dave Rupert

    @davatron5000

    ❤️ it! How do you invalidate or update the screenshot when changes happen?

  9. Zach Leatherman

    @zachleat

    > automatically cached on Netlify’s Edge CDN via docs.netlify.com/configure-buil…

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)