feat: 集成 Cloudflare 缓存清除功能并在文章变更时自动触发
This commit is contained in:
1 parent
198d4f5075
commit
f340258afd
7 files changed
+177
-3
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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 });
|
||||||
|
|||||||
Vendored
+3
-2
@@ -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
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user