go back

dynamic open graph images

how i implemented dynamic open graph images for my blog.

2023-09-18 (5 months ago)

you may have noticed that the open graph images for my blog posts are dynamic, and change depending on the content of the post.

for example, the open graph image for this post is og_preview

i don’t manually create these images, or even build them when i build the site, they are generated on the fly when the page is requested (and cached).

these images are generated using a library i wrote for quilt.

it doesn’t take much code to implement, and it’s pretty fast (approx. 5ms). editor

my half-baked temporary editor for quilt images. you can play with it here, the docs are mostly up to date, but just ignore the rest of the site.

how it works

i’m a big fan of storing state in the url, and i do that here too. i didn’t want to setup a database for this, so everything needed to generate the image is stored and signed in the url.

taking a closer look at the url, it’s pretty long, but it’s not that complicated.

https://cnvs.b-cdn.net/get/N4IgzglgXgpiBcBtAHABlQGgCzoLoZADMIAbGMBRUAOwEMBbOeEe2iakAgYwHsATJiAAEI0WJEAdauJlCyAFyEAjAOYB9XiR4AnIQF4hAYgCMMAEwBOAMxKLU2eIVD5MAB7yNPLboOHzMZEJUewdRJ1ouLhhqD00dfSNkY2QuQmQQ0KEM0KcAdwwhAAsEgAEAEQhGakgeaoAKAEpshyceVwKeAE8EuoBWTCF+pulMrJHMkoBhMlptOoiomM9vYdGxtfLtWlyAJR4AV2oBPh2YLnlaahUyOrahAHohLA7ux+ehXKEAWiE7x7MCsUfl0HkIAUJjKhVqMSgBlGDySZeHR1VTLHTQiYAMVIJEazVkBJkRPEuQg8mKJQACvswIVTiRaPIIAA3GC3dq-ToFT7A1xCABUYMB3y5grBDSEwBJDjhCKR3jqLnc6O0mNCMtkcvkWNq8lh0HZVihmpk2t1MTqEhAeyUPHkPAAsrUeF8AJIXEgQLjW9UbMpbXKw+Tadgqeb7Ck6AoDE3jDXxjbwnV6g2wOoANjjazE5r1VptPDtDud1Fd8PoEAAQl4+L7TeJmfIyKUQ4cuEz2U2yAUzO9rQA6If1xMwgPbYOhq5K8k9oQDIYNhtiJwQNT5IRr4oGcqVaI1ep+wmjibJi36w11HBH08I88F232p0ur6nFT7RnaEc50QCMBcUMAAdmVqVttHbTs6j-ACIGAiBagKYxjCsApB2HEAb1CTYJxDMMAHUtkAwCYD4KDyBguCEPnRCzFjAc6IGNcN2tMhCHkb9RmXUQ8xiNN2TMXpMNlM982tR8SxfD1aC9H0MK4kRsKDXCrgAQWoLhCh0EioM7AomL0tRCkQ+jjOzH8HkeRTJzDNSNK00jtBgWg+BjAyjPnEyIXooTRHkoQShxEg8R8kQAF8pAbJwtnYO1PgMRA-MMQhCF6JRUowRLkosWgrGeTLCBgYxegsDKT1CQwAHZUGStBSvMowKqURqCrq8ykuMCrekIWhWp-QwlGMLA+AKhtcCXMrc2TBUUQWaJYmRNVIoRIQwASSElsUHhkrABF+QMY0G0IeJ+XYed4CsKU-NabaEW6AwgSELM-KO3RulO1BzsuibfO+iZx1ySYIG0LgblWoV+QAal+G75E5MGhG6KGtsIHb5G5QYQqwqaFrqEotiOOzvXZaLqFihpMdlQLgvG+qxGR1G7uhlHbqEKHej88LfvEendoSHnYdZwYG05tYQFC-AQFoMBUYoJAaAYQRuzgAgvRcLYSAQEBrRUCAUZDJz6GtMWMHlxhNegoCQI4FXyRgdXNetaI+DIaWhB1lGCjAfWFb4ZweGcQoYGUbQeFyHavxAY3TcEWhI007ROBAVW7ekh2QDoClLggGASAK7Q6COcgBw7I3QpN9OFfNztE+T+3mGtUDaLMKwvlQZAvjMdJI-F0KgA?expires=18446744073709551615&signature=ec2ed4da94cc5d7eb674b09dd4248c8b

the first part of the url is the base url for the image,

https://cnvs.b-cdn.net/get/

everything after is the payload needed to generate the image, lzw compressed and base64 encoded.

i don’t want just anyone to be able to generate images, so the url is signed using a secret key, and the signature is appended to the end of the url.

when the page is requested, i make a request to my server with the payload for the blog, and the server returns a signed url for the image which is put in the og:image meta tag.

because the image isn’t generated at this point, the endpoint is almost instant.

the payload

{
    size: [800, 400],
    assets: [
        {
            name: "title",
            literal: `"${title}"`,
        },
        ...
    ],
    files: [
        {
            name: "main",
            code: `            

            let bg_color = #1e293b9
            let text_color = #e2e8f0
            let accent_color = #818cf8

            ...
            `
        },
    ]
}

the payload is a simple json object with the following fields:

  • size: the size of the image in pixels
  • assets: a list of assets that can be used in the image, these assets can be literals or image urls.
  • files: a list of quilt files that are used to generate the image (usually just one).

the assets are injected into the scope during runtime, so that can be referenced in your code.

for the blog post images, i pass in the title, description, author, date and the url of the post as assets. the rest of the payload remains the same.

why do all this?

because why not, i had fun making this, and i think it’s really neat.

find me somewhere else on the web