OpenGraph 图自动生成

OpenGraph 图自动生成

April 28, 2024
编码 , 分享 , 工具箱 ,

什么是 OpenGraph

💡
以下介绍由 Kimi 生成。
大白话就是:这些数据专门告诉社交平台,我们这个网页的基础信息等数据。

OpenGraph 是一种由 Facebook 开发的元标签(meta tags),用于网页上的社交分享。当一个网页链接被分享到社交媒体平台时,这些标签可以提供额外的信息,比如网页的标题、描述、图片等,从而在分享时创建一个更丰富、更吸引人的预览。

OpenGraph 标签通常放在 HTML 页面的 <head> 部分,它们允许社交媒体平台了解页面内容,以便在用户分享链接时生成一个预览。一个典型的 OpenGraph 标签集可能包括:

<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="http://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="http://ia.media-imdb.com/rock.jpg" />
<meta property="og:description" content="A group of U.S. Marines, under command of a renegade general, take over Alcatraz and threaten San Francisco Bay with biological weapons." />
<meta property="og:site_name" content="IMDb" />
  • og:title 指定了分享内容的标题。
  • og:type 描述了内容的类型。
  • og:url 提供了内容的网址。
  • og:image 指定了分享时使用的图片。
  • og:description 描述了内容的简短介绍。
  • og:site_name 指定了分享内容的网站名称。

这些标签对于社交媒体优化(Social Media Optimization, SMO)和提高网页在社交网络上的可见性非常重要。

起因

其中 og-image 部分为了分享到社交媒体时能展示更多的信息和更美观,一些博主们会制作专门的 OG 图片,甚至还有专门生成 OG 图片的工具,如:picprosecover-paint 等。

不过我觉得每次都要手动制作着实是不太方便,尤其是对于我这种懒人来说,而且如果以前的文章多的话制作起来更是一个大工程了。

所以,有没有简单的工具可以完成这件事情呢?

不用多说,肯定是有的,而且这里又要提到 蜗牛老哥

是的,继昨天的热力图后,又一次受到蜗牛老哥传道分享,这次是一款利用 Vercel 自部署,自动根据参数信息生成 OG 图的 API 服务。

第一次看到这个项目是在蜗牛老哥的 部署动态生成 OG Image 的 API 一文,不过当时这个项目还是有缺陷的 —— 不支持中文字库,所有的中文都会变成豆腐块显示,虽然文中提到了可以动态压缩字体,但是当时我还没有开始使用 SSG 工具展示博客,这部分无法实现,所以在测试过几次后就没有再继续尝试了。

这次在热力图弄完之后忽然又想起这个项目来,感觉自己目前这个状态应该够实力完成这个功能了,索性就趁热打铁一起处理了。

依旧是折腾了一下午才弄好,最终在各个社交平台的具体预览效果如下:

我基本上完全参照了 Dayu 的设计样式

思路

我这次的大概思路是

  • 因为博客现在是 11ty,所以我可以在获取完文章数据后,将需要留下的字符串汇总。
  • 利用字体文件压缩库根据第一步获取的字符串精简。
  • 将字体文件上传到网站上去。
  • Vercel 上的服务通过 URL 请求精简后的字体文件,一般压缩后的字体只有几百 K,这个时候已经完全没有问题。。
  • 成功加载字体后就可以绘制 OG 图了。

实现

因为 Vercel 限制的缘故,在项目运行时无法加载太大的资源文件,所以我们需要将用不到的字体全部删除,这样字体文件自然就减少了,这里压缩字体的代码我参考的这位大佬的 生成动态字体文件

const Fontmin = require("fontmin");

// 接受一个需要留下的字的字符串
module.exports = (titleText) => {
    const fontmin = new Fontmin()
        // 字体的源文件
        .src("assets/SmileySans.ttf")
        .use(
            Fontmin.glyph({
                text: titleText,
                hinting: false,
            })
        )
        // 文件生成成后放在网站的资源目录中,Rollup在后面会复制到dist目录里去
        .dest("src/assets/fonts");
    // 开始精简字体
    fontmin.run((err, files) => {
        if (err) throw err;
        console.log("compress font success\n");
    });
};

定义压缩字体的函数

    // Get all posts
    config.addCollection("posts", async function (collection) {
        // 获取Ghost博客的所有文章
        collection = await api.posts
            .browse({
                include: "tags,authors",
                limit: loadData,
                order: "published_at desc",
                filter: "visibility:public",
            })
            .catch((err) => {
                console.error(err);
            });

        // 获取所有文章的tags字符串、title字符串、desc简介字符串
        const titleText = collection
            .map(function (item) {
                let tags = item.tags.map(tag => tag.name).join(',');
                let desc = item.excerpt != null ? item.excerpt.substring(0,100) : '';
                return item.title + desc +tags;
            })
            .join(",");
        // 调用之前定义的压缩函数,将所有文章的标题、tags、简介字符传进去
        fontmin(titleText);

        return collection;
    });

在我 11ty 的数据初始化过程中引用

如果是动态博客可能在这一部就要卡住了,因为我之前用 Ghost 的时候就是卡在这里,但是结合我最近折腾 Github Action 的经验,我这里给的一条可行的方案是:

  • 提供一个拥有全站文章标题、tags、简介的 rss 或 json 文件地址
  • 利用 Github Action 之类的工具在每次文章更新后及时重新生成字体文件。
  • Vercel 请求 Github Action 中新的字体文件。
之后如果有时间我再来试试看这个方式的可行度。

Vercel 上的部分

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';

export const config = {
    runtime: 'edge',
};

export default async function handler(request: NextRequest) {
    try {
        // 请求压缩好的字体文件到缓存中来
        const fontData = await fetch(
            new URL('https://1900.live/assets/fonts/SmileySans.ttf', import.meta.url),
        ).then((res) => res.arrayBuffer());

        const { searchParams } = new URL(request.url);

        // 获取url参数
        const originalTitle = searchParams.get('title');
        const originalTags = searchParams.get('tags');
        const originalDesc = searchParams.get('desc');
        const originalDate = searchParams.get('date');

        let title = originalTitle;
        let tags = originalTags;
        let desc = originalDesc;
        let date = originalDate;

        // 设置各项参数的默认值和长度处理
        if (!title) {
            title = 'A Hugo blog about Charles Chin.';
        } else {
            let dot = title.length >= 20 ? '...':'';
            title = title.slice(0, 22) + dot;
        }

        if (!tags){
            tags = '分享';
        }else{
            tags = tags.slice(0, 10);
        }

        if(!desc){
            desc = 'All work and no play makes Jack a dull boy'
        }else{
            desc = desc.slice(0, 90) + '...';
        }

        if(!date){
          date = '1900/01/01';
        }

        // 绘制图像
        return new ImageResponse(
            (
                <div style={{
                    display: 'flex',
                    flexDirection: 'column',
                    height: '400px',
                    width: '800px',
                    backgroundColor: 'white',
                    border: 'solid 1px',
                    justifyContent: 'flex-end'
                  }}>
                    <div style={{
                      display: 'flex',
                      justifyContent: 'center'
                    }}>
                      <span style={{
                        backgroundColor: 'red',
                        padding: '5px 15px',
                        color: 'white',
                        borderRadius: '5px',
                        fontStyle: 'italic',
                        letterSpacing: '3px'
                      }}>
                        {tags}
                      </span>
                    </div>
                    <div style={{
                      display: 'flex',
                      padding: '40px 0px 25px 0px',
                      minHeight: '210px',
                      flexDirection: 'column',
                      alignItems: 'center',
                    }}>
                      <p style={{
                        fontSize: '50px',
                        fontWeight: 'bolder',
                        margin: '0',
                        fontStyle: 'italic',
                        textAlign: 'center',
                        lineHeight: '1.2',
                        padding: '0 35px'
                      }}>
                        {title}
                      </p>
                      <p style={{
                        fontSize: '28px',
                        fontWeight: 'bolder',
                        marginTop: '40px',
                        fontStyle: 'italic',
                        textAlign: 'center',
                        padding: '0 35px',
                        color: '#8b949e',
                        textIndent: '2em'
                      }}>
                       「   {desc}    」
                      </p>
                    </div>
                    <div style={{
                      display: 'flex',
                      justifyContent: 'space-between',
                      marginBottom: '16px'
                    }}>
                      <div style={{
                        display: 'flex',
                        marginLeft: '20px'
                      }}>
                        <img src="https://cdn.1900.live/20190640/ico.png" style={{
                          width: '50px',
                          height: '50px',
                          borderRadius: '50%'
                        }} alt="icon" />
                        <div style={{
                          display: 'flex',
                          flexDirection: 'column',
                          lineHeight: '1',
                          justifyContent: 'center',
                          fontSize: '14px',
                          fontStyle: 'italic',
                          fontWeight: '900',
                          marginLeft: '13px'
                        }}>
                          <span style={{
                            fontSize: '20px'
                          }}>
                            @1900
                          </span>
                          
                        </div>
                      </div>
                      <div style={{
                        display: 'flex',
                        alignItems: 'center'
                      }}>
                        <span style={{
                          fontSize: '20px',
                          marginRight: '20px',
                          fontWeight: 'bold',
                          fontStyle: 'italic'
                        }}>
                          {date}
                        </span>
                      </div>
                    </div>
                    {/* The last div is empty and has no content or styles, so it's not included in the JSX */}
                  </div>
            ),
            {
                width: 800,
                height: 400,
                fonts: [
                    {
                        name: 'SmileySans',
                        data: fontData,
                        style: 'italic',
                    },
                ],
            },
        );
    } catch (e: any) {
        console.log(`${e.message}`);
        return new Response(`Failed to generate the image`, {
            status: 500,
        });
    }
}

这部分代码没怎么动脑子,感觉写的很傻

如果你喜欢我这个样式可以直接 Fork 我的项目部署即可。

一切完成后使用 https://verceldomain.com/api/og 并附带对应的 title、tags、desc、date 调用即可,这里的各个参数一定要 encode 一下,避免出问题。

还有就是,有条件的一定要绑个域名,默认域名好像已经被墙了,无法访问。

示例:

https://og.190102.xyz/api/og?title=%E5%8D%9A%E5%AE%A2%E6%9B%B4%E6%96%B0%E7%83%AD%E5%8A%9B%E5%9B%BE+-+%E5%8F%AA%E6%98%AF%E7%8E%A9%E7%8E%A9+%7C+All+work+and+no+play+makes+Jack+a+dull+boy&desc=%E8%BF%99%E4%B8%AA%E5%8A%9F%E8%83%BD%E6%9C%80%E6%97%A9%E6%98%AF%E7%9C%8B%E5%88%B0%E6%A4%92%E7%9B%90%E8%B1%86%E8%B1%89+%E5%A6%82%E4%BD%95%E7%BB%99+Hugo+%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E7%83%AD%E5%8A%9B%E5%9B&date=2024-04-27

这里一定要将传递的参数 encode 一下

TODO

  • 后续看如何加上图片背景
  • 图片目前访问有点慢,有必要在 11ty 生成阶段将图片只是存在本地吗?

分享个 Recat 排版小技巧

因为 vercel 中绘制图片使用的是 React 的,而我又没学过,也不太想深入去学习,所以刚开始画布局的时候各种苦手,不得要领。

后来发现 React 的结构、样式好像和 Html 差不多,所以我就尝试着先在​网页上将大概的布局用熟悉的 HTML 画出来,再将得到的 Html 通过 ChatGPT 转换成成 React 代码,再使用 Vercel 提供的 https://og-playground.vercel.app/ 工具调整细节。

整个排版过程一下子就轻松、愉快起来了😄。

加入评论