博客折腾日记
翻了下git
记录,基于Astro
的第一版博客大致完成与去年七月中旬,修修补补到23年末算是有了雏形。
从hello world
到几个核心栏目的完成,过程中自己新认识了很多朋友,交流中对博客和搭建博客这件事也有了很多新的想法。
为什么选择 Astro
其实告别Hugo之后,我的第一个选择是Nuxt,期间还发了个帖子说说用 nuxt 写了个博客的体验,而告别Hugo 的原因是,模板语言虽然保持了打包的高效,但魔改起来实在是太费心力了,而且那个时期自己也没找到好用的IDE插件,自己也动过从头写一个Hugo 主题的念头,每次都以失败告终。
帖子中,Nuxt在开发中给的正反馈开始让我欲罢不能,但也有几个问题:
- 一个纯净美好的HTML页面,在各种Vue组件的包装下,查看网页源码的时候显得特别杂乱。
- SPA应用(它没错但我不太喜欢)。
最后,被Astro页面的2023 Web Framework Performance Report唬住了。
简单使用后,不管是文档还是IDE的开发体验,都没啥大问题,算是正式入坑了,当时Astro还在快速迭代,也迎合了我折腾的欲望。
为什么喜欢 bearblog
很早之前,查资料找到这个博主Mike - Line Simplification,从这既偷到了页面简洁素雅的设计,同时也学习到了D3.js,收获颇丰。
之后,从Hacker News - Bear Blog – A privacy-first, fast blogging platform上看到bearblog,就一眼爱上了,之前Hugo的博客也是基于bearblog的主题。
简洁的页面设计和网页原生的按钮直戳我心,页面尽量只有内容,甚至连导航栏都藏起来,都是源自这儿。
为了简洁做的事
这段故事,自己也发了个帖子,突然感觉 tailwindcss 不香了。
用 astro 做了一个静态网站,内容主要是文字为主。 当时用 tw 的时候是提高生产力为主,比如 light/dark 转换,prose 排版等等。 现在功能基本完成,想做一些优化的时候,发现某篇文章的 index.html: 总大小 ~ 78kb ,移除 tw 声明的变量和 class 定义之后 大小只有~ 24kb 。。。 尝试用 purgecss ,作用不是很明显(可能姿势不对)?
自己既想使用tailwindcss的便捷性,又不想为此付出这么大的代价(接近三倍的体积),有V友建议了Unocss。
迁移起来没太大的问题,用法基本兼容tailwindcss,又回头看了下自己,首页单页面的体积只有11kb
,此番折腾自己觉得还是值得的。
自己喜欢的多语言组织方式
在折腾多语言这一部分的时候,自己一直在动摇,一方面确实挺费劲的,另一方面,自己也忍不住的问自己,真的有英语为母语的朋友看我的博客吗?
最后,向往折腾的自己打败了“不自信”的自己,这个功能还是做出来了。
虽然目前Astro
已经支持了多语言,但我还是在用第三方的插件astro-i18n-aut。
原因是,官方的多语言,不管是组件还是内容,都需要组织在不同的目录下,而我不喜欢这种设计。
我的多语言的实现大概分两个部分,一是多语言的文本,用YAML
存储,虽然这样需要多安一个包 @rollup/plugin-yaml
,但我实在不喜欢JSON
。
layout: title: 陈昱行博客 description: 陈昱行的个人博客,在没人看到的地方写写画画。少些技术,多些生活。这里自己的学习生活,偶尔分享一下自己对这个世界的看法。 nav: posts: 随笔 footer: home: 首页 rss: 订阅 timeline: 时间线
第二部分,是内容的部分,考虑到自己会有懒的时候,所以无法保证所有的内容都有两种语言,而我想不管在中文还是英文界面都显示所有的内容。
于是想到了这么一种设计,用扩展后缀的方式区分中英文内容,英文内容以.en.md
或者.en.mdx
结尾,如果有指定的内容则显示,否则fallback回中文的内容。
在frontmatter里强制增加英文标题,这样可以保证至少首页做到都是英文,看起来舒服一些。
闲话从伪动态到真动态
闲话这个栏目,起源于叽喳,后来迫于不太稳定,也想把数据掌握在自己手里,自己做了个类似的东西。live
这次借着博客重构的机会,把这部分内容也集成了进来。
开始之初,有两个方案:
- 像之前一样,使用leancloud存储数据。
- 将这部分内容也做成静态的方式,存在一个文本文件里。
最后调研了一圈,发现了cloudflare D1
,好消息是Free Plan
也可以用,于是也没再纠结。
DROP TABLE IF EXISTS moments; CREATE TABLE IF NOT EXISTS moments ( id integer PRIMARY KEY AUTOINCREMENT, body text NOT NULL, tags text WITH NULL, star integer NOT NULL default 0, created_at text NOT NULL deleted_at text WITH NULL ); CREATE INDEX idx_moments_created_at ON moments (created_at);
使用cloudflare worker
做了一个简单的接口,调取数据。
在客户端,通过SSR的方式拉取数据,并分页渲染,这样好处是不对外暴露接口,坏处是如果有人想刷某个接口,直接刷该网页我也没啥办法。
基于TMDB的影视评价
其实本意还是想通过,imdb或者豆瓣来做,一方面可以复用客户端,二是内容很全。
奈何两者的接口反爬都太严格了,完全没有角度,自己还尝试了通过headless的方式访问IMDB的rating
导出csv
的接口,以失败告终,值得选择了TMDB。
自己维护的好处是可以与自己的闲话
互动,将自己的影视评论与具体的闲话
条目联动。
DROP TABLE IF EXISTS reviews; CREATE TABLE IF NOT EXISTS reviews ( id integer PRIMARY KEY AUTOINCREMENT, imdb_id text NOT NULL, title text NOT NULL, title_en text NOT NULL, media_type text NOT NULL, imdb_rating real NOT NULL, rating real NOT NULL, release_date text NOT NULL, rated_date text NOT NULL, moments_id integer NOT NULL, created_at text NOT NULL, deleted_at text WITH NULL );
短链
聊胜于无的功能,形式大于内容,大致思路如下:
- 在站点构建完成阶段,加入一段hook,为数据库中不存在的链接新建短链。
- 增加[id]路由,处理短链。
uuid
短链由一个固定前缀和三位随机数构成,对自己来说完全是够用的。
- [b] 开头代表是博客的链接
[o] 开头代表是草稿本的链接
import shortUUID from "short-uuid";
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const short3 = shortUUID(characters);
export function getOpenUUID(exist: string[]) { // generate a v4 uuid, subtract the first 3 characters, and then convert to base62 let uuid = 'o' + short3.generate().slice(0, 3); while (exist.includes(uuid)) { uuid = 'o' + short3.generate().slice(0, 3); } return uuid }
短链持久化
这部分也犹豫了好久,直接用vercel kv
,每月免费额度有限,最终还是结合了cloudflare D1
:
DROP TABLE IF EXISTS shorts; CREATE TABLE IF NOT EXISTS shorts ( id text PRIMARY KEY NOT NULL, url text NOT NULL, created_at text NOT NULL, deleted_at text WITH NULL ); CREATE INDEX idx_shorts_id ON shorts (id);
构建短链路由
完事具备,只需要做一个解析短链的路由即可大功告成。
import { createClient, kv as prodKV } from '@vercel/kv'
const notFound = new Response(null, {
status: 404,
statusText: 'Not found'
})
const getLink = async (id) => {
const kv = DEV
? createClient({
url: KV_REST_API_URL,
token: KV_REST_API_TOKEN
})
: prodKV
const cached = await kv.get(id)
if (cached) {
return cached
}
const apiURL = api.blog.com
const headers = {
Authorization: Bearer ${BLOG_API_SECRET}
}
const res = await fetch(apiURL, { headers })
if (res.status === 404) {
return null
}
const json = await res.json()
const url = json.url
await kv.set(id, url)
return url
}
export const GET = async ({ params, redirect }) => {
const { id } = params
const type = id?.slice(0, 1)
if (!type || !['o', 'b'].includes(type)) {
return notFound
}
const link = await getLink(id)
if (!link) {
return notFound
}
return redirect(link, 308)
}
代码块
也是一波三折,最开始看上了code-hike,无奈因为#255, 一直没用上。
后来参考Highlight a line on code block with Astro,迁移了部分样式,其实也挺好看的,不过自己处理的样式有点复杂,感觉污染了CSS。
最后看到starlight - expressive code 的解决方案,解决了几乎所有的痛点,样式复制过来也没问题。
import expressiveCode from 'astro-expressive-code' import {ExpressiveCodeTheme} from '@expressive-code/core' import {readFileSync} from 'fs' import {parse} from 'jsonc-parser'
const nightOwlDark = new ExpressiveCodeTheme( parse(readFileSync('./src/styles/expressive-code/night-owl-dark.jsonc', 'utf-8')) ) const nightOwlLight = new ExpressiveCodeTheme( parse(readFileSync('./src/styles/expressive-code/night-owl-light.jsonc', 'utf-8')) )
// 插件配置 ··· expressiveCode({ themes: [nightOwlDark, nightOwlLight], themeCssSelector: (theme) => { return '.' + theme.type } }) ···
component or directive?
最开始的时候,采用的是组件的形式增加提示,但问题是仅仅因为这个组件,就需要将md
变为mdx
,觉得成本有点高,后面还是改为使用remark-directive。
:::note{.info} 提示:这是个提示 :::
使用的方式代码如下,使用方法参考remark-directive
的例子即可。
:::note{.info} 提示:这是个提示 :::
除此之外,有篇博客有嵌入B站视频的需求,于是也用remark-directive
来实现了。
export function RDBilibiliPlugin() {
return (tree, file) => {
visit(tree, function (node) {
if (
node.type === 'containerDirective' ||
node.type === 'leafDirective'
) {
if (node.name !== 'bilibili') return
const data = node.data || (node.data = {})
const attributes = node.attributes || {}
const bvid = attributes.id
if (!bvid) {
file.fail('Unexpected missing id
on youtube
directive', node)
}
data.hName = 'iframe'
//
data.hProperties = {
src: //player.bilibili.com/player.html?bvid=${bvid}&autoplay=0
,
width: '100%',
height: 400,
aspectRatio: '16 / 9',
// fit height
class: 'm-auto',
// height: 400,
frameBorder: 0,
allow: 'picture-in-picture',
allowFullScreen: true
}
}
})
}
}
为什么没有评论
看到一个博主,评论区是大大的 "请通过邮件联系" 的字,自己觉得很酷。
另一方面,是一个没想明白的问题,首先我不喜欢匿名评论,但如果采用认证的方式,不管是Github还是其他的第三方登录,还是仍然无法囊括所有的读者。
所以有事还是给我发邮件吧ღ( ´・ᴗ・` )比心
由博客框架到一项个人技能
从为了做博客接触Astro以来,从偏爱到现在选择它作为首选的静态构建工具,一切仿佛自然而然发生。
- 个人页 chenyuhang.com
- 文集 chenyuhang.cn
- 草稿本 open.yuhang.ch
Subscribe to my newsletter
Read articles from 陈昱行 | Yuhang Chen directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by