DEV Community

Nicolás Giacconi
Nicolás Giacconi

Posted on

How to create dynamic NextJS post thumbnails...like DEV.to!

This article was originally published in spanish on my blog

How to highlight your posts on social media

They say an image is worth a thousand words...and that's vital when you share content on social media. I see it a lot when I'm on Twitter and I see people sharing links. Those links have featured images, which can improve the CTR (Clic Through Rate) and also the conversion rate. Obviously, that image has to be a quality-image, it has to explain the content, it needs to be adaptable to every device but... what happens with the post links or content without a featured image? 🤔

Those links ara harder "to sell" on social media and, in the most commons cases, they have a generic image, or the post website logo. But for a long time I'm seeing a website which resolved this particular case in a very original way, achieving (at least in my case) paying more attention to their posts shared on social media.. And this not only helps (and it's more fancy) on social media, but also in any microbrowser. But... what is a microbrowser?

You use microbrowsers everyday... but you still don't know it...

The microbrowsers are used everyday, for almost everybody with a smartphone/tablet/PC. Every time when a link is shared on social media like Facebook or Twitter, when some user shares a link in platforms like Slack, Microsoft Teams, or if a link is shared on any messaging service like WhatsApp, Skype or Telegram. Everytime a link is shared in any of those platforms, the platform makes a link fetch, making a GET query, and obtaining limited results to show it in a more fancy way to the platform user. Therefore, instead of showing only the plain link, the platform shows to the user the featured image obtained on the GET query, the link title and the link meta description. That is what a microbrowser does, and they're used to format the link content, shared on many platforms nowadays.

Share links with featured images VS share links without featured images

Despite the GET query, that doesn't means that the platform has to receive the whole website like a regular visit. The microbrowsers has the next limitations:

  • The HTML parsing is limited, and some tags are filtered
  • The cookies are not taken in account
  • They doesn't execute JavaScript
  • Some microbrowsers doesn't allow 301 or 302 redirections
  • The GET query doesn't count as a regular visit, and the link click doesn't counts as a referral (to visit trackers like Google Analytics)

In summary, the microbrowsers does a fetch of the basic information of the shared link, and that is the next info:

  • Link title, could be the title tag, or maybe the og:title tag, always inside the head tag.
  • Link description, which is the og:description tag value, always inside the head tag.
  • Link featured image, which can be the og:image, og:image:secure_url or twitter:image:src tag value. For the links shared on Twitter, you can also specify the twitter:card tag to make Twitter knows the visual format of the link featured image.

On my blog were already those tags to make the shared content more fancy on social media. But turning to the main point...what can we do with the links without featured image? How can be featured on social media?

DEV.to nailed it

DEV.to is an awesome platform to publish technical programming-related content. I strongly recommend this website, which achieved a huge, faithful and pacific community (something strange nowadays on the internet).

Almost everyday I found DEV.to content shared on my Twitter timeline, and sometimes the featured image is fancy thumbnail with the post title, the author, the publish date and some programming language logos. The first time I saw it I thinked that was a very clever solution to highlight the posts without featured images on social media in a very simple and fancy way. But... how they do it?

This is the way DEV.to shares content on social media... and it's awesome!

Generating dynamic thumbnails with Node and NextJS

Inspecting the DEV.to code (with Chrome Developer Tools, or the source code available on GitHub) I've seen a specific function to generate the post featured image. Adapt it to a JavaScript stack like the one on my blog (NextJS) it doesn't seems a problem. The basic functionality to achieve is the next one: get an URL where, if you make a GET query, it returns us an image with the post title you want to share, the blog's name, my personal image and the publish date. To achieve all of this, I decide to use the native NextJS serverless functions, as they adapt it perfectly to cases like this one. The only thing I have to do to create a serverless function in NextJS is to create a JavaScript function inside the /pages/api/ folder, in order to notify NextJS that this one is a serverless function (or Lambda in Amazon Web Services). With this function, we can get different results than with NextJS pages or React components. Also, the function will have as param the post slug, to know which post we need to return the featured image. The basic aproach is the next one:

export default async (req, res) => {
    const postSlug = req.query.post;
    const post = searchPostBySlug(postSlug);

    const postThumbnail = generateThumbnail(post);
    res.writeHead(200, {
        "Content-Type": "image/png",
        "Content-Length": Buffer.byteLength(screenShotBuffer),
    });
}
Enter fullscreen mode Exit fullscreen mode
  • We search the post with incoming slug
  • We generate the thumbnail we want to show when we share the link on social media
  • We return the thumbnail with the image headers

Easy-peasy right? Not really... In order to style the image content when we share the link, and get some DEV.to styles, we have to know that the serverless function doesn't work on the browser, but directly on the server, Node-only, so we can forget to parse and style HTML, not even CSS. But... there's an alternative. The best way to layout and style the image as we want, is with HTML and CSS, therefore, we need to achieve a Browser in the server. That we need is the chrome-aws-lambda and the headless version of Chrome, puppeteer-core. With these two npm packages we'll can parse HTML and CSS directly from the serverless function as a regular browser. Therefore, our serverless function could be as the next code to get the image we want:

import fs from 'fs';
import path from 'path';

import { getPostBySlug } from '../../services/postsService';

import chromium from 'chrome-aws-lambda';

export default async (req, res) => {
    const postSlug = req.query.post.replace('.jpg', '');
    const post = await getPostBySlug(postSlug);

    const imageAvatar = fs.readFileSync('./public/xaconi.jpg');
    const base64Image = new Buffer.from(imageAvatar).toString('base64');
    const dataURI = 'data:image/jpeg;base64,' + base64Image;
    const originalDate = new Date(post.attributes.date);
    const formattedDate = `${originalDate.getDate()}/${('0' + (originalDate.getMonth()+1)).slice(-2)}/${originalDate.getFullYear()}`;

    const browser = await chromium.puppeteer.launch({
        args: [...chromium.args, "--hide-scrollbars", "--disable-web-security"],
        defaultViewport: chromium.defaultViewport,
        executablePath: await chromium.executablePath,
        headless: true,
        ignoreHTTPSErrors: true,
    });

    const tags = post.attributes.tags?.map((tag) => {
        return `#${tag}`
    }).join(' | ') || "";

    const page = await browser.newPage();
    page.setViewport({ width: 1128, height: 600 });
    page.setContent(`<html>
        <!-- The HTML of the thumbnail to share -->
    </html>`);
    const screenShotBuffer = await page.screenshot();
    res.writeHead(200, {
        "Content-Type": "image/png",
        "Content-Length": Buffer.byteLength(screenShotBuffer),
    })
    res.end(screenShotBuffer);
}
Enter fullscreen mode Exit fullscreen mode

We load the images we need direcly on the HTML (the image of my avatar only) and we start the headless browser which it will parse the HTML and CSS code. We adjust some vars we'll use on the HTML structure and we send it to the browser to load them. At the end, the HTML code doesn't matter (and it's very subjective), what it matters is that, the content we send to the headless browser, is correctly layouted like with a regular browser. The HTML code I used is the next one, but you can layout the image to share as you want:

// ...

page.setContent(`<html>
    <body>
        <div class="social-image-content">
            <h1>
                ${ post.attributes.title }
            </h1>
            <div class="social-image-footer">
                <div class="social-image-footer-left">
                    <img src="${ dataURI }" />
                    <span>Xaconi.dev · ${ formattedDate } </span>
                </div>
                <div class="social-image-footer-right">
                    ${tags}
                </div>
            </div>
        </div>
    </body>
    <style>
        html, body {
            height : 100%;
        }
        body {
            align-items : center;
            display : flex;
            height : 600px;
            justify-content : center;
            margin: 0;
            width : 1128px;
            background-color: #e2e2e2;
        }
        .social-image-content {
            border : 2px solid black;
            border-radius : 5px;
            box-sizing: border-box;
            display : flex;
            flex-direction : column;
            height : calc(100% - 80px);
            margin : 40px;
            padding : 20px;
            width : calc(100% - 80px);
            position: relative;
            background-color: white;
        }
        .social-image-content::after {
            content: ' ';
            position: absolute;
            top: 7px;
            left: 7px;
            width: 100%;
            background-color: black;
            height: 100%;
            z-index: -1;
            border-radius: 5px;
        }
        .social-image-content h1 {
            font-size: 72px;
            margin-top: 90px;
        }
        .social-image-footer {
            display : flex;
            flex-direction : row;
            margin-top : auto;
        }
        .social-image-footer-left {
            align-items: center;
            display: flex;
            flex-direction: row;
            font-size : 28px;
            font-weight : 600;
            justify-content: center;
            line-height: 40px;
        }
        .social-image-footer-left img {
            border : 2px solid black;
            border-radius : 50%;
            height : 40px;
            margin-right : 10px;
            width : 40px;
        }
        .social-image-footer-right {
            align-items: center;
            display: flex;
            flex-direction: row;
            height : 40px;
            justify-content: center;
            margin-left : auto;
            font-size : 28px;
        }
        * {
            font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
            Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
            font-weight : 600;
        }
    </style>
</html>`);

// ...
Enter fullscreen mode Exit fullscreen mode

Finally, we have to put the serverless function call on the HTML tags for the microbrowsers. When they read the Post basic information, we'll receive this image.

<html>
    <head>
        <!-- ... -->
        <meta property="og:image" content="https://xaconi.dev/api/social-image/como-crear-thumbnails-dinamicos-en-next-js.jpg">
        <meta property="og:image:secure_url" content="https://xaconi.dev/api/social-image/como-crear-thumbnails-dinamicos-en-next-js.jpg">
        <meta name="twitter:image:src" content="https://xaconi.dev/api/social-image/como-crear-thumbnails-dinamicos-en-next-js.jpg">
        <!-- ... -->
        </head>
        <body>
            <!-- ... -->
        </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Well, this is done right? We have the code to generate a dynamic image in a serverless function, and this function can be called making a GET query from any browser. Testing the code on a local environment everything looks good... right? Well no, there is still a few things to fix.

Making the Deploy, bugs on Vercel... and be careful with our budle size...

My blog (and many other webs) are hosted by Vercel, which is a fantastic hosting for static pages, with frameworks like React, NextJS, Angular, Vue, etc. or SSG as Gatsby or Jekyll. Is an ideal service for blogs like mine, and offers a really interesting free-tier, besides gaining Analytics, performance helps, and most important... they allow serverless functions. The Vercel team (previously Zeit) are the creators of the NextJS framework, so if you have a web based on NextJS, Vercel is a totally recommended service.

But making the Deploy for this project, in concrete the thumbnail generation functionality, I've found some problems. On one hand, Vercel limits the serverless functions to 50MB max-size. It's a lot, but we have consider that we're loading a Chrome browser (even it's a headless version) in only one function. Even we're not surpassing the function size limit, we're close, and deploying the project to Vercel I found what it seems a bug, because the logs on Vercel told me that the image generation function was bigger than 50MB. I looked over the function and the bundle size and everything seems ok. Even so, the problem was there.

At last, I reached a Vercel GitHub issue, where other users commented exactly the same problem. The solution? Move the function from /pages/api/ to /api/ on the project root folder. That change makes that the NextJS serverless functions, become Vercel serverless functions. And with this change, the Deploy now was possible. The only change to do, besides that, was start my local development environment with vercel dev instead of next dev.

Code example and demo

On the public repo of my blog you can find the example of the finished code, or you can look at the link of my first post thumbnail. I also give you a basic CodePen with a layout sample of the final image style. You can edit it to get your desire thumbnail visual style and use it on the serverless function. The layout we'll be the same after being parsed with the headless Chrome.

🙏 And that's all folks! With a little code you can achieve wonderful things, and in this case, you can see the difrerence between share a link without thumbnail, compared with a link shared with a fancy custom image, on social media. You can play with the final layout (I used the same visual style as DEV.to). Another aproach is to use the headless Chrome to get a full render of the post and use that render to make screenshot and present it as featured image on social media. Personally, I think it's prettier the DEV.to version, but it needs more work.

Thanks for reading! If you like this article you can let me know about it, and if you have another aproach with the custom image thumbnail on social media, leave a comment. You can follow me on Twitter to get any updates on my blog's work!

Top comments (1)

Collapse
 
beyarz profile image
Beyar

Gold!