跳到主要内容

OneBlog的rss

最后更新于:

需求

1请给我当前博客实现rss功能
2
3rss订阅地址是博客域名后面+/feed
4
5https://onedayxyy.cn/feed 

RSS 已接好,订阅地址为 https://onedayxyy.cn/feed

做了什么

  1. 开启 RSS 输出user-configuration/hugo.toml):首页生成 feed.xml,最多 30 篇
  2. 自定义模板layouts/index.rss.xml):只收录与首页相同的博客文章(排除朋友圈等)
  3. 页面入口<head> 增加 alternate 链接;博主卡片增加 RSS 图标
  4. 边缘函数edgeone/blog-cnb-origin.js):/feed → 拉取 feed.xml,Content-Type 为 application/rss+xml

上线还需两步

  1. 重新构建并发布站点(生成 feed.xml
  2. 把更新后的 edgeone/blog-cnb-origin.js 整文件覆盖到 EdgeOne 边缘函数并发布(否则 /feed 仍会 301 到 /feed/ 后 404)

本地也可直接访问 /feed.xml 验证构建产物。需要的话我可以继续帮你提交 commit。


已推送到 origin/masterba7d3bfa 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, '&amp;')
420    .replace(/</g, '&lt;')
421    .replace(/>/g, '&gt;')
422    .replace(/"/g, '&quot;')
423    .replace(/'/g, '&#39;');
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}

验证

https://onedayxyy.cn/feed

最新文章

本页导航

文档导航