快捷菜单
常用功能一站直达
更多功能请点顶栏「快捷菜单」

问题
1新需求:
2博客后台有个按钮,点击后,立马把自己主站设为不可访问,并给读者提供一个漂亮的维护页面[破涕为笑]回答:
主站是静态 Hugo + EdgeOne 边缘函数,后台是独立的 EdgeOne Admin + KV。要「点按钮立刻下线」,只能走 后台写开关 → 主站边缘函数拦截,不能靠 Hugo 重建(那要几分钟)。
定方案前先确认两点:
1、维护页文案
2、维护开启后,你自己还想临时看主站吗?
回 A/B(例如 1B 2A)后我再出具体实现计划。
选:1B 2A
oneblogadmin.onedayxyy.cn 始终可访问,用于关维护不走 Hugo 重建:改配置再 CI 要几分钟,做不到「立马」。
改 [oneblog-admin/functions/[[default]].js](oneblog-admin/functions/[[default]].js):
site_maintenance1{
2 "enabled": true,
3 "title": "站点维护中",
4 "message": "正在升级,马上回来。",
5 "until": "2026-07-30 18:00",
6 "updatedAt": 1710000000000
7}health / auth/login 同级):GET /api/maintenance-status → 返回上述 JSON(无敏感信息),Cache-Control: no-store/api/v1/*,登录后才能改):GET /api/v1/maintenancePUT /api/v1/maintenance(body 同上字段)shouldAutoRebuildV1Api:不要因维护开关触发重建流水线本地开发:在 [admin-frontend/plugins/localAdminApi.js](admin-frontend/plugins/localAdminApi.js) 用 .local/maintenance.json 做同样读写,便于后台 UI 联调(本地 Hugo 不会被拦,属预期)。
改 [admin-frontend/src/views/System/Tools.vue](admin-frontend/src/views/System/Tools.vue) + [admin-frontend/src/api/settings.js](admin-frontend/src/api/settings.js):
el-popconfirm)onedayxyy.cn;本后台不受影响;需已发布带维护逻辑的边缘函数改 [edgeone/blog-cnb-origin.js](edgeone/blog-cnb-origin.js):
MAINTENANCE_STATUS_URL = 'https://oneblogadmin.onedayxyy.cn/api/maintenance-status'fetch + 短超时;可用 Cache API 缓存 5s,避免每次都打 Admin)enabled === true 时直接返回内嵌的漂亮 HTML,HTTP 503,Cache-Control: no-store,Retry-After: 300更新 [edgeone/README.md](edgeone/README.md):说明必须把新函数整文件覆盖并发布后,后台开关才真正生效。
no-store,避免 EdgeOne/浏览器把维护页或正常页长期缓存错乱[sw.js](hugo-teek-site/themes/hugo-teek/static/sw.js):维护 HTML 加一句轻量提示,或边缘对 HTML 导航强制 503;若仍被 SW 旧缓存挡住,可在维护页文案里写「强制刷新 / 无痕」——本次不做完整 SW 改造,以免扩大范围blog-cnb-origin.js 覆盖并发布.cnb.yml 去部署边缘函数(仍为控制台手动发布)一键维护模式已按计划落地,改动如下:
🟢已实现
oneblog-admin/functions/[[default\]].js)site_maintenanceGET /api/maintenance-status(无需登录)GET/PUT /api/v1/maintenance(不触发重建)localAdminApi.js 读写 .local/maintenance.jsonedgeone/blog-cnb-origin.js)🟢上线顺序
blog-cnb-origin.js 整文件覆盖并发布未发布边缘函数前,后台开关不会真正拦截主站。
blog-cnb-origin.js 内容:
1/**
2 * EdgeOne 边缘函数:从 CNB 公开产物库拉取静态站
3 *
4 * 用法:复制到腾讯云 EdgeOne 控制台 → 站点 → 边缘函数(整文件覆盖)
5 *
6 * 相对旧版改进:
7 * 1. HTML/CSS/JS 等文本资源做 gzip(缓冲后再压,避免流式 CompressionStream 在 EO 静默失败)
8 * 2. 指纹静态资源长缓存;HTML 短缓存
9 * 3. 响应头 X-EO-Compress 便于确认压缩是否生效
10 * 4. 找不到页面时回退到产物库中的 404.html(Hugo 自定义 404)
11 * 5. 维护模式:轮询 Admin 公开状态;开启时对所有请求返回 503 维护页
12 */
13addEventListener('fetch', (event) => {
14 event.respondWith(handleRequest(event.request));
15});
16
17const BASE = 'https://cnb.cool/onedayxyy/hugo-teek-public-dist/-/git/raw/dist';
18const MAINTENANCE_STATUS_URL = 'https://oneblogadmin.onedayxyy.cn/api/maintenance-status';
19const MAINTENANCE_CACHE_TTL_MS = 5000;
20const MAINTENANCE_FETCH_TIMEOUT_MS = 2500;
21
22async function handleRequest(request) {
23 const maintenance = await getMaintenanceStatus();
24 if (maintenance && maintenance.enabled) {
25 return buildMaintenanceResponse(maintenance);
26 }
27
28 const url = new URL(request.url);
29 let pathname = url.pathname;
30
31 if (pathname !== '/' && !pathname.endsWith('/') && !isFile(pathname)) {
32 const redirectUrl = new URL(request.url);
33 redirectUrl.pathname = pathname + '/';
34 return Response.redirect(redirectUrl.toString(), 301);
35 }
36
37 let uri = pathname;
38 if (uri.startsWith('/')) uri = uri.slice(1);
39 if (!uri || uri === '') uri = 'index.html';
40 if (uri.endsWith('/')) uri = uri + 'index.html';
41
42 let target = `${BASE}/${uri}`;
43 let resp = await fetchOrigin(target);
44
45 if (!resp.ok && !isFile(uri)) {
46 const indexUri = uri.endsWith('/') ? uri + 'index.html' : uri + '/index.html';
47 target = `${BASE}/${indexUri}`;
48 resp = await fetchOrigin(target);
49 if (resp.ok) uri = indexUri;
50 }
51
52 if (!resp.ok || !resp.body) {
53 // 回退到 Hugo 构建的自定义 404 页,避免纯文本 "Page Not Found"
54 const notFound = await fetchOrigin(`${BASE}/404.html`);
55 if (notFound.ok && notFound.body) {
56 const rawBuf = await notFound.arrayBuffer();
57 const contentType = 'text/html; charset=utf-8';
58 const headers = new Headers();
59 headers.set('Content-Type', contentType);
60 headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
61 headers.set('Content-Length', String(rawBuf.byteLength));
62 const uncompressed = new Response(rawBuf, {
63 status: 404,
64 headers,
65 });
66 return maybeGzip(request, uncompressed, '404.html', rawBuf, contentType);
67 }
68
69 return new Response('Page Not Found', {
70 status: 404,
71 headers: { 'Content-Type': 'text/html; charset=utf-8' },
72 });
73 }
74
75 const contentType =
76 getContentType(uri) || resp.headers.get('content-type') || 'text/html; charset=utf-8';
77
78 // 大体积 CSS/JS(如 main.css ~1MB)若整包 arrayBuffer + gzip,EO 易直接 545。
79 // 指纹静态资源改为直通,交给平台智能压缩。
80 const originLen = Number(resp.headers.get('content-length') || 0);
81 if (shouldPassthrough(uri, contentType, originLen)) {
82 const headers = new Headers();
83 headers.set('Content-Type', contentType);
84 headers.set('Cache-Control', cacheControlFor(uri));
85 if (originLen > 0) headers.set('Content-Length', String(originLen));
86 headers.set('X-EO-Compress', 'passthrough');
87 return new Response(resp.body, { status: resp.status, headers });
88 }
89
90 // 先读入缓冲:EO 上对流式 pipeThrough(CompressionStream) 经常不生效
91 let rawBuf;
92 try {
93 rawBuf = await resp.arrayBuffer();
94 } catch (e) {
95 const headers = new Headers();
96 headers.set('Content-Type', contentType);
97 headers.set('Cache-Control', cacheControlFor(uri));
98 headers.set('X-EO-Compress', 'passthrough-read-fail');
99 const retry = await fetchOrigin(target);
100 return new Response(retry.body, { status: retry.status, headers });
101 }
102
103 // 读完后发现超大:不再 gzip,避免边缘函数 OOM/545
104 if (rawBuf.byteLength > PASS_THROUGH_MAX_BYTES) {
105 const headers = new Headers();
106 headers.set('Content-Type', contentType);
107 headers.set('Cache-Control', cacheControlFor(uri));
108 headers.set('Content-Length', String(rawBuf.byteLength));
109 headers.set('X-EO-Compress', 'skip-large');
110 return new Response(rawBuf, { status: resp.status, headers });
111 }
112
113 const headers = new Headers();
114 headers.set('Content-Type', contentType);
115 headers.set('Cache-Control', cacheControlFor(uri));
116 headers.set('Content-Length', String(rawBuf.byteLength));
117
118 const uncompressed = new Response(rawBuf, {
119 status: resp.status,
120 headers,
121 });
122
123 return maybeGzip(request, uncompressed, uri, rawBuf, contentType);
124}
125
126/** 超过该体积不再做边缘 gzip 缓冲(main.css 约 1MB) */
127const PASS_THROUGH_MAX_BYTES = 400 * 1024;
128
129function shouldPassthrough(uri, contentType, contentLength) {
130 if (contentLength > PASS_THROUGH_MAX_BYTES) return true;
131 // 指纹 CSS/JS 体积常偏大,优先直通
132 if (/\.min\.[a-f0-9]{8,}\.(?:css|js)(?:$|\?)/i.test(uri)) {
133 if (contentLength === 0) return true; // 未知长度时也不冒险缓冲
134 if (contentLength > 200 * 1024) return true;
135 }
136 return false;
137}
138
139function fetchOrigin(target) {
140 return fetch(target, {
141 headers: {
142 'User-Agent': 'curl/8.0.0',
143 Accept: '*/*',
144 'Accept-Encoding': 'identity',
145 },
146 });
147}
148
149function isFile(path) {
150 return /\.[^/]+$/.test(path);
151}
152
153function getContentType(uri) {
154 const map = {
155 html: 'text/html; charset=utf-8',
156 css: 'text/css; charset=utf-8',
157 js: 'application/javascript; charset=utf-8',
158 mjs: 'application/javascript; charset=utf-8',
159 json: 'application/json; charset=utf-8',
160 png: 'image/png',
161 jpg: 'image/jpeg',
162 jpeg: 'image/jpeg',
163 gif: 'image/gif',
164 svg: 'image/svg+xml',
165 webp: 'image/webp',
166 avif: 'image/avif',
167 ico: 'image/x-icon',
168 woff: 'font/woff',
169 woff2: 'font/woff2',
170 txt: 'text/plain; charset=utf-8',
171 xml: 'application/xml; charset=utf-8',
172 map: 'application/json',
173 };
174 if (!isFile(uri)) return 'text/html; charset=utf-8';
175 const ext = uri.split('.').pop().toLowerCase();
176 return map[ext];
177}
178
179function cacheControlFor(uri) {
180 if (/\.(?:css|js|mjs|woff2?|png|jpe?g|gif|webp|avif|svg|ico)(?:$|\?)/i.test(uri)) {
181 return 'public, max-age=31536000, immutable';
182 }
183 return 'public, max-age=60, must-revalidate';
184}
185
186function isCompressible(uri, contentType) {
187 if (/\.(?:png|jpe?g|gif|webp|avif|woff2?|mp4|webm|gz|br)(?:$|\?)/i.test(uri)) {
188 return false;
189 }
190 const type = contentType || '';
191 return /text\/|javascript|json|xml|svg/.test(type);
192}
193
194async function compressGzip(rawBuf) {
195 if (typeof CompressionStream === 'undefined') {
196 return { ok: false, reason: 'no-compression-stream' };
197 }
198 try {
199 const cs = new CompressionStream('gzip');
200 const writer = cs.writable.getWriter();
201 await writer.write(new Uint8Array(rawBuf));
202 await writer.close();
203 const compressed = await new Response(cs.readable).arrayBuffer();
204 if (!compressed || compressed.byteLength < 2) {
205 return { ok: false, reason: 'empty-output' };
206 }
207 // gzip magic 1F 8B
208 const u8 = new Uint8Array(compressed);
209 if (u8[0] !== 0x1f || u8[1] !== 0x8b) {
210 return { ok: false, reason: 'bad-magic' };
211 }
212 return { ok: true, buf: compressed };
213 } catch (e) {
214 return { ok: false, reason: 'err:' + (e && e.message ? String(e.message).slice(0, 80) : 'unknown') };
215 }
216}
217
218async function maybeGzip(request, response, uri, rawBuf, contentType) {
219 const accept = (request.headers.get('Accept-Encoding') || '').toLowerCase();
220 // EdgeOne 常会剥掉/改写传入边缘函数的 Accept-Encoding,导致误判 skip-client。
221 // 现代浏览器都支持 gzip:仅当明确只要 identity 时才跳过。
222 const identityOnly =
223 accept === 'identity' ||
224 (/identity/.test(accept) && !/gzip|\*|br/.test(accept));
225 if (identityOnly) {
226 const h = new Headers(response.headers);
227 h.set('X-EO-Compress', 'skip-identity');
228 return new Response(rawBuf, { status: response.status, headers: h });
229 }
230 if (!isCompressible(uri, contentType)) {
231 const h = new Headers(response.headers);
232 h.set('X-EO-Compress', 'skip-type');
233 return new Response(rawBuf, { status: response.status, headers: h });
234 }
235
236 const result = await compressGzip(rawBuf);
237 if (!result.ok) {
238 const h = new Headers(response.headers);
239 h.set('X-EO-Compress', result.reason || 'fail');
240 // 不设 Vary,尽量让 EO「智能压缩」有机会接管
241 return new Response(rawBuf, { status: response.status, headers: h });
242 }
243
244 const headers = new Headers(response.headers);
245 headers.set('Content-Encoding', 'gzip');
246 headers.set('Content-Length', String(result.buf.byteLength));
247 headers.set('Vary', 'Accept-Encoding');
248 headers.set(
249 'X-EO-Compress',
250 'gzip-ok;ae=' + (accept ? accept.slice(0, 40) : 'missing')
251 );
252 return new Response(result.buf, { status: response.status, headers });
253}
254
255// ==================== 站点维护模式 ====================
256// fail-open:状态接口失败/超时则照常提供站点,避免 Admin 故障锁死主站。
257
258let maintenanceMemoryCache = { at: 0, data: null };
259
260async function getMaintenanceStatus() {
261 const now = Date.now();
262 if (
263 maintenanceMemoryCache.data &&
264 now - maintenanceMemoryCache.at < MAINTENANCE_CACHE_TTL_MS
265 ) {
266 return maintenanceMemoryCache.data;
267 }
268
269 try {
270 if (typeof caches !== 'undefined' && caches.default) {
271 const cache = caches.default;
272 const cacheReq = new Request(MAINTENANCE_STATUS_URL, { method: 'GET' });
273 const cached = await cache.match(cacheReq);
274 if (cached) {
275 const data = await cached.json();
276 maintenanceMemoryCache = { at: now, data };
277 return data;
278 }
279 }
280 } catch (_) {
281 /* ignore cache read */
282 }
283
284 const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
285 const timer = controller
286 ? setTimeout(() => controller.abort(), MAINTENANCE_FETCH_TIMEOUT_MS)
287 : null;
288
289 try {
290 const resp = await fetch(MAINTENANCE_STATUS_URL, {
291 method: 'GET',
292 headers: { Accept: 'application/json', 'Cache-Control': 'no-cache' },
293 signal: controller ? controller.signal : undefined,
294 });
295 if (!resp.ok) return null;
296 const data = await resp.json();
297 maintenanceMemoryCache = { at: now, data };
298
299 try {
300 if (typeof caches !== 'undefined' && caches.default) {
301 const cache = caches.default;
302 const cacheResp = new Response(JSON.stringify(data), {
303 headers: {
304 'Content-Type': 'application/json',
305 'Cache-Control': 'max-age=5',
306 },
307 });
308 await cache.put(MAINTENANCE_STATUS_URL, cacheResp);
309 }
310 } catch (_) {
311 /* ignore cache write */
312 }
313
314 return data;
315 } catch (_) {
316 return null;
317 } finally {
318 if (timer) clearTimeout(timer);
319 }
320}
321
322function escapeHtml(str) {
323 return String(str || '')
324 .replace(/&/g, '&')
325 .replace(/</g, '<')
326 .replace(/>/g, '>')
327 .replace(/"/g, '"')
328 .replace(/'/g, ''');
329}
330
331function buildMaintenanceHtml(status) {
332 const title = escapeHtml(status.title || '站点维护中');
333 const message = escapeHtml(status.message || '正在升级,马上回来。');
334 const until = status.until ? escapeHtml(status.until) : '';
335 const untilBlock = until
336 ? `<p class="until"><span>预计恢复</span><strong>${until}</strong></p>`
337 : '';
338
339 return `<!DOCTYPE html>
340<html lang="zh-CN">
341<head>
342 <meta charset="utf-8">
343 <meta name="viewport" content="width=device-width, initial-scale=1">
344 <meta name="robots" content="noindex">
345 <title>${title} · One Blog</title>
346 <link rel="preconnect" href="https://fonts.googleapis.com">
347 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
348 <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
349 <style>
350 :root {
351 --ink: #1a2e2a;
352 --muted: #5a6f68;
353 --accent: #2f8f73;
354 --accent-soft: #7bc4a8;
355 --paper: #f6f3eb;
356 --glow: #d8efe4;
357 }
358 * { box-sizing: border-box; }
359 html, body { height: 100%; margin: 0; }
360 body {
361 min-height: 100%;
362 display: grid;
363 place-items: center;
364 padding: 28px 20px;
365 color: var(--ink);
366 font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
367 background:
368 radial-gradient(ellipse 80% 55% at 18% 12%, #dff3ea 0%, transparent 55%),
369 radial-gradient(ellipse 70% 50% at 88% 88%, #efe4c8 0%, transparent 50%),
370 radial-gradient(ellipse 50% 40% at 70% 20%, #cfe8de 0%, transparent 45%),
371 linear-gradient(165deg, #eef6f2 0%, var(--paper) 48%, #ebe4d6 100%);
372 overflow: hidden;
373 position: relative;
374 }
375 .blob {
376 position: absolute;
377 border-radius: 50%;
378 filter: blur(48px);
379 opacity: .55;
380 pointer-events: none;
381 animation: drift 14s ease-in-out infinite;
382 }
383 .blob-a {
384 width: 340px; height: 340px;
385 left: -80px; top: -40px;
386 background: #9fd9c2;
387 }
388 .blob-b {
389 width: 280px; height: 280px;
390 right: -60px; bottom: -30px;
391 background: #e8d5a3;
392 animation-delay: -5s;
393 animation-duration: 18s;
394 }
395 .blob-c {
396 width: 180px; height: 180px;
397 left: 42%; top: 62%;
398 background: #b7e0d2;
399 animation-delay: -9s;
400 animation-duration: 16s;
401 }
402 .grain {
403 position: absolute;
404 inset: 0;
405 pointer-events: none;
406 opacity: .04;
407 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
408 }
409 .stage {
410 position: relative;
411 z-index: 1;
412 width: min(520px, 100%);
413 text-align: center;
414 animation: enter .8s cubic-bezier(.22,1,.36,1) both;
415 }
416 .visual {
417 width: 168px;
418 height: 168px;
419 margin: 0 auto 28px;
420 position: relative;
421 animation: float 5.5s ease-in-out infinite;
422 }
423 .visual svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 18px 28px rgba(47,143,115,.18)); }
424 .ring {
425 position: absolute;
426 inset: -10px;
427 border-radius: 50%;
428 border: 1.5px dashed rgba(47,143,115,.28);
429 animation: spin 28s linear infinite;
430 }
431 .brand {
432 font-family: Fraunces, "Noto Serif SC", Georgia, serif;
433 font-size: clamp(34px, 7vw, 48px);
434 font-weight: 700;
435 letter-spacing: -.02em;
436 line-height: 1;
437 margin: 0 0 8px;
438 color: var(--ink);
439 }
440 .brand em {
441 font-style: normal;
442 background: linear-gradient(120deg, var(--accent), #4aa888 45%, #c4a35a);
443 -webkit-background-clip: text;
444 background-clip: text;
445 color: transparent;
446 }
447 .domain {
448 margin: 0 0 28px;
449 font-size: 13px;
450 letter-spacing: .16em;
451 text-transform: uppercase;
452 color: var(--muted);
453 font-weight: 500;
454 }
455 h1 {
456 margin: 0 0 12px;
457 font-size: clamp(24px, 4.5vw, 32px);
458 font-weight: 700;
459 letter-spacing: .02em;
460 }
461 .message {
462 margin: 0 auto;
463 max-width: 34em;
464 font-size: 16px;
465 line-height: 1.75;
466 color: var(--muted);
467 white-space: pre-wrap;
468 font-weight: 400;
469 }
470 .progress {
471 width: min(220px, 70%);
472 height: 4px;
473 margin: 28px auto 0;
474 border-radius: 999px;
475 background: rgba(47,143,115,.12);
476 overflow: hidden;
477 }
478 .progress > i {
479 display: block;
480 height: 100%;
481 width: 42%;
482 border-radius: inherit;
483 background: linear-gradient(90deg, var(--accent-soft), var(--accent), #c4a35a);
484 animation: slide 2.4s ease-in-out infinite;
485 }
486 .until {
487 display: inline-flex;
488 align-items: center;
489 gap: 10px;
490 margin: 22px 0 0;
491 padding: 8px 14px;
492 border-radius: 999px;
493 background: rgba(255,255,255,.55);
494 border: 1px solid rgba(47,143,115,.16);
495 font-size: 13px;
496 color: var(--muted);
497 backdrop-filter: blur(8px);
498 }
499 .until span { opacity: .75; }
500 .until strong { color: var(--ink); font-weight: 600; }
501 .hint {
502 margin: 26px 0 0;
503 font-size: 12px;
504 color: rgba(90,111,104,.72);
505 }
506 @keyframes enter {
507 from { opacity: 0; transform: translateY(18px) scale(.98); }
508 to { opacity: 1; transform: none; }
509 }
510 @keyframes float {
511 0%, 100% { transform: translateY(0); }
512 50% { transform: translateY(-10px); }
513 }
514 @keyframes drift {
515 0%, 100% { transform: translate(0, 0) scale(1); }
516 50% { transform: translate(24px, -18px) scale(1.08); }
517 }
518 @keyframes spin { to { transform: rotate(360deg); } }
519 @keyframes slide {
520 0% { transform: translateX(-120%); }
521 100% { transform: translateX(280%); }
522 }
523 @media (prefers-reduced-motion: reduce) {
524 .blob, .visual, .ring, .progress > i, .stage { animation: none !important; }
525 }
526 </style>
527</head>
528<body>
529 <div class="blob blob-a" aria-hidden="true"></div>
530 <div class="blob blob-b" aria-hidden="true"></div>
531 <div class="blob blob-c" aria-hidden="true"></div>
532 <div class="grain" aria-hidden="true"></div>
533 <main class="stage">
534 <div class="visual" aria-hidden="true">
535 <div class="ring"></div>
536 <svg viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
537 <defs>
538 <linearGradient id="g1" x1="28" y1="24" x2="140" y2="148" gradientUnits="userSpaceOnUse">
539 <stop stop-color="#E8F7F1"/>
540 <stop offset="1" stop-color="#B7E0D2"/>
541 </linearGradient>
542 <linearGradient id="g2" x1="56" y1="62" x2="118" y2="118" gradientUnits="userSpaceOnUse">
543 <stop stop-color="#2F8F73"/>
544 <stop offset="1" stop-color="#C4A35A"/>
545 </linearGradient>
546 </defs>
547 <circle cx="84" cy="84" r="72" fill="url(#g1)"/>
548 <circle cx="84" cy="84" r="54" fill="#F8FBF9" fill-opacity=".72"/>
549 <path d="M58 92c8-18 22-28 36-28s28 10 36 28" stroke="url(#g2)" stroke-width="5" stroke-linecap="round"/>
550 <path d="M70 78c4-8 10-12 14-12s10 4 14 12" stroke="#2F8F73" stroke-width="4" stroke-linecap="round" opacity=".55"/>
551 <circle cx="84" cy="104" r="7" fill="#2F8F73"/>
552 <path d="M84 111v18" stroke="#2F8F73" stroke-width="4" stroke-linecap="round"/>
553 <path d="M74 126h20" stroke="#C4A35A" stroke-width="4" stroke-linecap="round"/>
554 <circle cx="118" cy="58" r="5" fill="#C4A35A" opacity=".8"/>
555 <circle cx="52" cy="64" r="3.5" fill="#2F8F73" opacity=".45"/>
556 </svg>
557 </div>
558 <p class="brand">One <em>Blog</em></p>
559 <p class="domain">onedayxyy.cn</p>
560 <h1>${title}</h1>
561 <p class="message">${message}</p>
562 <div class="progress" aria-hidden="true"><i></i></div>
563 ${untilBlock}
564 <p class="hint">若仍看到旧页面,请强制刷新或使用无痕窗口</p>
565 </main>
566</body>
567</html>`;
568}
569
570function buildMaintenanceResponse(status) {
571 const html = buildMaintenanceHtml(status);
572 return new Response(html, {
573 status: 503,
574 headers: {
575 'Content-Type': 'text/html; charset=utf-8',
576 'Cache-Control': 'no-store, no-cache, must-revalidate',
577 Pragma: 'no-cache',
578 'Retry-After': '300',
579 'X-OneBlog-Maintenance': '1',
580 },
581 });
582}效果:

维护页已重做成更亮、更有氛围的版本,主要变化:
One Blog(Fraunces)作为视觉主信号prefers-reduced-motion)文案仍走后台配置的标题 / 说明 / 预计恢复时间。
生效方式:把更新后的 edgeone/blog-cnb-origin.js 在 EdgeOne 控制台整文件覆盖并发布;不必重建 Hugo。需要的话我可以帮你提交推送。
blog-cnb-origin.js 内容:
1/**
2 * EdgeOne 边缘函数:从 CNB 公开产物库拉取静态站
3 *
4 * 用法:复制到腾讯云 EdgeOne 控制台 → 站点 → 边缘函数(整文件覆盖)
5 *
6 * 相对旧版改进:
7 * 1. HTML/CSS/JS 等文本资源做 gzip(缓冲后再压,避免流式 CompressionStream 在 EO 静默失败)
8 * 2. 指纹静态资源长缓存;HTML 短缓存
9 * 3. 响应头 X-EO-Compress 便于确认压缩是否生效
10 * 4. 找不到页面时回退到产物库中的 404.html(Hugo 自定义 404)
11 * 5. 维护模式:轮询 Admin 公开状态;开启时对所有请求返回 503 维护页
12 */
13addEventListener('fetch', (event) => {
14 event.respondWith(handleRequest(event.request));
15});
16
17const BASE = 'https://cnb.cool/onedayxyy/hugo-teek-public-dist/-/git/raw/dist';
18const MAINTENANCE_STATUS_URL = 'https://oneblogadmin.onedayxyy.cn/api/maintenance-status';
19const MAINTENANCE_CACHE_TTL_MS = 5000;
20const MAINTENANCE_FETCH_TIMEOUT_MS = 2500;
21
22async function handleRequest(request) {
23 const maintenance = await getMaintenanceStatus();
24 if (maintenance && maintenance.enabled) {
25 return buildMaintenanceResponse(maintenance);
26 }
27
28 const url = new URL(request.url);
29 let pathname = url.pathname;
30
31 if (pathname !== '/' && !pathname.endsWith('/') && !isFile(pathname)) {
32 const redirectUrl = new URL(request.url);
33 redirectUrl.pathname = pathname + '/';
34 return Response.redirect(redirectUrl.toString(), 301);
35 }
36
37 let uri = pathname;
38 if (uri.startsWith('/')) uri = uri.slice(1);
39 if (!uri || uri === '') uri = 'index.html';
40 if (uri.endsWith('/')) uri = uri + 'index.html';
41
42 let target = `${BASE}/${uri}`;
43 let resp = await fetchOrigin(target);
44
45 if (!resp.ok && !isFile(uri)) {
46 const indexUri = uri.endsWith('/') ? uri + 'index.html' : uri + '/index.html';
47 target = `${BASE}/${indexUri}`;
48 resp = await fetchOrigin(target);
49 if (resp.ok) uri = indexUri;
50 }
51
52 if (!resp.ok || !resp.body) {
53 // 回退到 Hugo 构建的自定义 404 页,避免纯文本 "Page Not Found"
54 const notFound = await fetchOrigin(`${BASE}/404.html`);
55 if (notFound.ok && notFound.body) {
56 const rawBuf = await notFound.arrayBuffer();
57 const contentType = 'text/html; charset=utf-8';
58 const headers = new Headers();
59 headers.set('Content-Type', contentType);
60 headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
61 headers.set('Content-Length', String(rawBuf.byteLength));
62 const uncompressed = new Response(rawBuf, {
63 status: 404,
64 headers,
65 });
66 return maybeGzip(request, uncompressed, '404.html', rawBuf, contentType);
67 }
68
69 return new Response('Page Not Found', {
70 status: 404,
71 headers: { 'Content-Type': 'text/html; charset=utf-8' },
72 });
73 }
74
75 const contentType =
76 getContentType(uri) || resp.headers.get('content-type') || 'text/html; charset=utf-8';
77
78 // 大体积 CSS/JS(如 main.css ~1MB)若整包 arrayBuffer + gzip,EO 易直接 545。
79 // 指纹静态资源改为直通,交给平台智能压缩。
80 const originLen = Number(resp.headers.get('content-length') || 0);
81 if (shouldPassthrough(uri, contentType, originLen)) {
82 const headers = new Headers();
83 headers.set('Content-Type', contentType);
84 headers.set('Cache-Control', cacheControlFor(uri));
85 if (originLen > 0) headers.set('Content-Length', String(originLen));
86 headers.set('X-EO-Compress', 'passthrough');
87 return new Response(resp.body, { status: resp.status, headers });
88 }
89
90 // 先读入缓冲:EO 上对流式 pipeThrough(CompressionStream) 经常不生效
91 let rawBuf;
92 try {
93 rawBuf = await resp.arrayBuffer();
94 } catch (e) {
95 const headers = new Headers();
96 headers.set('Content-Type', contentType);
97 headers.set('Cache-Control', cacheControlFor(uri));
98 headers.set('X-EO-Compress', 'passthrough-read-fail');
99 const retry = await fetchOrigin(target);
100 return new Response(retry.body, { status: retry.status, headers });
101 }
102
103 // 读完后发现超大:不再 gzip,避免边缘函数 OOM/545
104 if (rawBuf.byteLength > PASS_THROUGH_MAX_BYTES) {
105 const headers = new Headers();
106 headers.set('Content-Type', contentType);
107 headers.set('Cache-Control', cacheControlFor(uri));
108 headers.set('Content-Length', String(rawBuf.byteLength));
109 headers.set('X-EO-Compress', 'skip-large');
110 return new Response(rawBuf, { status: resp.status, headers });
111 }
112
113 const headers = new Headers();
114 headers.set('Content-Type', contentType);
115 headers.set('Cache-Control', cacheControlFor(uri));
116 headers.set('Content-Length', String(rawBuf.byteLength));
117
118 const uncompressed = new Response(rawBuf, {
119 status: resp.status,
120 headers,
121 });
122
123 return maybeGzip(request, uncompressed, uri, rawBuf, contentType);
124}
125
126/** 超过该体积不再做边缘 gzip 缓冲(main.css 约 1MB) */
127const PASS_THROUGH_MAX_BYTES = 400 * 1024;
128
129function shouldPassthrough(uri, contentType, contentLength) {
130 if (contentLength > PASS_THROUGH_MAX_BYTES) return true;
131 // 指纹 CSS/JS 体积常偏大,优先直通
132 if (/\.min\.[a-f0-9]{8,}\.(?:css|js)(?:$|\?)/i.test(uri)) {
133 if (contentLength === 0) return true; // 未知长度时也不冒险缓冲
134 if (contentLength > 200 * 1024) return true;
135 }
136 return false;
137}
138
139function fetchOrigin(target) {
140 return fetch(target, {
141 headers: {
142 'User-Agent': 'curl/8.0.0',
143 Accept: '*/*',
144 'Accept-Encoding': 'identity',
145 },
146 });
147}
148
149function isFile(path) {
150 return /\.[^/]+$/.test(path);
151}
152
153function getContentType(uri) {
154 const map = {
155 html: 'text/html; charset=utf-8',
156 css: 'text/css; charset=utf-8',
157 js: 'application/javascript; charset=utf-8',
158 mjs: 'application/javascript; charset=utf-8',
159 json: 'application/json; charset=utf-8',
160 png: 'image/png',
161 jpg: 'image/jpeg',
162 jpeg: 'image/jpeg',
163 gif: 'image/gif',
164 svg: 'image/svg+xml',
165 webp: 'image/webp',
166 avif: 'image/avif',
167 ico: 'image/x-icon',
168 woff: 'font/woff',
169 woff2: 'font/woff2',
170 txt: 'text/plain; charset=utf-8',
171 xml: 'application/xml; charset=utf-8',
172 map: 'application/json',
173 };
174 if (!isFile(uri)) return 'text/html; charset=utf-8';
175 const ext = uri.split('.').pop().toLowerCase();
176 return map[ext];
177}
178
179function cacheControlFor(uri) {
180 if (/\.(?:css|js|mjs|woff2?|png|jpe?g|gif|webp|avif|svg|ico)(?:$|\?)/i.test(uri)) {
181 return 'public, max-age=31536000, immutable';
182 }
183 return 'public, max-age=60, must-revalidate';
184}
185
186function isCompressible(uri, contentType) {
187 if (/\.(?:png|jpe?g|gif|webp|avif|woff2?|mp4|webm|gz|br)(?:$|\?)/i.test(uri)) {
188 return false;
189 }
190 const type = contentType || '';
191 return /text\/|javascript|json|xml|svg/.test(type);
192}
193
194async function compressGzip(rawBuf) {
195 if (typeof CompressionStream === 'undefined') {
196 return { ok: false, reason: 'no-compression-stream' };
197 }
198 try {
199 const cs = new CompressionStream('gzip');
200 const writer = cs.writable.getWriter();
201 await writer.write(new Uint8Array(rawBuf));
202 await writer.close();
203 const compressed = await new Response(cs.readable).arrayBuffer();
204 if (!compressed || compressed.byteLength < 2) {
205 return { ok: false, reason: 'empty-output' };
206 }
207 // gzip magic 1F 8B
208 const u8 = new Uint8Array(compressed);
209 if (u8[0] !== 0x1f || u8[1] !== 0x8b) {
210 return { ok: false, reason: 'bad-magic' };
211 }
212 return { ok: true, buf: compressed };
213 } catch (e) {
214 return { ok: false, reason: 'err:' + (e && e.message ? String(e.message).slice(0, 80) : 'unknown') };
215 }
216}
217
218async function maybeGzip(request, response, uri, rawBuf, contentType) {
219 const accept = (request.headers.get('Accept-Encoding') || '').toLowerCase();
220 // EdgeOne 常会剥掉/改写传入边缘函数的 Accept-Encoding,导致误判 skip-client。
221 // 现代浏览器都支持 gzip:仅当明确只要 identity 时才跳过。
222 const identityOnly =
223 accept === 'identity' ||
224 (/identity/.test(accept) && !/gzip|\*|br/.test(accept));
225 if (identityOnly) {
226 const h = new Headers(response.headers);
227 h.set('X-EO-Compress', 'skip-identity');
228 return new Response(rawBuf, { status: response.status, headers: h });
229 }
230 if (!isCompressible(uri, contentType)) {
231 const h = new Headers(response.headers);
232 h.set('X-EO-Compress', 'skip-type');
233 return new Response(rawBuf, { status: response.status, headers: h });
234 }
235
236 const result = await compressGzip(rawBuf);
237 if (!result.ok) {
238 const h = new Headers(response.headers);
239 h.set('X-EO-Compress', result.reason || 'fail');
240 // 不设 Vary,尽量让 EO「智能压缩」有机会接管
241 return new Response(rawBuf, { status: response.status, headers: h });
242 }
243
244 const headers = new Headers(response.headers);
245 headers.set('Content-Encoding', 'gzip');
246 headers.set('Content-Length', String(result.buf.byteLength));
247 headers.set('Vary', 'Accept-Encoding');
248 headers.set(
249 'X-EO-Compress',
250 'gzip-ok;ae=' + (accept ? accept.slice(0, 40) : 'missing')
251 );
252 return new Response(result.buf, { status: response.status, headers });
253}
254
255// ==================== 站点维护模式 ====================
256// fail-open:状态接口失败/超时则照常提供站点,避免 Admin 故障锁死主站。
257
258let maintenanceMemoryCache = { at: 0, data: null };
259
260async function getMaintenanceStatus() {
261 const now = Date.now();
262 if (
263 maintenanceMemoryCache.data &&
264 now - maintenanceMemoryCache.at < MAINTENANCE_CACHE_TTL_MS
265 ) {
266 return maintenanceMemoryCache.data;
267 }
268
269 try {
270 if (typeof caches !== 'undefined' && caches.default) {
271 const cache = caches.default;
272 const cacheReq = new Request(MAINTENANCE_STATUS_URL, { method: 'GET' });
273 const cached = await cache.match(cacheReq);
274 if (cached) {
275 const data = await cached.json();
276 maintenanceMemoryCache = { at: now, data };
277 return data;
278 }
279 }
280 } catch (_) {
281 /* ignore cache read */
282 }
283
284 const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
285 const timer = controller
286 ? setTimeout(() => controller.abort(), MAINTENANCE_FETCH_TIMEOUT_MS)
287 : null;
288
289 try {
290 const resp = await fetch(MAINTENANCE_STATUS_URL, {
291 method: 'GET',
292 headers: { Accept: 'application/json', 'Cache-Control': 'no-cache' },
293 signal: controller ? controller.signal : undefined,
294 });
295 if (!resp.ok) return null;
296 const data = await resp.json();
297 maintenanceMemoryCache = { at: now, data };
298
299 try {
300 if (typeof caches !== 'undefined' && caches.default) {
301 const cache = caches.default;
302 const cacheResp = new Response(JSON.stringify(data), {
303 headers: {
304 'Content-Type': 'application/json',
305 'Cache-Control': 'max-age=5',
306 },
307 });
308 await cache.put(MAINTENANCE_STATUS_URL, cacheResp);
309 }
310 } catch (_) {
311 /* ignore cache write */
312 }
313
314 return data;
315 } catch (_) {
316 return null;
317 } finally {
318 if (timer) clearTimeout(timer);
319 }
320}
321
322function escapeHtml(str) {
323 return String(str || '')
324 .replace(/&/g, '&')
325 .replace(/</g, '<')
326 .replace(/>/g, '>')
327 .replace(/"/g, '"')
328 .replace(/'/g, ''');
329}
330
331function buildMaintenanceHtml(status) {
332 const title = escapeHtml(status.title || '站点维护中');
333 const message = escapeHtml(status.message || '正在升级,马上回来。');
334 const until = status.until ? escapeHtml(status.until) : '';
335 const untilBlock = until
336 ? `<p class="until"><span>预计恢复</span><strong>${until}</strong></p>`
337 : '';
338
339 return `<!DOCTYPE html>
340<html lang="zh-CN">
341<head>
342 <meta charset="utf-8">
343 <meta name="viewport" content="width=device-width, initial-scale=1">
344 <meta name="robots" content="noindex">
345 <title>${title} · One Blog</title>
346 <link rel="preconnect" href="https://fonts.googleapis.com">
347 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
348 <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
349 <style>
350 :root {
351 --ink: #1a2e2a;
352 --muted: #5a6f68;
353 --accent: #2f8f73;
354 --accent-soft: #7bc4a8;
355 --paper: #f6f3eb;
356 --glow: #d8efe4;
357 }
358 * { box-sizing: border-box; }
359 html, body { height: 100%; margin: 0; }
360 body {
361 min-height: 100%;
362 display: grid;
363 place-items: center;
364 padding: 28px 20px;
365 color: var(--ink);
366 font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
367 background:
368 radial-gradient(ellipse 80% 55% at 18% 12%, #dff3ea 0%, transparent 55%),
369 radial-gradient(ellipse 70% 50% at 88% 88%, #efe4c8 0%, transparent 50%),
370 radial-gradient(ellipse 50% 40% at 70% 20%, #cfe8de 0%, transparent 45%),
371 linear-gradient(165deg, #eef6f2 0%, var(--paper) 48%, #ebe4d6 100%);
372 overflow: hidden;
373 position: relative;
374 }
375 .blob {
376 position: absolute;
377 border-radius: 50%;
378 filter: blur(48px);
379 opacity: .55;
380 pointer-events: none;
381 animation: drift 14s ease-in-out infinite;
382 }
383 .blob-a {
384 width: 340px; height: 340px;
385 left: -80px; top: -40px;
386 background: #9fd9c2;
387 }
388 .blob-b {
389 width: 280px; height: 280px;
390 right: -60px; bottom: -30px;
391 background: #e8d5a3;
392 animation-delay: -5s;
393 animation-duration: 18s;
394 }
395 .blob-c {
396 width: 180px; height: 180px;
397 left: 42%; top: 62%;
398 background: #b7e0d2;
399 animation-delay: -9s;
400 animation-duration: 16s;
401 }
402 .grain {
403 position: absolute;
404 inset: 0;
405 pointer-events: none;
406 opacity: .04;
407 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
408 }
409 .stage {
410 position: relative;
411 z-index: 1;
412 width: min(520px, 100%);
413 text-align: center;
414 animation: enter .8s cubic-bezier(.22,1,.36,1) both;
415 }
416 .visual {
417 width: 168px;
418 height: 168px;
419 margin: 0 auto 28px;
420 position: relative;
421 animation: float 5.5s ease-in-out infinite;
422 }
423 .visual svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 18px 28px rgba(47,143,115,.18)); }
424 .ring {
425 position: absolute;
426 inset: -10px;
427 border-radius: 50%;
428 border: 1.5px dashed rgba(47,143,115,.28);
429 animation: spin 28s linear infinite;
430 }
431 .brand {
432 font-family: Fraunces, "Noto Serif SC", Georgia, serif;
433 font-size: clamp(34px, 7vw, 48px);
434 font-weight: 700;
435 letter-spacing: -.02em;
436 line-height: 1;
437 margin: 0 0 8px;
438 color: var(--ink);
439 }
440 .brand em {
441 font-style: normal;
442 background: linear-gradient(120deg, var(--accent), #4aa888 45%, #c4a35a);
443 -webkit-background-clip: text;
444 background-clip: text;
445 color: transparent;
446 }
447 .domain {
448 margin: 0 0 28px;
449 font-size: 13px;
450 letter-spacing: .16em;
451 text-transform: uppercase;
452 color: var(--muted);
453 font-weight: 500;
454 }
455 h1 {
456 margin: 0 0 12px;
457 font-size: clamp(24px, 4.5vw, 32px);
458 font-weight: 700;
459 letter-spacing: .02em;
460 }
461 .message {
462 margin: 0 auto;
463 max-width: 34em;
464 font-size: 16px;
465 line-height: 1.75;
466 color: var(--muted);
467 white-space: pre-wrap;
468 font-weight: 400;
469 }
470 .progress {
471 width: min(220px, 70%);
472 height: 4px;
473 margin: 28px auto 0;
474 border-radius: 999px;
475 background: rgba(47,143,115,.12);
476 overflow: hidden;
477 }
478 .progress > i {
479 display: block;
480 height: 100%;
481 width: 42%;
482 border-radius: inherit;
483 background: linear-gradient(90deg, var(--accent-soft), var(--accent), #c4a35a);
484 animation: slide 2.4s ease-in-out infinite;
485 }
486 .until {
487 display: inline-flex;
488 align-items: center;
489 gap: 10px;
490 margin: 22px 0 0;
491 padding: 8px 14px;
492 border-radius: 999px;
493 background: rgba(255,255,255,.55);
494 border: 1px solid rgba(47,143,115,.16);
495 font-size: 13px;
496 color: var(--muted);
497 backdrop-filter: blur(8px);
498 }
499 .until span { opacity: .75; }
500 .until strong { color: var(--ink); font-weight: 600; }
501 .hint {
502 margin: 26px 0 0;
503 font-size: 12px;
504 color: rgba(90,111,104,.72);
505 }
506 @keyframes enter {
507 from { opacity: 0; transform: translateY(18px) scale(.98); }
508 to { opacity: 1; transform: none; }
509 }
510 @keyframes float {
511 0%, 100% { transform: translateY(0); }
512 50% { transform: translateY(-10px); }
513 }
514 @keyframes drift {
515 0%, 100% { transform: translate(0, 0) scale(1); }
516 50% { transform: translate(24px, -18px) scale(1.08); }
517 }
518 @keyframes spin { to { transform: rotate(360deg); } }
519 @keyframes slide {
520 0% { transform: translateX(-120%); }
521 100% { transform: translateX(280%); }
522 }
523 @media (prefers-reduced-motion: reduce) {
524 .blob, .visual, .ring, .progress > i, .stage { animation: none !important; }
525 }
526 </style>
527</head>
528<body>
529 <div class="blob blob-a" aria-hidden="true"></div>
530 <div class="blob blob-b" aria-hidden="true"></div>
531 <div class="blob blob-c" aria-hidden="true"></div>
532 <div class="grain" aria-hidden="true"></div>
533 <main class="stage">
534 <div class="visual" aria-hidden="true">
535 <div class="ring"></div>
536 <svg viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
537 <defs>
538 <linearGradient id="g1" x1="28" y1="24" x2="140" y2="148" gradientUnits="userSpaceOnUse">
539 <stop stop-color="#E8F7F1"/>
540 <stop offset="1" stop-color="#B7E0D2"/>
541 </linearGradient>
542 <linearGradient id="g2" x1="56" y1="62" x2="118" y2="118" gradientUnits="userSpaceOnUse">
543 <stop stop-color="#2F8F73"/>
544 <stop offset="1" stop-color="#C4A35A"/>
545 </linearGradient>
546 </defs>
547 <circle cx="84" cy="84" r="72" fill="url(#g1)"/>
548 <circle cx="84" cy="84" r="54" fill="#F8FBF9" fill-opacity=".72"/>
549 <path d="M58 92c8-18 22-28 36-28s28 10 36 28" stroke="url(#g2)" stroke-width="5" stroke-linecap="round"/>
550 <path d="M70 78c4-8 10-12 14-12s10 4 14 12" stroke="#2F8F73" stroke-width="4" stroke-linecap="round" opacity=".55"/>
551 <circle cx="84" cy="104" r="7" fill="#2F8F73"/>
552 <path d="M84 111v18" stroke="#2F8F73" stroke-width="4" stroke-linecap="round"/>
553 <path d="M74 126h20" stroke="#C4A35A" stroke-width="4" stroke-linecap="round"/>
554 <circle cx="118" cy="58" r="5" fill="#C4A35A" opacity=".8"/>
555 <circle cx="52" cy="64" r="3.5" fill="#2F8F73" opacity=".45"/>
556 </svg>
557 </div>
558 <p class="brand">One <em>Blog</em></p>
559 <p class="domain">onedayxyy.cn</p>
560 <h1>${title}</h1>
561 <p class="message">${message}</p>
562 <div class="progress" aria-hidden="true"><i></i></div>
563 ${untilBlock}
564 <p class="hint">若仍看到旧页面,请强制刷新或使用无痕窗口</p>
565 </main>
566</body>
567</html>`;
568}
569
570function buildMaintenanceResponse(status) {
571 const html = buildMaintenanceHtml(status);
572 return new Response(html, {
573 status: 503,
574 headers: {
575 'Content-Type': 'text/html; charset=utf-8',
576 'Cache-Control': 'no-store, no-cache, must-revalidate',
577 Pragma: 'no-cache',
578 'Retry-After': '300',
579 'X-OneBlog-Maintenance': '1',
580 },
581 });
582}效果:

问题:
这里的标签页自己logo图片怎么没得?

回答:
因为维护模式把所有请求都拦成了维护 HTML,浏览器去拉 /favicon.ico 时拿到的也是网页,不是图标;维护页里原先也没写 <link rel="icon">。
已修好两处:
/favicon.ico 等路径请把更新后的 edgeone/blog-cnb-origin.js 在 EdgeOne 覆盖发布;发布后强制刷新标签页即可看到 logo。
edgeone/blog-cnb-origin.js内容:
1/**
2 * EdgeOne 边缘函数:从 CNB 公开产物库拉取静态站
3 *
4 * 用法:复制到腾讯云 EdgeOne 控制台 → 站点 → 边缘函数(整文件覆盖)
5 *
6 * 相对旧版改进:
7 * 1. HTML/CSS/JS 等文本资源做 gzip(缓冲后再压,避免流式 CompressionStream 在 EO 静默失败)
8 * 2. 指纹静态资源长缓存;HTML 短缓存
9 * 3. 响应头 X-EO-Compress 便于确认压缩是否生效
10 * 4. 找不到页面时回退到产物库中的 404.html(Hugo 自定义 404)
11 * 5. 维护模式:轮询 Admin 公开状态;开启时对所有请求返回 503 维护页
12 */
13addEventListener('fetch', (event) => {
14 event.respondWith(dispatch(event.request));
15});
16
17const BASE = 'https://cnb.cool/onedayxyy/hugo-teek-public-dist/-/git/raw/dist';
18const MAINTENANCE_STATUS_URL = 'https://oneblogadmin.onedayxyy.cn/api/maintenance-status';
19const MAINTENANCE_CACHE_TTL_MS = 5000;
20const MAINTENANCE_FETCH_TIMEOUT_MS = 2500;
21
22async function dispatch(request) {
23 const maintResult = await getMaintenanceStatus();
24 const pathname = new URL(request.url).pathname;
25 // 维护模式下仍放行站点图标,避免标签页变成空白文档图标
26 if (maintResult.data && maintResult.data.enabled && !isFaviconPath(pathname)) {
27 return buildMaintenanceResponse(maintResult.data, maintResult.source);
28 }
29 const response = await handleRequest(request);
30 return withMaintProbe(response, maintResult);
31}
32
33async function handleRequest(request) {
34 const url = new URL(request.url);
35 let pathname = url.pathname;
36
37 if (pathname !== '/' && !pathname.endsWith('/') && !isFile(pathname)) {
38 const redirectUrl = new URL(request.url);
39 redirectUrl.pathname = pathname + '/';
40 return Response.redirect(redirectUrl.toString(), 301);
41 }
42
43 let uri = pathname;
44 if (uri.startsWith('/')) uri = uri.slice(1);
45 if (!uri || uri === '') uri = 'index.html';
46 if (uri.endsWith('/')) uri = uri + 'index.html';
47
48 let target = `${BASE}/${uri}`;
49 let resp = await fetchOrigin(target);
50
51 if (!resp.ok && !isFile(uri)) {
52 const indexUri = uri.endsWith('/') ? uri + 'index.html' : uri + '/index.html';
53 target = `${BASE}/${indexUri}`;
54 resp = await fetchOrigin(target);
55 if (resp.ok) uri = indexUri;
56 }
57
58 if (!resp.ok || !resp.body) {
59 // 回退到 Hugo 构建的自定义 404 页,避免纯文本 "Page Not Found"
60 const notFound = await fetchOrigin(`${BASE}/404.html`);
61 if (notFound.ok && notFound.body) {
62 const rawBuf = await notFound.arrayBuffer();
63 const contentType = 'text/html; charset=utf-8';
64 const headers = new Headers();
65 headers.set('Content-Type', contentType);
66 headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
67 headers.set('Content-Length', String(rawBuf.byteLength));
68 const uncompressed = new Response(rawBuf, {
69 status: 404,
70 headers,
71 });
72 return maybeGzip(request, uncompressed, '404.html', rawBuf, contentType);
73 }
74
75 return new Response('Page Not Found', {
76 status: 404,
77 headers: { 'Content-Type': 'text/html; charset=utf-8' },
78 });
79 }
80
81 const contentType =
82 getContentType(uri) || resp.headers.get('content-type') || 'text/html; charset=utf-8';
83
84 // 大体积 CSS/JS(如 main.css ~1MB)若整包 arrayBuffer + gzip,EO 易直接 545。
85 // 指纹静态资源改为直通,交给平台智能压缩。
86 const originLen = Number(resp.headers.get('content-length') || 0);
87 if (shouldPassthrough(uri, contentType, originLen)) {
88 const headers = new Headers();
89 headers.set('Content-Type', contentType);
90 headers.set('Cache-Control', cacheControlFor(uri));
91 if (originLen > 0) headers.set('Content-Length', String(originLen));
92 headers.set('X-EO-Compress', 'passthrough');
93 return new Response(resp.body, { status: resp.status, headers });
94 }
95
96 // 先读入缓冲:EO 上对流式 pipeThrough(CompressionStream) 经常不生效
97 let rawBuf;
98 try {
99 rawBuf = await resp.arrayBuffer();
100 } catch (e) {
101 const headers = new Headers();
102 headers.set('Content-Type', contentType);
103 headers.set('Cache-Control', cacheControlFor(uri));
104 headers.set('X-EO-Compress', 'passthrough-read-fail');
105 const retry = await fetchOrigin(target);
106 return new Response(retry.body, { status: retry.status, headers });
107 }
108
109 // 读完后发现超大:不再 gzip,避免边缘函数 OOM/545
110 if (rawBuf.byteLength > PASS_THROUGH_MAX_BYTES) {
111 const headers = new Headers();
112 headers.set('Content-Type', contentType);
113 headers.set('Cache-Control', cacheControlFor(uri));
114 headers.set('Content-Length', String(rawBuf.byteLength));
115 headers.set('X-EO-Compress', 'skip-large');
116 return new Response(rawBuf, { status: resp.status, headers });
117 }
118
119 const headers = new Headers();
120 headers.set('Content-Type', contentType);
121 headers.set('Cache-Control', cacheControlFor(uri));
122 headers.set('Content-Length', String(rawBuf.byteLength));
123
124 const uncompressed = new Response(rawBuf, {
125 status: resp.status,
126 headers,
127 });
128
129 return maybeGzip(request, uncompressed, uri, rawBuf, contentType);
130}
131
132/** 超过该体积不再做边缘 gzip 缓冲(main.css 约 1MB) */
133const PASS_THROUGH_MAX_BYTES = 400 * 1024;
134
135function shouldPassthrough(uri, contentType, contentLength) {
136 if (contentLength > PASS_THROUGH_MAX_BYTES) return true;
137 // 指纹 CSS/JS 体积常偏大,优先直通
138 if (/\.min\.[a-f0-9]{8,}\.(?:css|js)(?:$|\?)/i.test(uri)) {
139 if (contentLength === 0) return true; // 未知长度时也不冒险缓冲
140 if (contentLength > 200 * 1024) return true;
141 }
142 return false;
143}
144
145function fetchOrigin(target) {
146 return fetch(target, {
147 headers: {
148 'User-Agent': 'curl/8.0.0',
149 Accept: '*/*',
150 'Accept-Encoding': 'identity',
151 },
152 });
153}
154
155function isFile(path) {
156 return /\.[^/]+$/.test(path);
157}
158
159function isFaviconPath(pathname) {
160 const p = String(pathname || '').toLowerCase();
161 return (
162 p === '/favicon.ico' ||
163 p === '/favicon.png' ||
164 p === '/favicon.svg' ||
165 p === '/apple-touch-icon.png' ||
166 p === '/apple-touch-icon-precomposed.png' ||
167 p.endsWith('/favicon.ico')
168 );
169}
170
171function getContentType(uri) {
172 const map = {
173 html: 'text/html; charset=utf-8',
174 css: 'text/css; charset=utf-8',
175 js: 'application/javascript; charset=utf-8',
176 mjs: 'application/javascript; charset=utf-8',
177 json: 'application/json; charset=utf-8',
178 png: 'image/png',
179 jpg: 'image/jpeg',
180 jpeg: 'image/jpeg',
181 gif: 'image/gif',
182 svg: 'image/svg+xml',
183 webp: 'image/webp',
184 avif: 'image/avif',
185 ico: 'image/x-icon',
186 woff: 'font/woff',
187 woff2: 'font/woff2',
188 txt: 'text/plain; charset=utf-8',
189 xml: 'application/xml; charset=utf-8',
190 map: 'application/json',
191 };
192 if (!isFile(uri)) return 'text/html; charset=utf-8';
193 const ext = uri.split('.').pop().toLowerCase();
194 return map[ext];
195}
196
197function cacheControlFor(uri) {
198 if (/\.(?:css|js|mjs|woff2?|png|jpe?g|gif|webp|avif|svg|ico)(?:$|\?)/i.test(uri)) {
199 return 'public, max-age=31536000, immutable';
200 }
201 return 'public, max-age=60, must-revalidate';
202}
203
204function isCompressible(uri, contentType) {
205 if (/\.(?:png|jpe?g|gif|webp|avif|woff2?|mp4|webm|gz|br)(?:$|\?)/i.test(uri)) {
206 return false;
207 }
208 const type = contentType || '';
209 return /text\/|javascript|json|xml|svg/.test(type);
210}
211
212async function compressGzip(rawBuf) {
213 if (typeof CompressionStream === 'undefined') {
214 return { ok: false, reason: 'no-compression-stream' };
215 }
216 try {
217 const cs = new CompressionStream('gzip');
218 const writer = cs.writable.getWriter();
219 await writer.write(new Uint8Array(rawBuf));
220 await writer.close();
221 const compressed = await new Response(cs.readable).arrayBuffer();
222 if (!compressed || compressed.byteLength < 2) {
223 return { ok: false, reason: 'empty-output' };
224 }
225 // gzip magic 1F 8B
226 const u8 = new Uint8Array(compressed);
227 if (u8[0] !== 0x1f || u8[1] !== 0x8b) {
228 return { ok: false, reason: 'bad-magic' };
229 }
230 return { ok: true, buf: compressed };
231 } catch (e) {
232 return { ok: false, reason: 'err:' + (e && e.message ? String(e.message).slice(0, 80) : 'unknown') };
233 }
234}
235
236async function maybeGzip(request, response, uri, rawBuf, contentType) {
237 const accept = (request.headers.get('Accept-Encoding') || '').toLowerCase();
238 // EdgeOne 常会剥掉/改写传入边缘函数的 Accept-Encoding,导致误判 skip-client。
239 // 现代浏览器都支持 gzip:仅当明确只要 identity 时才跳过。
240 const identityOnly =
241 accept === 'identity' ||
242 (/identity/.test(accept) && !/gzip|\*|br/.test(accept));
243 if (identityOnly) {
244 const h = new Headers(response.headers);
245 h.set('X-EO-Compress', 'skip-identity');
246 return new Response(rawBuf, { status: response.status, headers: h });
247 }
248 if (!isCompressible(uri, contentType)) {
249 const h = new Headers(response.headers);
250 h.set('X-EO-Compress', 'skip-type');
251 return new Response(rawBuf, { status: response.status, headers: h });
252 }
253
254 const result = await compressGzip(rawBuf);
255 if (!result.ok) {
256 const h = new Headers(response.headers);
257 h.set('X-EO-Compress', result.reason || 'fail');
258 // 不设 Vary,尽量让 EO「智能压缩」有机会接管
259 return new Response(rawBuf, { status: response.status, headers: h });
260 }
261
262 const headers = new Headers(response.headers);
263 headers.set('Content-Encoding', 'gzip');
264 headers.set('Content-Length', String(result.buf.byteLength));
265 headers.set('Vary', 'Accept-Encoding');
266 headers.set(
267 'X-EO-Compress',
268 'gzip-ok;ae=' + (accept ? accept.slice(0, 40) : 'missing')
269 );
270 return new Response(result.buf, { status: response.status, headers });
271}
272
273// ==================== 站点维护模式 ====================
274// fail-open:状态接口失败/超时则照常提供站点,避免 Admin 故障锁死主站。
275// 仅用内存短缓存;不用 Cache API(EdgeOne 上 max-age 可能不失效,会把旧的 enabled:false 粘住)。
276
277let maintenanceMemoryCache = { at: 0, result: null };
278
279/**
280 * @returns {{ data: object|null, source: string }}
281 * source: memory | fetch | http-<code> | timeout | error | empty
282 */
283async function getMaintenanceStatus() {
284 const now = Date.now();
285 if (
286 maintenanceMemoryCache.result &&
287 now - maintenanceMemoryCache.at < MAINTENANCE_CACHE_TTL_MS
288 ) {
289 return {
290 data: maintenanceMemoryCache.result.data,
291 source: 'memory',
292 };
293 }
294
295 // 不用 AbortController:部分 EO 运行时对 signal 支持不完整,改为 Promise.race 超时
296 let timer = null;
297 let fetchPromise = null;
298 try {
299 fetchPromise = fetch(MAINTENANCE_STATUS_URL, {
300 method: 'GET',
301 headers: {
302 Accept: 'application/json',
303 'Cache-Control': 'no-cache',
304 Pragma: 'no-cache',
305 },
306 redirect: 'follow',
307 });
308 const timeoutPromise = new Promise((_, reject) => {
309 timer = setTimeout(
310 () => reject(new Error('maintenance-status-timeout')),
311 MAINTENANCE_FETCH_TIMEOUT_MS
312 );
313 });
314 const resp = await Promise.race([fetchPromise, timeoutPromise]);
315 if (!resp || !resp.ok) {
316 const result = {
317 data: null,
318 source: resp ? `http-${resp.status}` : 'empty',
319 };
320 maintenanceMemoryCache = { at: now, result };
321 return result;
322 }
323 const data = await resp.json();
324 const result = { data, source: 'fetch' };
325 maintenanceMemoryCache = { at: now, result };
326 return result;
327 } catch (err) {
328 const msg = err && err.message ? String(err.message) : '';
329 const result = {
330 data: null,
331 source: msg.indexOf('timeout') >= 0 ? 'timeout' : 'error',
332 };
333 maintenanceMemoryCache = { at: now, result };
334 return result;
335 } finally {
336 if (timer) clearTimeout(timer);
337 // 超时获胜时避免未处理的 fetch rejection
338 if (fetchPromise) fetchPromise.catch(() => {});
339 }
340}
341
342/** 给普通响应打诊断头。必须原地改 headers,禁止 new Response(body) 重建:
343 * EdgeOne 重建带 Content-Encoding:gzip 的响应对 body 可能已是明文,浏览器会白屏。 */
344function withMaintProbe(response, maintResult) {
345 const enabled = !!(maintResult && maintResult.data && maintResult.data.enabled);
346 const source = (maintResult && maintResult.source) || 'unknown';
347 let probe = 'off';
348 if (source === 'fetch' || source === 'memory') {
349 probe = enabled ? 'on' : 'off';
350 } else {
351 probe = `fail:${source}`;
352 }
353 try {
354 response.headers.set('X-OneBlog-Maint', probe);
355 response.headers.set('X-OneBlog-Maint-Src', source);
356 return response;
357 } catch (_) {
358 // 极端情况下 headers 只读:宁可不打诊断头,也不重建 body(避免 gzip 错乱)
359 return response;
360 }
361}
362
363function escapeHtml(str) {
364 return String(str || '')
365 .replace(/&/g, '&')
366 .replace(/</g, '<')
367 .replace(/>/g, '>')
368 .replace(/"/g, '"')
369 .replace(/'/g, ''');
370}
371
372function buildMaintenanceHtml(status) {
373 const title = escapeHtml(status.title || '站点维护中');
374 const message = escapeHtml(status.message || '正在升级,马上回来。');
375 const until = status.until ? escapeHtml(status.until) : '';
376 const untilBlock = until
377 ? `<p class="until"><span>预计恢复</span><strong>${until}</strong></p>`
378 : '';
379
380 return `<!DOCTYPE html>
381<html lang="zh-CN">
382<head>
383 <meta charset="utf-8">
384 <meta name="viewport" content="width=device-width, initial-scale=1">
385 <meta name="robots" content="noindex">
386 <title>${title} · One Blog</title>
387 <link rel="icon" type="image/webp" href="https://oneimg.onedayxyy.cn/images/site-icons/xyy-logo_58fed615.avif?w=150&h=150&fit=crop&fm=webp&q=80">
388 <link rel="apple-touch-icon" href="https://oneimg.onedayxyy.cn/images/site-icons/xyy-logo_58fed615.avif?w=180&h=180&fit=crop&fm=webp&q=80">
389 <link rel="preconnect" href="https://fonts.googleapis.com">
390 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
391 <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
392 <style>
393 :root {
394 --ink: #1a2e2a;
395 --muted: #5a6f68;
396 --accent: #2f8f73;
397 --accent-soft: #7bc4a8;
398 --paper: #f6f3eb;
399 --glow: #d8efe4;
400 }
401 * { box-sizing: border-box; }
402 html, body { height: 100%; margin: 0; }
403 body {
404 min-height: 100%;
405 display: grid;
406 place-items: center;
407 padding: 28px 20px;
408 color: var(--ink);
409 font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
410 background:
411 radial-gradient(ellipse 80% 55% at 18% 12%, #dff3ea 0%, transparent 55%),
412 radial-gradient(ellipse 70% 50% at 88% 88%, #efe4c8 0%, transparent 50%),
413 radial-gradient(ellipse 50% 40% at 70% 20%, #cfe8de 0%, transparent 45%),
414 linear-gradient(165deg, #eef6f2 0%, var(--paper) 48%, #ebe4d6 100%);
415 overflow: hidden;
416 position: relative;
417 }
418 .blob {
419 position: absolute;
420 border-radius: 50%;
421 filter: blur(48px);
422 opacity: .55;
423 pointer-events: none;
424 animation: drift 14s ease-in-out infinite;
425 }
426 .blob-a {
427 width: 340px; height: 340px;
428 left: -80px; top: -40px;
429 background: #9fd9c2;
430 }
431 .blob-b {
432 width: 280px; height: 280px;
433 right: -60px; bottom: -30px;
434 background: #e8d5a3;
435 animation-delay: -5s;
436 animation-duration: 18s;
437 }
438 .blob-c {
439 width: 180px; height: 180px;
440 left: 42%; top: 62%;
441 background: #b7e0d2;
442 animation-delay: -9s;
443 animation-duration: 16s;
444 }
445 .grain {
446 position: absolute;
447 inset: 0;
448 pointer-events: none;
449 opacity: .04;
450 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
451 }
452 .stage {
453 position: relative;
454 z-index: 1;
455 width: min(520px, 100%);
456 text-align: center;
457 animation: enter .8s cubic-bezier(.22,1,.36,1) both;
458 }
459 .visual {
460 width: 168px;
461 height: 168px;
462 margin: 0 auto 28px;
463 position: relative;
464 animation: float 5.5s ease-in-out infinite;
465 }
466 .visual svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 18px 28px rgba(47,143,115,.18)); }
467 .ring {
468 position: absolute;
469 inset: -10px;
470 border-radius: 50%;
471 border: 1.5px dashed rgba(47,143,115,.28);
472 animation: spin 28s linear infinite;
473 }
474 .brand {
475 font-family: Fraunces, "Noto Serif SC", Georgia, serif;
476 font-size: clamp(34px, 7vw, 48px);
477 font-weight: 700;
478 letter-spacing: -.02em;
479 line-height: 1;
480 margin: 0 0 8px;
481 color: var(--ink);
482 }
483 .brand em {
484 font-style: normal;
485 background: linear-gradient(120deg, var(--accent), #4aa888 45%, #c4a35a);
486 -webkit-background-clip: text;
487 background-clip: text;
488 color: transparent;
489 }
490 .domain {
491 margin: 0 0 28px;
492 font-size: 13px;
493 letter-spacing: .16em;
494 text-transform: uppercase;
495 color: var(--muted);
496 font-weight: 500;
497 }
498 h1 {
499 margin: 0 0 12px;
500 font-size: clamp(24px, 4.5vw, 32px);
501 font-weight: 700;
502 letter-spacing: .02em;
503 }
504 .message {
505 margin: 0 auto;
506 max-width: 34em;
507 font-size: 16px;
508 line-height: 1.75;
509 color: var(--muted);
510 white-space: pre-wrap;
511 font-weight: 400;
512 }
513 .progress {
514 width: min(220px, 70%);
515 height: 4px;
516 margin: 28px auto 0;
517 border-radius: 999px;
518 background: rgba(47,143,115,.12);
519 overflow: hidden;
520 }
521 .progress > i {
522 display: block;
523 height: 100%;
524 width: 42%;
525 border-radius: inherit;
526 background: linear-gradient(90deg, var(--accent-soft), var(--accent), #c4a35a);
527 animation: slide 2.4s ease-in-out infinite;
528 }
529 .until {
530 display: inline-flex;
531 align-items: center;
532 gap: 10px;
533 margin: 22px 0 0;
534 padding: 8px 14px;
535 border-radius: 999px;
536 background: rgba(255,255,255,.55);
537 border: 1px solid rgba(47,143,115,.16);
538 font-size: 13px;
539 color: var(--muted);
540 backdrop-filter: blur(8px);
541 }
542 .until span { opacity: .75; }
543 .until strong { color: var(--ink); font-weight: 600; }
544 .hint {
545 margin: 26px 0 0;
546 font-size: 12px;
547 color: rgba(90,111,104,.72);
548 }
549 @keyframes enter {
550 from { opacity: 0; transform: translateY(18px) scale(.98); }
551 to { opacity: 1; transform: none; }
552 }
553 @keyframes float {
554 0%, 100% { transform: translateY(0); }
555 50% { transform: translateY(-10px); }
556 }
557 @keyframes drift {
558 0%, 100% { transform: translate(0, 0) scale(1); }
559 50% { transform: translate(24px, -18px) scale(1.08); }
560 }
561 @keyframes spin { to { transform: rotate(360deg); } }
562 @keyframes slide {
563 0% { transform: translateX(-120%); }
564 100% { transform: translateX(280%); }
565 }
566 @media (prefers-reduced-motion: reduce) {
567 .blob, .visual, .ring, .progress > i, .stage { animation: none !important; }
568 }
569 </style>
570</head>
571<body>
572 <div class="blob blob-a" aria-hidden="true"></div>
573 <div class="blob blob-b" aria-hidden="true"></div>
574 <div class="blob blob-c" aria-hidden="true"></div>
575 <div class="grain" aria-hidden="true"></div>
576 <main class="stage">
577 <div class="visual" aria-hidden="true">
578 <div class="ring"></div>
579 <svg viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
580 <defs>
581 <linearGradient id="g1" x1="28" y1="24" x2="140" y2="148" gradientUnits="userSpaceOnUse">
582 <stop stop-color="#E8F7F1"/>
583 <stop offset="1" stop-color="#B7E0D2"/>
584 </linearGradient>
585 <linearGradient id="g2" x1="56" y1="62" x2="118" y2="118" gradientUnits="userSpaceOnUse">
586 <stop stop-color="#2F8F73"/>
587 <stop offset="1" stop-color="#C4A35A"/>
588 </linearGradient>
589 </defs>
590 <circle cx="84" cy="84" r="72" fill="url(#g1)"/>
591 <circle cx="84" cy="84" r="54" fill="#F8FBF9" fill-opacity=".72"/>
592 <path d="M58 92c8-18 22-28 36-28s28 10 36 28" stroke="url(#g2)" stroke-width="5" stroke-linecap="round"/>
593 <path d="M70 78c4-8 10-12 14-12s10 4 14 12" stroke="#2F8F73" stroke-width="4" stroke-linecap="round" opacity=".55"/>
594 <circle cx="84" cy="104" r="7" fill="#2F8F73"/>
595 <path d="M84 111v18" stroke="#2F8F73" stroke-width="4" stroke-linecap="round"/>
596 <path d="M74 126h20" stroke="#C4A35A" stroke-width="4" stroke-linecap="round"/>
597 <circle cx="118" cy="58" r="5" fill="#C4A35A" opacity=".8"/>
598 <circle cx="52" cy="64" r="3.5" fill="#2F8F73" opacity=".45"/>
599 </svg>
600 </div>
601 <p class="brand">One <em>Blog</em></p>
602 <p class="domain">onedayxyy.cn</p>
603 <h1>${title}</h1>
604 <p class="message">${message}</p>
605 <div class="progress" aria-hidden="true"><i></i></div>
606 ${untilBlock}
607 <p class="hint">若仍看到旧页面,请强制刷新或使用无痕窗口</p>
608 </main>
609</body>
610</html>`;
611}
612
613function buildMaintenanceResponse(status, source) {
614 const html = buildMaintenanceHtml(status);
615 return new Response(html, {
616 status: 503,
617 headers: {
618 'Content-Type': 'text/html; charset=utf-8',
619 'Cache-Control': 'no-store, no-cache, must-revalidate',
620 Pragma: 'no-cache',
621 'Retry-After': '300',
622 'X-OneBlog-Maintenance': '1',
623 'X-OneBlog-Maint': 'on',
624 'X-OneBlog-Maint-Src': source || 'fetch',
625 },
626 });
627}效果:

结束。
精选 · 友链 · 更多

One的公众号
爱折腾博客的小白