DEV Community

Cover image for Make a beautiful OG image effortlessly for Qwik (emoji support)
YURIIDE
YURIIDE

Posted on • Updated on

Make a beautiful OG image effortlessly for Qwik (emoji support)

Original: https://github.com/fabian-hiller/valibot/blob/main/website/src/routes/og-image/index.ts
Fonts: https://github.com/fabian-hiller/valibot/tree/main/website/src/fonts

This script "OG image" created for the website Qit.tools.

import type { RequestHandler } from "@builder.io/qwik-city";
import { fetchFont, html, ImageResponse } from "og-img";

const U200D = String.fromCharCode(8205);
const UFE0Fg = /\uFE0F/g;

/**
 * Calculates the icon code based on the character.
 *
 * @param {string} char - The character to calculate the icon code for.
 * @return {string} The icon code calculated based on the character.
 */
function getIconCode(char: string): string {
  return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, "") : char);
}

/**
 * Converts a string of Unicode surrogates into a string of hexadecimal code points.
 *
 * @param {string} unicodeSurrogates - The string of Unicode surrogates to convert.
 * @return {string} The string of hexadecimal code points.
 */
function toCodePoint(unicodeSurrogates: string): string {
  const r = [];
  let c = 0,
    p = 0,
    i = 0;

  while (i < unicodeSurrogates.length) {
    c = unicodeSurrogates.charCodeAt(i++);
    if (p) {
      r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));
      p = 0;
    } else if (55296 <= c && c <= 56319) {
      p = c;
    } else {
      r.push(c.toString(16));
    }
  }
  return r.join("-");
}

/**
 * Generates a URL for a Twemoji SVG image based on the given code.
 *
 * @param {string} code - The code representing the Twemoji.
 * @return {string} The URL of the Twemoji SVG image.
 */
export const apis = {
  twemoji: (code: string) =>
    "https://cdnjs.cloudflare.com/ajax/libs/twemoji/15.1.0/svg/" +
    code.toLowerCase() +
    ".svg",
  openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",
  blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",
  noto: "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",
  fluent: (code: string) =>
    "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
    code.toLowerCase() +
    "_color.svg",
  fluentFlat: (code: string) =>
    "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
    code.toLowerCase() +
    "_flat.svg",
};

/**
 * Asynchronously loads emoji based on the provided code and type.
 *
 * @param {string} code - The code representing the emoji.
 * @param {keyof typeof apis} [type] - The type of emoji API to use.
 * @return {Promise<string>} The loaded emoji as text.
 */
async function loadEmoji(
  code: string,
  type?: keyof typeof apis,
): Promise<string> {
  if (!type || !apis[type]) {
    type = "twemoji";
  }

  const api = apis[type];
  if (typeof api === "function") {
    const r = await fetch(api(code));
    return await r.text();
  }
  const r_1 = await fetch(`${api}${code.toUpperCase()}.svg`);
  return await r_1.text();
}

/**
 * Asynchronously loads an additional asset based on the provided code and segment.
 *
 * @param {string} code - The code representing the asset.
 * @param {string} segment - The segment of the asset.
 * @return {Promise<string>} The loaded asset code or the asset itself as a data URL.
 */
async function loadAdditionalAsset(
  code: string,
  segment: string,
): Promise<string> {
  // console.log(code, segment, Buffer.from(segment).toString('base64'));
  // console.log(getIconCode(segment));
  // console.log(await loadEmoji(getIconCode(segment)));
  if (code === "emoji") {
    return (
      `data:image/svg+xml;base64,` + btoa(await loadEmoji(getIconCode(segment)))
    );
  }

  return code;
}

export const onGet: RequestHandler = async ({ cacheControl, send, url }) => {
  // Disable caching
  cacheControl("no-cache");

  // Get data from search params
  const title = url.searchParams.get("title");
  const description = url.searchParams.get("description");
  const path = url.searchParams.get("path");

  // Create icon and font directory URL
  const iconUrl = import.meta.env.PUBLIC_WEBSITE_URL + "/pwa-192x192.png";
  const fontDirUrl = import.meta.env.PUBLIC_WEBSITE_URL + "/fonts";

  // Create Lexend 400 font object
  const lexend400 = {
    name: "Lexend",
    data: await fetchFont(fontDirUrl + "/lexend-400.ttf"),
    style: "normal",
    weight: 400,
  } as const;

  // Create Lexend 500 font object
  const lexend500 = {
    name: "Lexend",
    data: await fetchFont(fontDirUrl + "/lexend-500.ttf"),
    style: "normal",
    weight: 500,
  } as const;

  // Create Lexend Exa 500 font object
  const lexendExa500 = {
    name: "Lexend Exa",
    data: await fetchFont(fontDirUrl + "/lexend-exa-500.ttf"),
    style: "normal",
    weight: 500,
  } as const;

  // If title is available, return image with text
  if (title) {
    send(
      new ImageResponse(
        html`
          <div
            tw="flex h-full w-full flex-col justify-between bg-indigo-900 p-16"
            style="font-family: 'Lexend';background-image: linear-gradient( 120deg, #0f172a 15%, #172554 );"
          >
            <div tw="flex items-center justify-between">
              <div tw="flex items-center">
                <img tw="w-16 h-16" src="${iconUrl}" />
                <div tw="flex items-end">
                  <div tw="text-5xl font-medium text-slate-300 ml-2">Qit.</div>
                  <div class="ml-1 flex pb-0 text-base text-slate-300">
                    tools
                  </div>
                </div>
              </div>
              <div
                tw="max-w-[70%] text-3xl text-slate-400"
                style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
              >
                qit.tools${path ? path : ""}
              </div>
            </div>
            <div tw="flex flex-col">
              <h1
                tw="max-w-[90%] text-5xl font-medium leading-normal text-slate-200"
                style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
              >
                ${title}
              </h1>
              <p
                tw="text-3xl text-slate-400 leading-normal"
                style="${description ? "" : "display: none"}}"
              >
                ${description
                  ? description.length > 170
                    ? description.slice(0, 170).trimEnd() + "..."
                    : description
                  : ""}
              </p>
            </div>
          </div>
        `,
        {
          width: 1200,
          height: 630,
          fonts: [lexend400, lexend500, lexendExa500],
          loadAdditionalAsset,
        },
      ),
    );

    // Otherwise, return image just with logo
  } else {
    send(
      new ImageResponse(
        html`
          <div
            tw="flex h-full w-full items-center justify-center bg-slate-900"
            style="font-family: 'Lexend Exa';background-image: linear-gradient( 120deg, #0f172a 15%, #172554 );"
          >
            <div tw="flex items-center">
              <img tw="w-36 h-36" src="${iconUrl}" />
              <div tw="flex items-end">
                <div tw="text-9xl font-medium text-slate-300 ml-10">Qit.</div>
                <div class="ml-2 flex pb-2 text-2xl text-slate-300">tools</div>
              </div>
            </div>
          </div>
        `,
        {
          width: 1200,
          height: 630,
          fonts: [lexendExa500],
          loadAdditionalAsset,
        },
      ),
    );
  }
};

Enter fullscreen mode Exit fullscreen mode

Top comments (0)