feat: 集成 Cloudflare 缓存清除功能并在文章变更时自动触发

This commit is contained in:
seahi committed 2026-09-25 08:54:39 +08:00
1 parent 198d4f5075
commit f340258afd
7 files changed
+177 -3

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function purgeCache(platform: App.Platform | undefined, paths: string[]) {
// @ts-expect-error Cloudflare env types
const token = platform?.env?.CF_API_TOKEN;
// @ts-expect-error Cloudflare env types
const zoneId = platform?.env?.CF_ZONE_ID;
const domain = platform?.env?.CF_DOMAIN; // 必须是带协议的绝对域名,如 https://blog.example.com
if (!token || !zoneId) {
console.warn('未配置 CF_API_TOKEN 或 CF_ZONE_ID,跳过清除 Cloudflare 缓存');
return;
}
if (!domain) {
console.warn('未配置 CF_DOMAIN,无法精准拼接 URL,跳过清除缓存以保护其他项目。');
return;
}
// 将路径转换为带域名的绝对 URL,Cloudflare 的 purge_cache(files) 要求必须为绝对 URL
const urls = paths.map((p) => {
// 确保拼接正确,防止出现双斜杠或少斜杠
const cleanDomain = domain.replace(/\/$/, '');
const cleanPath = p.startsWith('/') ? p : `/${p}`;
return `${cleanDomain}${cleanPath}`;
});
try {
const body = { files: urls };
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(body)
}
);
const result = (await response.json()) as any;
if (result.success) {
console.log(
'✅ 成功清除 Cloudflare 缓存',
urls ? `(URLs: ${urls.join(', ')})` : '(全部缓存)'
);
} else {
console.error('❌ 清除缓存失败:', result.errors);
}
} catch (error) {
console.error('❌ 调用 Cloudflare Purge API 时发生异常:', error);
}
}
export async function fetchAnalytics(platform: App.Platform | undefined) {
// @ts-expect-error Cloudflare env types
const token = platform?.env?.CF_API_TOKEN;
// @ts-expect-error Cloudflare env types
const zoneId = platform?.env?.CF_ZONE_ID;
if (!token || !zoneId) {
return null;
}
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const query = `
query {
viewer {
zones(filter: { zoneTag: "${zoneId}" }) {
httpRequests1dGroups(
limit: 30,
filter: { date_gt: "${thirtyDaysAgo}" }
orderBy: [date_ASC]
) {
dimensions {
date
}
sum {
cachedBytes
bytes
cachedRequests
requests
pageViews
}
}
}
}
}
`;
try {
const response = await fetch('https://api.cloudflare.com/client/v4/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ query })
});
const result = (await response.json()) as any;
return result?.data?.viewer?.zones?.[0]?.httpRequests1dGroups || [];
} catch (err) {
console.error('Failed to fetch analytics', err);
return null;
}
}
+18
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { purgeCache } from '$lib/server/cloudflare';
interface PostListRow { interface PostListRow {
id: number; id: number;
@@ -82,10 +83,27 @@ export const actions = {
try { try {
const placeholders = ids.map(() => '?').join(','); const placeholders = ids.map(() => '?').join(',');
const { results: postsToDelete } = await db
.prepare(`SELECT slug FROM posts WHERE id IN (${placeholders})`)
.bind(...ids)
.all<{ slug: string }>();
await db await db
.prepare(`DELETE FROM posts WHERE id IN (${placeholders})`) .prepare(`DELETE FROM posts WHERE id IN (${placeholders})`)
.bind(...ids) .bind(...ids)
.run(); .run();
try {
const pathsToPurge = ['/', '/posts', ...postsToDelete.map((p) => `/posts/${p.slug}`)];
if (platform?.context?.waitUntil) {
platform.context.waitUntil(purgeCache(platform, pathsToPurge));
} else {
await purgeCache(platform, pathsToPurge);
}
} catch (err) {
console.error('Failed to trigger cache purge:', err);
}
return { success: true }; return { success: true };
} catch (err: any) { } catch (err: any) {
console.error('Error deleting post:', err); console.error('Error deleting post:', err);
@@ -8,6 +8,7 @@ import {
resolveSeriesWeight, resolveSeriesWeight,
SeriesValidationError SeriesValidationError
} from '$lib/server/series'; } from '$lib/server/series';
import { purgeCache } from '$lib/server/cloudflare';
export const load: PageServerLoad = async ({ params, platform, parent }) => { export const load: PageServerLoad = async ({ params, platform, parent }) => {
const db = platform?.env?.DB; const db = platform?.env?.DB;
@@ -176,6 +177,17 @@ export const actions: Actions = {
return fail(500, { error: '保存修改失败' }); return fail(500, { error: '保存修改失败' });
} }
try {
const pathsToPurge = ['/', '/posts', `/posts/${slug}`];
if (platform?.context?.waitUntil) {
platform.context.waitUntil(purgeCache(platform, pathsToPurge));
} else {
await purgeCache(platform, pathsToPurge);
}
} catch (err) {
console.error('Failed to trigger cache purge:', err);
}
redirect(303, '/admin/posts'); redirect(303, '/admin/posts');
} }
}; };
@@ -8,6 +8,7 @@ import {
resolveSeriesWeight, resolveSeriesWeight,
SeriesValidationError SeriesValidationError
} from '$lib/server/series'; } from '$lib/server/series';
import { purgeCache } from '$lib/server/cloudflare';
export const load: PageServerLoad = async ({ platform }) => { export const load: PageServerLoad = async ({ platform }) => {
const db = platform?.env?.DB; const db = platform?.env?.DB;
@@ -116,6 +117,17 @@ export const actions: Actions = {
return fail(500, { error: '保存失败' }); return fail(500, { error: '保存失败' });
} }
try {
const pathsToPurge = ['/', '/posts', `/posts/${slug}`];
if (platform?.context?.waitUntil) {
platform.context.waitUntil(purgeCache(platform, pathsToPurge));
} else {
await purgeCache(platform, pathsToPurge);
}
} catch (err) {
console.error('Failed to trigger cache purge:', err);
}
redirect(303, '/admin/posts'); redirect(303, '/admin/posts');
} }
}; };
+21
View File
@@ -1,5 +1,6 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { purgeCache } from '$lib/server/cloudflare';
export const GET: RequestHandler = async ({ params, platform }) => { export const GET: RequestHandler = async ({ params, platform }) => {
const db = platform?.env.DB; const db = platform?.env.DB;
@@ -124,6 +125,16 @@ export const PUT: RequestHandler = async ({ params, request, platform }) => {
} }
} }
} }
try {
const pathsToPurge = ['/', '/posts', `/posts/${slug}`];
if (platform?.context?.waitUntil) {
platform.context.waitUntil(purgeCache(platform, pathsToPurge));
} else {
await purgeCache(platform, pathsToPurge);
}
} catch (err) {
console.error('Failed to trigger cache purge:', err);
}
return json({ success: true, slug }); return json({ success: true, slug });
} else { } else {
return json({ error: 'Failed to update post' }, { status: 500 }); return json({ error: 'Failed to update post' }, { status: 500 });
@@ -153,6 +164,16 @@ export const DELETE: RequestHandler = async ({ params, platform }) => {
const result = await db.prepare('DELETE FROM posts WHERE slug = ?').bind(slug).run(); const result = await db.prepare('DELETE FROM posts WHERE slug = ?').bind(slug).run();
if (result.success) { if (result.success) {
try {
const pathsToPurge = ['/', '/posts', `/posts/${slug}`];
if (platform?.context?.waitUntil) {
platform.context.waitUntil(purgeCache(platform, pathsToPurge));
} else {
await purgeCache(platform, pathsToPurge);
}
} catch (err) {
console.error('Failed to trigger cache purge:', err);
}
return json({ success: true }); return json({ success: true });
} else { } else {
return json({ error: 'Failed to delete post' }, { status: 500 }); return json({ error: 'Failed to delete post' }, { status: 500 });
+3 -2
View File
@@ -1,5 +1,5 @@
/* eslint-disable */ /* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 9194822214de3b6a9d6a85fa866a429b) // Generated by Wrangler by running `wrangler types` (hash: 1304802731dc94c585a8753b21107217)
// Runtime types generated with workerd@1.20260801.1 2026-08-07 nodejs_compat // Runtime types generated with workerd@1.20260801.1 2026-08-07 nodejs_compat
interface __BaseEnv_Env { interface __BaseEnv_Env {
R2: R2Bucket; R2: R2Bucket;
@@ -7,6 +7,7 @@ interface __BaseEnv_Env {
AI: Ai; AI: Ai;
CF_ACCESS_TEAM_DOMAIN: 'https://hisland.cloudflareaccess.com'; CF_ACCESS_TEAM_DOMAIN: 'https://hisland.cloudflareaccess.com';
CF_ACCESS_AUD: '35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff'; CF_ACCESS_AUD: '35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff';
CF_DOMAIN: 'https://manual.seahi.me';
} }
declare namespace Cloudflare { declare namespace Cloudflare {
interface Env extends __BaseEnv_Env {} interface Env extends __BaseEnv_Env {}
@@ -17,7 +18,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
}; };
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv extends StringifyValues< interface ProcessEnv extends StringifyValues<
Pick<Cloudflare.Env, 'CF_ACCESS_TEAM_DOMAIN' | 'CF_ACCESS_AUD'> Pick<Cloudflare.Env, 'CF_ACCESS_TEAM_DOMAIN' | 'CF_ACCESS_AUD' | 'CF_DOMAIN'>
> {} > {}
} }
+2 -1
View File
@@ -6,7 +6,8 @@
"pages_build_output_dir": ".svelte-kit/cloudflare", "pages_build_output_dir": ".svelte-kit/cloudflare",
"vars": { "vars": {
"CF_ACCESS_TEAM_DOMAIN": "https://hisland.cloudflareaccess.com", "CF_ACCESS_TEAM_DOMAIN": "https://hisland.cloudflareaccess.com",
"CF_ACCESS_AUD": "35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff" "CF_ACCESS_AUD": "35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff",
"CF_DOMAIN": "https://manual.seahi.me"
}, },
"d1_databases": [ "d1_databases": [
{ {