DEV Community

Cover image for How to implement emojis in Vercel/satori
Abdurrahman Rajab for OpenSauced

Posted on

How to implement emojis in Vercel/satori

One of the main features that we have in OpenSauced is the highlights, which enables the users to share their latest contributions that they made in open source on our website.

This allows them to have impact-focused stories around their work, more visibility, and an opportunity to inspire and connect with other contributors. While having the highlights on our website was great, we wanted to enable the users to share them on social media, which let us work on implementing the open graph for our website.

In the open graph repo, we used Vercel/Satori library to provide the needed social card images. This implementation got us into an issue with emojis, which were shown as black lines instead of rendered correctly, as shown in the image below.

A social card with broken emojis

Those blocks meant that we needed to change our implementation to fix the issue. When we started to investigate the issue, we thought that it was related to our code and that we just needed to implement some library to render the emojis, but we were wrong with that assumption.

Understanding the Issue

To figure out what we need to do, we investigated the issue by:

  1. Checking what we had,
  2. Creating a hypothesis,
  3. Checking if anyone else reported the same issue before or reported on a solution,
  4. Testing the hypothesis and working on a solution.

Whenever a hypothesis was not working, we moved on and worked on another solution.

Testing Our Hypothesis

We assumed at the beginning that we needed to implement an emoji rendering frame to our implementation, which did not work. Because that didn’t work, we checked Vercel/Satori documentation since we rely on them. Once there, we noticed they had that document on the readme, which we did not see!

The only issue is that they mention how to find an emoji on a page, but they do not mention how to render that emoji. Because there wasn’t an explanation, we checked to see if they had an example.

We went to their repository and checked their examples to see if we could figure out if they had implemented the same logic before or not. We found that they have an open-source example using emojis, and we implemented this example in our project.

When we checked the example, we noticed that it's not only open source, but the Vercel team has modified Twitter's open-source code to implement their solution, which is a great example showing how open source can help companies to be more innovative.

To add emoji support, we converted the string to code point, a number representation of the string in the character systems. Then, we used a third library, Tweemoji, to request and return the SVG code of that emoji.

Before adding the utility functions we used to get the SVG image of the emojis, we only called the Satori function.

    const svg = await satori(template, {
      width: 1200,
      height: 627,
      fonts: [
        {
          name: "Inter",
          data: interArrayBuffer,
          weight: 400,
          style: "normal",
        },
        {
          name: "Inter",
          data: interArrayBufferMedium,
          weight: 500,
          style: "normal",
        },
      ],
      tailwindConfig,
    );
Enter fullscreen mode Exit fullscreen mode

With emoji implementation, we added the load assets function, a built-in function for satori, to the call and updated it with util functions.

  loadAdditionalAsset: async (code: string, segment: string) => {
        if (code === "emoji") {
          // if segment is an emoji
          // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
          return `data:image/svg+xml;base64,${btoa(await loadEmoji("twemoji", getIconCode(segment)))}`;
        }


        // if segment is normal text
        return code;
      },
    }
Enter fullscreen mode Exit fullscreen mode

If you are curious about the full code. Feel free to check this PR with the full code.

A few months later, Biodrop, another open-source project, followed the same implementation that we used in our project to enable the embedding of their cards on other websites.

A social card with emojis after the implementation of this feature

A social card with emojis after the implementation of this feature.

Do you know any other open-source projects that enabled people to be more innovative and faster?

Top comments (3)

Collapse
 
nickytonline profile image
Nick Taylor

Nice one @a0m0rajab!

emojis and pizza

Collapse
 
a0m0rajab profile image
Abdurrahman Rajab

Thanks Nick 🤗

Collapse
 
frontdevcho profile image
taptaptap • Edited

hi!

I am trying to use satori directly within the next 14 API route, but the emoji is not output properly. Do you happen to know the cause?

Image description

import {NextResponse} from "next/server";
import { ImageResponse } from 'next/og'
import satori from 'satori';
import sharp from "sharp";
import fs from 'fs';
import path from 'path';

export async function GET(request: Request) {

    const robotoFontBuffer = fs.readFileSync(path.join(process.cwd(), 'public', 'fonts', 'Roboto-Bold.ttf'));
    const notoSansFontBuffer = fs.readFileSync(path.join(process.cwd(), 'public', 'fonts', 'NotoSansKR-SemiBold.ttf'));
    const notoColorEmojiFontBuffer = fs.readFileSync(path.join(process.cwd(), 'public', 'fonts', 'NotoColorEmoji-Regular.ttf'));

    const svg = await satori((<div
        style={{
            display: 'flex',
            height: '100%',
            width: '100%',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #fff1f1)',
            fontSize: 60,
            letterSpacing: -2,
            fontWeight: 700,
            textAlign: 'center',
        }}
    >
        <svg
            height={80}
            viewBox="0 0 75 65"
            fill="black"
            style={{margin: '0 75px'}}
        >
            <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
        </svg>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(0, 124, 240), rgb(0, 223, 216))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Develop
        </div>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(121, 40, 202), rgb(255, 0, 128))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Preview
        </div>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(255, 77, 77), rgb(249, 203, 40))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Ship ❤️
        </div>
        <img src="https://picsum.photos/150" width={150} height={150}/>
    </div>), {
        width: 1200,
        height: 627,
        fonts: [
            {
                style: "normal",
                name: "Noto Sans",
                data: notoSansFontBuffer,
                weight: 600,
            },
        ],
        debug: true,
    },);

    const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();

    return new NextResponse(pngBuffer, {
        status: 200,
        headers: {
            'Content-Type': 'image/png',
        }
    });

    // return new ImageResponse(
    // (<div
    //     style={{
    //         display: 'flex',
    //         height: '100%',
    //         width: '100%',
    //         alignItems: 'center',
    //         justifyContent: 'center',
    //         flexDirection: 'column',
    //         backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #fff1f1)',
    //         fontSize: 60,
    //         letterSpacing: -2,
    //         fontWeight: 700,
    //         textAlign: 'center',
    //     }}
    // >
    //     <svg
    //         height={80}
    //         viewBox="0 0 75 65"
    //         fill="black"
    //         style={{margin: '0 75px'}}
    //     >
    //         <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
    //     </svg>
    //     <div
    //         style={{
    //             backgroundImage: 'linear-gradient(90deg, rgb(0, 124, 240), rgb(0, 223, 216))',
    //             backgroundClip: 'text',
    //             '-webkit-background-clip': 'text',
    //             color: 'transparent',
    //         }}
    //     >
    //         Develop
    //     </div>
    //     <div
    //         style={{
    //             backgroundImage: 'linear-gradient(90deg, rgb(121, 40, 202), rgb(255, 0, 128))',
    //             backgroundClip: 'text',
    //             '-webkit-background-clip': 'text',
    //             color: 'transparent',
    //         }}
    //     >
    //         Preview
    //     </div>
    //     <div
    //         style={{
    //             backgroundImage: 'linear-gradient(90deg, rgb(255, 77, 77), rgb(249, 203, 40))',
    //             backgroundClip: 'text',
    //             '-webkit-background-clip': 'text',
    //             color: 'transparent',
    //         }}
    //     >
    //         Ship ❤️
    //     </div>
    // </div>),
    //     {
    //         width: 1200,
    //         height: 627,
    //         fonts: [
    //             {
    //                 style: "normal",
    //                 name: "Noto Sans",
    //                 data: notoSansFontBuffer,
    //                 weight: 600,
    //             },
    //         ],
    //     });
}
Enter fullscreen mode Exit fullscreen mode