博客模板更新史 · 蛤克西欧卷 · 第二章

前两天给自己的博客做了一个非常微小的更新,如果你是从 Twitter 或者 Telegram 等正常渠道点进本文的话, 应该就能看到新的变化了:每一篇文章都有了一个看起来还不赖的缩图。老实讲这件事情本身实际上没什么技术 含量,但是折腾的过程还是挺有意思的,于是写一篇小文章记录一下。

其实每次在 Telegram 上水群看到 GitHub 项目的缩图心都会很痒,因为那么大的一张图明晃晃的摆在聊天信息 里面看起来就非常的醒目。可是 GitHub 的那个贴图太素了,不太符合我这种花哨的审美取向,于是我就打开了 Affinity,开始研究来做一个狂拽酷炫屌炸天的缩图。

这篇文章会简单介绍一下设计思路和工程实现方法,如果你也想要做类似的工作的话,可以参考一下喔!

设计

我面对的第一个问题是博客素材规格的问题,如你所见所有文章的题头图都是窄窄的一条带鱼图,然而社交网站 上的缩图尺寸则普遍接近 16:9,这就让事情变得有些难搞了:如果硬把一张图拉成大图,那么画面一定会糊掉, 而且内容会被裁得乱七八糟。为了处理这件事我的第一个想法是把图像的结构重新调整一下,参考 JetBrains 的题头图设计思路,让整张图能够完整呈现,还可以体现出某种有韵律的动态感。

当时脑袋里面构想的图片大概如下图所示,一张长条图被折叠成了三段放在图像的右侧作为装饰,折叠部分的阴影 使用了青色来确保画面不会太暗。背景使用了我一贯喜欢的铁青色,这个颜色也是本博客暗色模式下的背景色, 这样的设计可以帮助我统一视觉系统的一致性。

JetBrain 的题头图设计JetBrain 的题头图设计
我的第一个想法我的第一个想法

左下角是文字,为了给画面加一点引人注目的装饰,所以标题的第一行选择随机从 Material Design Classic 的色彩系统当中随机挑选一种颜色作为背景,这样整个图片看起来不会太无聊。为了区隔文本和背景,顺便加了一个 颜色很跳的阴影和边框,这里的选色和右面装饰图的阴影是一致的,这样的选择可以确保画面不是太花。因为画面 上已经没地方塞其他东西了,所以 Logo 被当成了阴影置于画面后方,算是强调一下个人 IP 了。

但是这个设计最大的问题是实现难度。对于大多数个人博客维护者来讲,手动给每篇文章做贴图是非常折磨人的, 所以我一定会尽可能的把这个过程自动化,但是这个设计稿想要通过某种自动化的方式绘制出来,其开发成本真的 太高了(对我来讲,折腾博客模板的时间如果超过了写文章时间的 20% 那这个时间成本就是不值当的)。我甚至还 没给那条带鱼图加三位透视,整张图的绘制过程就已经让人非常头痛,虽然说可以硬着头皮直接上,但我还是想要 留更多时间在创作内容上,于是这个设计想法被搁置了。

后来看到了另外一张图的设计稿重新点燃了我的灵感抄袭的欲望(哎呀,读书人的事情怎么能叫偷呢),这是 志祺七七频道在 YouTube 上的一个视频的缩略图,它 完美的符合了本博客奇葩题头图尺寸的需求,中间撕开的部分正好是一个长条形,可以把带鱼图嵌进去。

于是乎我火速做了一张样稿测试自己的想法,后来发现效果非常好。这张图可以完美的产生一种被遮盖的地方还有 内容的幻觉,因为内容堆叠的层次比较简单,所以用色和结构配比上也比较好掌控,所以最后就决定用这个模板了。 具体的做法也相对简单一些,纸张撕裂的边缘是从网上找的贴图。背景上除了铁青色之外覆盖了一层黑色的纸张 纹理让画面看起来更丰富一些,撕痕的部分则是使用了岩石的纹理,反正我不说你肯定不知道那个纹理是什么

志祺七七频道在 YouTube 上的一个视频的缩略图志祺七七频道在 YouTube 上的一个视频的缩略图
我的第二个想法我的第二个想法

最后右上角加上 Logo 强调 IP,左下角加一个大号 Logo 来丰富画面层次,适当的给画面点缀一些阴影,整张图就 做完了。虽然罗里吧嗦的说了一大堆,但实际上手画原型稿的时候其实只花了半个多小时。

工程

图像绘制

如果只是一些简单的缩图,实际上用 @vercel/og 或者 @napi-rs/canvas 来画都是可以的,但这张图的绘制 复杂程度显然已经有点超规格了,所以我选择了一个让自己比较舒适的方式进行绘制: puppeteer-core 这个东西实际上是做 E2E 测试用的, 但我非常喜欢误用它,比如我的简历就是用 Puppeteer 生成的 PDF,当然为了支持视觉回归,它也可以直接把页面 截取成图。

那么现在事情就变得很简单了,乖乖当个切图仔,把图片切成一层一层的,然后留一个空白的 div,调好位置,用容器 的盒模型把多余不要的图减裁掉。页面加载的时候读一下 Query Parameter 把数据填到 CSS 和 HTML 上,这个页面 就算渲染出来了。这个页面不需要开 HTTP 服务器,直接走 File Protocol 就可以加载,图片加载也是没问题的。

人生小提点:你并不总是需要开一个 HTTP 服务器来做一些简单的前端开发工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const $stripe = document.querySelector(".stripe");
const urlParams = new URLSearchParams(window.location.search);
const image = urlParams.get("image");

if (image) {
const $style = document.createElement("style");
$style.innerHTML = `
.stripe {
background-image: url("${
location.href.split("?")[0]
}/../../source/images/article_cover/${image}");
}
.text-line:first-of-type {
background-color: ${color};
}`;
document.body.appendChild($style);
}

接下来就要处理一些比较麻烦的事情了,文本的排版。实际上我最开始的想法是直接用 inline 文本来做,因为文本 左右有边缘,所以需要用 box-shadow 来给断行的文本左右加一个假的内边距。 一切看起来都很美好,但是直到我做到了「第一行文本的颜色随机」这个需求。实际上 CSS 的 first-line 选择器 并不支持 background 这个属性所以这条路子就此结束了。

考虑到这东西并不需要考虑排版性能的问题,所以就换了一个思路,先用 Intl.Segmenter 这个 API 给标题文本做 分词,在用 span 把每一个词包裹住,用 CSS 控制这个 span 内的文本一定不会被折行,最后就会得到一个多行文本。

接下来用 boundingBox API 算一遍分词完的文本一共有多少行,每行里面究竟有多少个词。最后用 JS 把每一行的 文本包裹在单独的 div 里,这样我们就可以给第一行的文本单独设置颜色了。

人生小提点:Material Design Classic 的色板是我用过的最好用的色板,基本上随便拉一个颜色出来糊到任意 一张图上都不会觉得突兀,如果你也在处理类似的问题,可以用 这个色板 试试看。

图像输出

最后一步就是图像输出啦,这个过程我将之称作「烘焙」。需要做的事情很简单,先检查一下这个图有没有生成过,如果 没有生成过的话就调用一个无头 Chromium 给页面做截图。实际上任何一个基于 Chromium 的浏览器都是可以喂给 puppeteer 做图像生成的,并不一定非得装个 Chrome,充满广告的 Edge 也是可以的,这里我使用的是 Vivaldi:

1
2
3
4
5
const browser = await puppeteer.launch({
executablePath:
"C:/Users/[YOUR NAME]/AppData/Local/Vivaldi/Application/vivaldi.exe",
defaultViewport: { width: 1200, height: 600 },
});

接下来就是简单的遍历所有 Markdown 文档,对每个文档生成图片了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const encodedTitle = encodeURI(title);
const encodedImage = encodeURI(image);
await page.goto(
`file:///${__dirname}/resource/coverTemplate.html?title=${encodedTitle}&image=${encodedImage}`,
{
waitUntil: "networkidle0",
timeout: 60000,
}
);

await page.screenshot({
// 输出地址是一个 PNG 文件的话,他就会渲染成 PNG 图片了。
path: outputPath,
});

然后把这个 Node 脚本放到 package.json 里,每次写完文章跑一边图像烘焙脚本,香喷喷的卡片就生成出来啦。 整体的视觉效果可以说是非常不错了!

最终的输出结果最终的输出结果

模板调整

最后就是调整 Hexo 的模板了,实际上 Hexo 内置了一个 Open Graph 元信息生成的函数, 但是它的默认输出和今天开发的目的有些不同。比如说,它默认会输出一系列的 og 标签(这不符合标准),标签 的内容是文章内的图片而不是我们预先烘焙的图片。另外,它默认输出的卡片格式并不是大号卡片而是小号的 「网站图标 + 文本概述」的样式,然而我们想要漂亮的大卡片,所以在这里我们需要调整一下模板的输出参数。

我们在 head.ejs 当中找到这一行:

1
<%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id}) %>

我们在里面加点料:

1
2
3
4
5
6
7
8
9
10
11
12
<%- open_graph({
twitter_id: theme.twitter,
google_plus: theme.google_plus,
fb_admins: theme.fb_admins,
fb_app_id: theme.fb_app_id,
image: is_post()
? [
full_url_for(`/images/og_cover/${pathToId(page.path), false}.png`)
]
: [],
'twitter:card': 'summary_large_image',
}) %>

我们在这里加了两个新的属性,一个是 image,它覆盖了默认的 OG 图像,将之替换为一个唯一值,即我们刚刚 烘焙出来的图像。这样在 Facebook 上就能正常显示大图了。但是 Twitter 和 Telegram 上需要额外的设置才能 展示大图,这个标签就是 twitter:card (是的,Telegram 实际上读的是 Twitter 的配置,实数 NTR 了)。

接下来在 Hexo 的开发服务器看一下有没有这些内容输出:

1
2
3
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://roriri.one/images/og_cover/xxxxxxx.png">
<meta property="og:image" content="https://roriri.one/images/og_cover/xxxxxxx.png">

如果能看到这三个 meta 标签,整个网站的配置就算是成功了。

从开始设计到完整做完应该是花了三四个小时,最后输出的结果还挺好看的,可以说是非常物有所值。希望这份简单的 小笔记能够给你的博客模板开发带来一些新的灵感。

以上就是今天的笔记啦,莉莉爱你 ♥~

Comments

No comments here,

Why not write something?