国庆期间经过一番折腾,将原Hexo上的博客迁移到Astro上了,这里记录一下
迁移过程#
本网站依赖以下优秀项目的技术支持:
cworld1 / astro-theme-pure
Waiting for api.github.com...
???
???
???
walinejs / waline
Waiting for api.github.com...
???
???
???
lyc8503 / UptimeFlare
Waiting for api.github.com...
???
???
???
lyc8503 / onedrive-cf-index-ng
Waiting for api.github.com...
???
???
???
得益于优秀的开源项目和详尽的文档,在迁移时并未遇到什么阻碍,以下主要记录一些较为麻烦或新添加的功能。
Aliyun自建图床#
因为此网站托管在Vercel ↗上,本地存储图片可能加载速度较慢,之前试过各种第三方图床经常崩掉跑路
所以这里通过阿里云OSS存储服务+PicGo实现自建图床 ,再也不用担心丢图片了(? ,对于个人博客来说,选用量计费充十几块钱备用即可
- 在官网开启OSS存储服务 ↗
- 创建Bucket

在对象存储->Bucket列表->[你的Bucket名]->概览中找到访问端口备用
- 在
在对象存储->Bucket列表->[你的Bucket名]->读写权限中修改为公共读
- 右上角头像->
AccessKey->RAM访问控制创建用户,勾选使用永久AccessKey访问,记录ID和Secret备用,配置OSS访问权限 - 下载
PicGO图床并按文档配置
Molunerfinn / PicGo
Waiting for api.github.com...
???
???
???
- 之后在Typora中配置自动图片上传

waline + Supabase#
waline ↗的配置文档已经十分详细,此主题的集成也已经很完善。但推荐绑定的数据库服务LeanCloud好像已不支持未备案域名,这里用Supabase代替,类似
- 进入supabase ↗官网新建项目

- 导入
PostgreSQL的waline.pgsql ↗创建好表和表结构
CREATE SEQUENCE wl_comment_seq;
CREATE TABLE wl_comment (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_comment_seq'),
user_id int DEFAULT NULL,
comment text,
insertedAt timestamp(0) without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip varchar(100) DEFAULT '',
link varchar(255) DEFAULT NULL,
mail varchar(255) DEFAULT NULL,
nick varchar(255) DEFAULT NULL,
pid int DEFAULT NULL,
rid int DEFAULT NULL,
sticky numeric DEFAULT NULL,
status varchar(50) NOT NULL DEFAULT '',
"like" int DEFAULT NULL,
ua text,
url varchar(255) DEFAULT NULL,
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ;
CREATE SEQUENCE wl_counter_seq;
CREATE TABLE wl_counter (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_counter_seq'),
time int DEFAULT NULL,
reaction0 int DEFAULT NULL,
reaction1 int DEFAULT NULL,
reaction2 int DEFAULT NULL,
reaction3 int DEFAULT NULL,
reaction4 int DEFAULT NULL,
reaction5 int DEFAULT NULL,
reaction6 int DEFAULT NULL,
reaction7 int DEFAULT NULL,
reaction8 int DEFAULT NULL,
url varchar(255) NOT NULL DEFAULT '',
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ;
CREATE SEQUENCE wl_users_seq;
CREATE TABLE wl_users (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_users_seq'),
display_name varchar(255) NOT NULL DEFAULT '',
email varchar(255) NOT NULL DEFAULT '',
password varchar(255) NOT NULL DEFAULT '',
type varchar(50) NOT NULL DEFAULT '',
label varchar(255) DEFAULT NULL,
url varchar(255) DEFAULT NULL,
avatar varchar(255) DEFAULT NULL,
github varchar(255) DEFAULT NULL,
twitter varchar(255) DEFAULT NULL,
facebook varchar(255) DEFAULT NULL,
google varchar(255) DEFAULT NULL,
weibo varchar(255) DEFAULT NULL,
qq varchar(255) DEFAULT NULL,
"2fa" varchar(32) DEFAULT NULL,
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ;sql- 按文档或下图设置好环境变量,Waline 会自动根据你配置的环境变量切换到对应的数据存储服务。
Cloudflare CDN加速#
因为国内厂商的CDN加速都需要域名备案,这里使用Cloudflare代替
正常注册Cloudflare绑定域名即可,将DNS服务器改为Cloudflare提供的
可以参考这篇博客 ↗,细节不再赘述
文章加密功能#
为主题添加一个文章加密功能
./src/content/content.config.ts
content.config.ts
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
language: z.string().optional(),
draft: z.boolean().default(false),
// Special fields
comment: z.boolean().default(true),
password: z.coerce.string().optional(),
})
})js./src/utils/encrypt.ts
encrypt.ts
// 加密函数
export async function encrypt(text: string, password: string): Promise<string> {
const key = password.length >= 16 ? password.slice(0, 16) : password.padEnd(16, '0');
const iv = crypto.getRandomValues(new Uint8Array(16));
const keyBuffer = new TextEncoder().encode(key);
const textBuffer = new TextEncoder().encode(text);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC', length: 128 },
false,
['encrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
textBuffer
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
// 处理大数据时避免调用栈溢出
let binary = '';
for (let i = 0; i < combined.length; i++) {
binary += String.fromCharCode(combined[i]);
}
return btoa(binary);
}
// 解密函数
export async function decrypt(encryptedData: string, password: string): Promise<string> {
const key = password.length >= 16 ? password.slice(0, 16) : password.padEnd(16, '0');
// 解码 base64 字符串到 Uint8Array
const binaryString = atob(encryptedData);
const combinedData = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
combinedData[i] = binaryString.charCodeAt(i);
}
const iv = combinedData.slice(0, 16);
const encrypted = combinedData.slice(16);
const keyBuffer = new TextEncoder().encode(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC', length: 128 },
false,
['decrypt']
);
const decryptedData = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
encrypted
);
return new TextDecoder().decode(decryptedData);
}js./src/components/PasswordProtected.astro
./src/layouts/BlogPost.astro
BlogPost.astro
<Hero {data} {remarkPluginFrontmatter} slot='header'>
<Fragment slot='description'>
{!isDraft && enableComment && <PageInfo comment class='mt-1' />}
</Fragment>
</Hero>
<slot />
{password ? (
<PasswordProtected password={password}>
<slot />
</PasswordProtected>
) : (
<>
<slot />
</>
)} astro示例: CSS太难了

其他问题#
一些Bug:
- waline经
Cloudflare代理不显示评论者IP - 文章内容过短时出现阅读进度为负数
- 加密文章解锁后,大纲导航功能失效
