diff --git a/src/lib/server/cloudflare.ts b/src/lib/server/cloudflare.ts new file mode 100644 index 0000000..ba1f672 --- /dev/null +++ b/src/lib/server/cloudflare.ts @@ -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; + } +} diff --git a/src/routes/admin/posts/+page.server.ts b/src/routes/admin/posts/+page.server.ts index 2cdd63d..b569715 100644 --- a/src/routes/admin/posts/+page.server.ts +++ b/src/routes/admin/posts/+page.server.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { PageServerLoad } from './$types'; +import { purgeCache } from '$lib/server/cloudflare'; interface PostListRow { id: number; @@ -82,10 +83,27 @@ export const actions = { try { 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 .prepare(`DELETE FROM posts WHERE id IN (${placeholders})`) .bind(...ids) .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 }; } catch (err: any) { console.error('Error deleting post:', err); diff --git a/src/routes/admin/posts/[id]/+page.server.ts b/src/routes/admin/posts/[id]/+page.server.ts index 416c884..baa5320 100644 --- a/src/routes/admin/posts/[id]/+page.server.ts +++ b/src/routes/admin/posts/[id]/+page.server.ts @@ -8,6 +8,7 @@ import { resolveSeriesWeight, SeriesValidationError } from '$lib/server/series'; +import { purgeCache } from '$lib/server/cloudflare'; export const load: PageServerLoad = async ({ params, platform, parent }) => { const db = platform?.env?.DB; @@ -176,6 +177,17 @@ export const actions: Actions = { 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'); } }; diff --git a/src/routes/admin/posts/new/+page.server.ts b/src/routes/admin/posts/new/+page.server.ts index 35b4485..9b4ea90 100644 --- a/src/routes/admin/posts/new/+page.server.ts +++ b/src/routes/admin/posts/new/+page.server.ts @@ -8,6 +8,7 @@ import { resolveSeriesWeight, SeriesValidationError } from '$lib/server/series'; +import { purgeCache } from '$lib/server/cloudflare'; export const load: PageServerLoad = async ({ platform }) => { const db = platform?.env?.DB; @@ -116,6 +117,17 @@ export const actions: Actions = { 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'); } }; diff --git a/src/routes/api/v1/posts/[slug]/+server.ts b/src/routes/api/v1/posts/[slug]/+server.ts index bf53d12..362e0d1 100644 --- a/src/routes/api/v1/posts/[slug]/+server.ts +++ b/src/routes/api/v1/posts/[slug]/+server.ts @@ -1,5 +1,6 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +import { purgeCache } from '$lib/server/cloudflare'; export const GET: RequestHandler = async ({ params, platform }) => { 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 }); } else { 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(); 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 }); } else { return json({ error: 'Failed to delete post' }, { status: 500 }); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 04d6189..a16d7c6 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* 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 interface __BaseEnv_Env { R2: R2Bucket; @@ -7,6 +7,7 @@ interface __BaseEnv_Env { AI: Ai; CF_ACCESS_TEAM_DOMAIN: 'https://hisland.cloudflareaccess.com'; CF_ACCESS_AUD: '35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff'; + CF_DOMAIN: 'https://manual.seahi.me'; } declare namespace Cloudflare { interface Env extends __BaseEnv_Env {} @@ -17,7 +18,7 @@ type StringifyValues> = { }; declare namespace NodeJS { interface ProcessEnv extends StringifyValues< - Pick + Pick > {} } diff --git a/wrangler.jsonc b/wrangler.jsonc index c74209d..ab06a4a 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -6,7 +6,8 @@ "pages_build_output_dir": ".svelte-kit/cloudflare", "vars": { "CF_ACCESS_TEAM_DOMAIN": "https://hisland.cloudflareaccess.com", - "CF_ACCESS_AUD": "35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff" + "CF_ACCESS_AUD": "35b84a1c6ea81e1c036272ee68ecc4197041aadc1bed0815accac283a30e01ff", + "CF_DOMAIN": "https://manual.seahi.me" }, "d1_databases": [ {