使用 Strapi 和 Astro 搭建静态博客
2025-04-11 22:08:46
在 Strapi 中创建集合类型
文章 Article 集合
名称 | 类型 |
---|---|
title | 文本 |
description | 文本 |
tags | 关联 Tag |
slug | 文本 |
content | 富文本编辑器 |
标签 Tag 集合
名称 | 类型 |
---|---|
title | 文本 |
slug | 文本 |
articles | 关联 Article |
在 Astro 中编写 Strapi 的 Loader
创建 Zod 类型声明
实现 Article
和 Tag
的类型声明
const Article = z.object({
id: z.string(),
documentId: z.string(),
title: z.string(),
description: z.string().nullable(),
slug: z.string().nullable(),
content: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
publishedAt: z.string(),
tags: z.array(reference('tag')),
tocHtml: z.string().nullable(),
contentHtml: z.string()
})
const Tag = z.object({
id: z.string(),
documentId: z.string(),
name: z.string(),
slug: z.string().nullable(),
articles: z.array(reference('article'))
})
Strapi Api 接口
使用 Astro 官方提供的包装函数
导出 fetchArticleApi
和 fetchTagApi
用来获取内容
请求参数 query
中的 populate
字段是为了获取关联集合类型的 documentId
export async function fetchArticleApi() {
return await fetchApi<Article[]>({
endpoint: 'articles',
query: {
'populate[tags][fields]': 'documentId'
},
wrappedByKey: 'data',
});
}
export async function fetchTagApi() {
return await fetchApi<Tag[]>({
endpoint: 'tags',
query: {
'populate[articles][fields]': 'documentId'
},
wrappedByKey: 'data',
});
}
创建文章 Loader
使用 documentId
作为 Astro 内容集合的 id
使用文章内容创建摘要 digest
判断是否新增,用于增量更新
使用 unified
将 Markdown
渲染成 Html
格式保存下来
tocList
是使用 toc
生成的文章目录,用于生成目录的 Html
最后先从 store
中删除历史文章,再添加进去
function strapiArticleLoader(): Loader {
return {
name: 'strapi-article-loader',
load: async (context: LoaderContext): Promise<void> => {
const articles = await fetchArticleApi();
for (const article of articles) {
const id = article.documentId;
const content = article.content ? article.content : "";
const digest = context.generateDigest(content);
const dataEntry = context.store.get(article.documentId);
if (dataEntry && dataEntry.digest === digest) {
context.logger.info(`跳过 ${article.slug ? article.slug : article.id} 博客渲染,博客未更新`);
continue;
} else {
context.logger.info(`渲染 ${article.slug ? article.slug : article.id} 博客`);
}
let tocList: List | undefined;
let tocHtml: string | null = null;
const contentFile = await unified()
.use(remarkParse)
.use(() => (tree: Root) => {
const result = toc(tree, { maxDepth: 2 });
tocList = result.map;
})
.use(remarkRehype)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings)
.use(rehypeShiki, { themes: { light: 'one-dark-pro', dark: 'one-dark-pro' } })
.use(rehypeStringify)
.process(content);
if (tocList) {
await unified()
.use(remarkRehype)
.run({ type: 'root', children: [tocList] })
.then((result) => {
tocHtml = unified()
.use(rehypeStringify)
.stringify(result);
})
}
const tags: string[] = [];
for (const tag of article.tags) {
tags.push(tag.documentId);
}
const data = await context.parseData({
id, data: {
id: article.id.toString(),
documentId: id,
title: article.title,
description: article.description,
slug: article.slug,
content: article.content,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
publishedAt: article.publishedAt,
tags: tags,
tocHtml: tocHtml,
contentHtml: contentFile.toString()
}
});
context.store.delete(id);
context.store.set({ id, data, digest });
}
}
}
}
创建标签 Loader
Tag
不需要渲染,每次全量更新
function strapiTagLoader(): Loader {
return {
name: 'strapi-tag-loader',
load: async (context: LoaderContext): Promise<void> => {
const tags = await fetchTagApi();
context.store.clear();
for (const tag of tags) {
const id = tag.documentId;
const articles: string[] = [];
for (const article of tag.articles) {
articles.push(article.documentId);
}
const data = await context.parseData({
id, data: {
id: tag.id.toString(),
documentId: tag.documentId,
name: tag.name,
slug: tag.slug,
articles: articles
}
});
context.store.set({ id, data });
}
}
}
}
最后导出文章和标签的内容集合
const article = defineCollection({
loader: strapiArticleLoader(),
schema: Article
});
const tag = defineCollection({
loader: strapiTagLoader(),
schema: Tag
});
export const collections = { article, tag };
实现 Webhook 服务
使用 express
实现一个监听 Strapi Webhook 的服务
import crypto from 'crypto';
import express from 'express';
import bodyParser from 'body-parser';
import { exec } from 'child_process';
const app = express();
let PORT = process.env.WEBHOOK_PORT;
let SECRET_TOKEN = process.env.WEBHOOK_TOKEN;
if (!PORT) {
PORT = "14321";
}
if (!SECRET_TOKEN) {
SECRET_TOKEN = crypto.randomBytes(32).toString('hex').toUpperCase();
console.log(`生成 Token: ${SECRET_TOKEN}`);
}
app.use(bodyParser.json());
app.post('/webhook', (req, res) => {
const authHeader = req.headers['authorization'];
if (!authHeader || authHeader !== `Bearer ${SECRET_TOKEN}`) {
console.warn(`无效的 Token: ${authHeader}`);
res.status(401).end();
return;
}
console.log('收到 Strapi Webhook 请求');
console.log(JSON.stringify(req.body, null, 2));
res.status(200).end();
console.log('正在执行 Astro 构建...');
exec('pnpm build', { cwd: "/app/astro" }, (error, stdout, stderr) => {
if (error) {
console.error(`构建失败: ${error.message}`);
return;
}
if (stderr) {
console.warn(`构建警告: ${stderr}`);
}
console.log(`构建成功: ${stdout}`);
});
});
app.listen(PORT, () => {
console.log(`Strapi Webhook 服务已启动, 监听端口 ${PORT}`);
});
在 Strapi 中添加 Webhook
Url
为 https://sssc.top:14321/webhook
标头密钥为 Authorization
,值为 Bearer
+ TOKEN