在Astro中使用Satori、Sharp和Astro端点设置静态OG(Open Graph)图像

你是否将链接分享给朋友,并想知道预览图片是从哪里来的? 那就是 Open Graph Protocol,这是一组可丰富链接的HTML <meta> 扩展,最初由Facebook发明。 本博客文章描述了如何在构建时生成这些图片,使用的是 Astro web framework

大多数现有的指南(包括Vercel官方的 OG Image Generation)都使用函数动态生成OG图像。 虽然这种方式没有问题,但我真的希望我的图像在构建时静态生成;这样更快、更便宜,也更酷。

构建你的图片

首先要做的是弄清楚你的OG图像应该是什么样子的。 我们将使用Vercel的 Satori,这是一个可以将HTML树渲染成SVG的JavaScript库。 Vercel还提供了一个很棒的 OG Image Playground,在这里你可以玩耍并快速获得结果。

OG Image Playground的屏幕截图,左边是代码编辑器,右边是预览和一些设置。

将大小设置为1200×630px,因为这是Facebook在 他们的指南 中推荐的大小。 在布局时大量使用Flexbox,并在确定布局时启用调试模式。 A Complete Guide to Flexbox是一个很棒的资源。

创建一个Astro端点

在Playground中构建了一张漂亮的图片?那么让我们开始吧。 安装Satori以生成SVG和 sharp 以将其转换为PNG:

$ npm install satori sharp

然后创建一个端点,例如 pages/og-image.png.ts,其中包含以下代码:

import fs from "fs/promises";
import satori from "satori";
import sharp from "sharp";
import type { APIRoute } from 'astro';

export const get: APIRoute = async function get({ params, request }) {
  const robotoData = await fs.readFile("./public/fonts/roboto/Roboto-Regular.ttf");

  const svg = await satori(
    {
      type: "h1",
      props: {
        children: "Hello world",
        style: {
          fontWeight: "bold"
        }
      }
    },
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Roboto",
          data: robotoData,
          weight: "normal",
          style: "normal",
        },
      ],
    }
  );

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

  return new Response(png, {
    headers: {
      "Content-Type": "image/png",
    },
  });
}

你可以在上面的代码中看到一个缺点,即Astro不支持TSX端点,所以我们需要使用类似React元素的对象。

你还需要始终提供一个字体,因为它将被嵌入到图像中。 上面的示例中我使用了 RobotoInterOpen Sans 是其他可靠的免费无衬线字体(选择WOFF或TTF/OTF,不支持WOFF2)。

运行 astro dev 并导航到端点(在我们的示例中为 :3000/og-image.png),查看生成的图像。

为集合中的每个项目生成图像

一旦你在函数中找到喜欢的图像,你可以为整个集合批量创建它们。 假设你有一个 blog 集合,你的博文位于 pages/blog/:slug/index.astro。 创建一个 pages/blog/:slug/og-image.png.ts,其中包含你的API路由和导出的 getStaticPaths 函数:

import { getCollection, getEntryBySlug } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}

export const get: APIRoute = async function get({ params, request }) {
  // ...
}

现在运行 astro build,你将看到它为你站点上的每篇博文静态生成了一个OG图像。

处理图片

图片(如字体)需要嵌入到SVG中。 我找到的最简单的方法是使用 data urls, 首先将文件读取为Base64字符串,例如:

const myImageBase64 = (await fs.readFile("./public/my-image.png")).toString("base64");

然后在Satori中将其设置为 backgroundImagesrc 属性,例如:

{
  type: "div",
  props: {
    style: {
      backgroundImage: `url('data:image/png;base64,${myImageBase64}')`,
    }
  }
}

请注意,Satori不支持 backgroundSize: cover,所以如果你有这种情况,你需要使用 image-size 和一些数学计算来自己构建它。

在HTML中设置OG标签

现在只剩下一件事要做,就是在你的 <head> 中链接到你的OG图像。 有两个属性你可能想在图像中使用:og:imagetwitter:image

<meta property="og:image" content="/blog/og-image.png" />
<meta property="twitter:image" content="/blog/og-image.png" />

在集合中使用动态路径,自动使用正确的图像。 请查看 Open Graph protocol 了解更多关于Open Graph元扩展的信息。

进一步链接和总结

如果你想看到实际的工作代码(我知道我经常这样做),请查看驱动我书评OG图像的端点: pages/books/[...slug]/og-image.png.ts。 这是它的样子:书评的OG图像

我有遗漏的地方吗?这个工作流程还能进一步改进吗? 给我发邮件 或者在Fediverse中@我, 我很想听听你的想法。