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

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

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