Jekyll 博客添加 PV 统计与评论区

Jekyll 博客添加 PV 统计与评论区

本教程介绍如何为你的 Jekyll 博客添加两个实用功能:文章阅读统计(PV Counter) 和 评论区(Giscus)。


1. PV 阅读统计

1.1 方案说明

使用 Cloudflare Workers + Durable Objects 实现原子计数,每个页面路径对应一个独立的 Durable Object 实例,避免并发冲突。

1.2 创建 Cloudflare Worker

1.2.1 步骤 1:登录 Cloudflare Dashboard

访问 https://dash.cloudflare.com 登录你的账号。

1.2.2 步骤 2:创建 Worker 项目

在本地创建一个新目录(例如 worker/),初始化 Wrangler 项目:

mkdir worker && cd worker npx wrangler init jekyll-pv-do

1.2.3 步骤 3:编写 Worker 代码

创建 worker/index.js:

export class PageViewCounter { constructor(state, env) { this.state = state this.data = { pageViews: {}, total: 0 } this.storagePromise = this.state.storage.get('data').then(d => { if (d) this.data = d }) } async fetch(request) { await this.storagePromise const url = new URL(request.url) const page = url.searchParams.get('page') || '/' if (request.method === 'POST') { if (!this.data.pageViews[page]) { this.data.pageViews[page] = 0 } this.data.pageViews[page] += 1 this.data.total += 1 this.state.storage.put('data', this.data) } return new Response(JSON.stringify({ page, count: this.data.pageViews[page] || 0, total: this.data.total }), { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }) } } export default { async fetch(request, env) { const url = new URL(request.url) if (url.pathname !== '/track') { return new Response('Not Found', { status: 404 }) } const origin = request.headers.get('Origin') || '' const referer = request.headers.get('Referer') || '' const allowedHosts = ['yourdomain.com'] const isAllowed = allowedHosts.some(h => origin.includes(h) || referer.includes(h)) if (!isAllowed) { return new Response('Forbidden', { status: 403 }) } const page = url.searchParams.get('page') || '/' const id = env.PV_COUNTER.idFromName(page) const stub = env.PV_COUNTER.get(id) return stub.fetch(request) } }

注意:将 allowedHosts 数组中的 yourdomain.com 替换为你的实际域名。

1.2.4 步骤 4:配置 Wrangler

创建 worker/wrangler.toml:

name = "jekyll-pv-do" main = "index.js" compatibility_date = "2025-04-05" [[durable_objects]] name = "PV_COUNTER" class_name = "PageViewCounter" [[migrations]] tag = "v1" new_sqlite_classes = ["PageViewCounter"]

1.2.5 步骤 5:部署 Worker

cd worker npx wrangler deploy

部署成功后,你会看到一个默认域名(如 jekyll-pv-do.your-account.workers.dev)。

1.3 配置自定义域名(可选)

在 Cloudflare Dashboard 中:

  1. 进入 Workers & Pages → 选择你的 Worker
  2. 点击 Triggers → Add Custom Domain
  3. 输入你的子域名(如 pv.yourdomain.com)
  4. 在 DNS 设置中添加 CNAME 记录指向 Worker 域名

1.4 在 Jekyll 中添加 PV 统计

1.4.1 步骤 1:创建 PV 统计组件

创建 _includes/pv-counter.html:

<div class="pv-counter" id="pv-counter"> <span class="pv-item">阅读 <span id="pv-count" class="pv-num">—</span></span> <span class="pv-sep">·</span> <span class="pv-item">全站 <span id="pv-total" class="pv-num">—</span></span> </div> <script> (function() { var apiUrl = 'https://jekyll-pv-do.yourdomain.com/track'; var page = window.location.pathname; function render(count, total) { document.getElementById('pv-count').textContent = count; document.getElementById('pv-total').textContent = total; } fetch(apiUrl + '?page=' + encodeURIComponent(page), { method: 'POST', credentials: 'omit' }).then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) .catch(function() { fetch(apiUrl + '?page=' + encodeURIComponent(page) + '&dry=1') .then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) }); })(); </script> <style> .pv-counter { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--text-muted-color, #6c757d); padding: 0.5rem 0; margin-top: 0.5rem; } .pv-num { font-weight: 600; color: var(--text-color, #212529); } .pv-sep { opacity: 0.5; } </style>

注意:将 apiUrl 中的域名替换为你的 Worker 域名。

1.4.2 步骤 2:在文章页面中引入

在 _layouts/post.html 中添加(在内容下方):

<div class="content"> <!doctype html> <!-- `site.alt_lang` can specify a language different from the UI --> <html lang="zh-CN" > <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1b1b1e"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="viewport" content="width=device-width, user-scalable=no initial-scale=1, shrink-to-fit=no, viewport-fit=cover" ><!-- Setup Open Graph image --> <!-- Begin Jekyll SEO tag v2.9.0 --> <meta name="generator" content="Jekyll v4.4.1" /> <meta property="og:title" content="Cloudflare Workers 自定义域名配置" /> <meta property="og:locale" content="zh_CN" /> <meta name="description" content="在 Cloudflare Workers 上绑定自定义域名,不是去 DNS 面板手写一条 CNAME 指向 *.workers.dev,而是在 Workers 侧声明自定义域名后,由 Cloudflare 自动帮你创建 DNS 记录 + 签发 SSL 证书。这条 DNS 记录直接把你的域名”指”向这个 Worker,绕开了传统的 CNAME 链条。" /> <meta name="twitter:description" property="og:description" content="在 Cloudflare Workers 上绑定自定义域名,不是去 DNS 面板手写一条 CNAME 指向 *.workers.dev,而是在 Workers 侧声明自定义域名后,由 Cloudflare 自动帮你创建 DNS 记录 + 签发 SSL 证书。这条 DNS 记录直接把你的域名”指”向这个 Worker,绕开了传统的 CNAME 链条。" /> <link rel="canonical" href="https://andyvictory.dpdns.org/docs/cloudflare/worker-custom-domain/" /> <meta property="og:url" content="https://andyvictory.dpdns.org/docs/cloudflare/worker-custom-domain/" /> <meta property="og:site_name" content="AndyVictory’s homepage" /> <meta property="og:type" content="article" /> <meta property="article:published_time" content="2026-08-08T02:00:00+00:00" /> <meta property="article:modified_time" content="2026-08-08T02:00:00+00:00" /> <meta name="twitter:card" content="summary" /> <meta name="twitter:title" content="Cloudflare Workers 自定义域名配置" /> <script type="application/ld+json"> {"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2026-08-08T02:00:00+00:00","datePublished":"2026-08-08T02:00:00+00:00","description":"在 Cloudflare Workers 上绑定自定义域名,不是去 DNS 面板手写一条 CNAME 指向 *.workers.dev,而是在 Workers 侧声明自定义域名后,由 Cloudflare 自动帮你创建 DNS 记录 + 签发 SSL 证书。这条 DNS 记录直接把你的域名”指”向这个 Worker,绕开了传统的 CNAME 链条。","headline":"Cloudflare Workers 自定义域名配置","mainEntityOfPage":{"@type":"WebPage","@id":"https://andyvictory.dpdns.org/docs/cloudflare/worker-custom-domain/"},"url":"https://andyvictory.dpdns.org/docs/cloudflare/worker-custom-domain/"}</script> <!-- End Jekyll SEO tag --> <title>Cloudflare Workers 自定义域名配置 | AndyVictory's homepage </title> <!-- The Favicons for Web, Android, Microsoft, and iOS (iPhone and iPad) Apps Generated by: https://realfavicongenerator.net/ --> <link rel="apple-touch-icon" sizes="180x180" href="/assets/img/favicons/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicons/favicon-16x16.png"> <link rel="shortcut icon" href="/assets/img/favicons/favicon.ico"> <meta name="apple-mobile-web-app-title" content="AndyVictory's homepage"> <meta name="application-name" content="AndyVictory's homepage"> <meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-config" content="/assets/img/favicons/browserconfig.xml"> <meta name="theme-color" content="#ffffff"> <!-- Resource Hints --> <link rel="preconnect" href="https://fonts.googleapis.com" > <link rel="dns-prefetch" href="https://fonts.googleapis.com" > <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="dns-prefetch" href="https://fonts.gstatic.com" > <link rel="preconnect" href="https://cdn.jsdelivr.net" > <link rel="dns-prefetch" href="https://cdn.jsdelivr.net" > <!-- Bootstrap --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"> <!-- Theme style --> <link rel="stylesheet" href="/assets/css/jekyll-theme-chirpy.css"> <!-- Web Font --> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Source+Sans+Pro:wght@400;600;700;900&display=swap"> <!-- Font Awesome Icons --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css"> <!-- 3rd-party Dependencies --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tocbot@4.27.20/dist/tocbot.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/loading-attribute-polyfill@2.1.1/dist/loading-attribute-polyfill.min.css"> <!-- Image Popup --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox@3.3.0/dist/css/glightbox.min.css"> <!-- JavaScript --> <!-- Switch the mode between dark and light. --> <script type="text/javascript"> class ModeToggle { static get MODE_KEY() { return 'mode'; } static get MODE_ATTR() { return 'data-mode'; } static get DARK_MODE() { return 'dark'; } static get LIGHT_MODE() { return 'light'; } static get ID() { return 'mode-toggle'; } constructor() { let self = this;this.sysDarkPrefers.addEventListener('change', () => { if (self.hasMode) { self.clearMode(); } self.notify(); }); if (!this.hasMode) { return; } if (this.isDarkMode) { this.setDark(); } else { this.setLight(); } } get sysDarkPrefers() { return window.matchMedia('(prefers-color-scheme: dark)'); } get isPreferDark() { return this.sysDarkPrefers.matches; } get isDarkMode() { return this.mode === ModeToggle.DARK_MODE; } get hasMode() { return this.mode != null; } get mode() { return sessionStorage.getItem(ModeToggle.MODE_KEY); }get modeStatus() { if (this.hasMode) { return this.mode; } else { return this.isPreferDark ? ModeToggle.DARK_MODE : ModeToggle.LIGHT_MODE; } } setDark() { document.documentElement.setAttribute(ModeToggle.MODE_ATTR, ModeToggle.DARK_MODE); sessionStorage.setItem(ModeToggle.MODE_KEY, ModeToggle.DARK_MODE); } setLight() { document.documentElement.setAttribute(ModeToggle.MODE_ATTR, ModeToggle.LIGHT_MODE); sessionStorage.setItem(ModeToggle.MODE_KEY, ModeToggle.LIGHT_MODE); } clearMode() { document.documentElement.removeAttribute(ModeToggle.MODE_ATTR); sessionStorage.removeItem(ModeToggle.MODE_KEY); }notify() { window.postMessage( { direction: ModeToggle.ID, message: this.modeStatus }, '*' ); } flipMode() { if (this.hasMode) { this.clearMode(); } else { if (this.isPreferDark) { this.setLight(); } else { this.setDark(); } } this.notify(); } } const modeToggle = new ModeToggle(); </script> <!-- A placeholder to allow defining custom metadata --> </head> <body> <!-- The Side Bar --> <aside aria-label="Sidebar" id="sidebar" class="d-flex flex-column align-items-end"> <header class="profile-wrapper"> <a href="/" id="avatar" class="rounded-circle"><img src="/assets/img/avatar.jpg" width="112" height="112" alt="avatar" onerror="this.style.display='none'"></a> <h1 class="site-title"> <a href="/">AndyVictory's homepage</a> </h1> <p class="site-subtitle fst-italic mb-0"></p> </header> <!-- .profile-wrapper --> <nav class="flex-column flex-grow-1 w-100 ps-0"> <ul class="nav"> <!-- home --> <li class="nav-item"> <a href="/" class="nav-link"> <i class="fa-fw fas fa-home"></i> <span>首页</span> </a> </li> <!-- the real tabs --> <li class="nav-item"> <a href="/tabs/docs.html" class="nav-link"> <i class="fa-fw fas fa-book"></i> <span>DOCS</span> </a> </li> <!-- .nav-item --> <li class="nav-item"> <a href="/tabs/tags.html" class="nav-link"> <i class="fa-fw fas fa-tags"></i> <span>标签</span> </a> </li> <!-- .nav-item --> <li class="nav-item"> <a href="/tabs/apps.html" class="nav-link"> <i class="fa-fw fas fa-mobile-alt"></i> <span>个人APP</span> </a> </li> <!-- .nav-item --> <li class="nav-item"> <a href="/tabs/games.html" class="nav-link"> <i class="fa-fw fas fa-gamepad"></i> <span>GAMES</span> </a> </li> <!-- .nav-item --> <li class="nav-item"> <a href="/tabs/blog.html" class="nav-link"> <i class="fa-fw fas fa-feather-alt"></i> <span>BLOG</span> </a> </li> <!-- .nav-item --> </ul> </nav> <div class="sidebar-bottom d-flex flex-wrap align-items-center w-100"> <button type="button" class="btn btn-link nav-link" aria-label="Switch Mode" id="mode-toggle"> <i class="fas fa-adjust"></i> </button> </div> <!-- .sidebar-bottom --> </aside> <!-- #sidebar --> <div id="main-wrapper" class="d-flex justify-content-center"> <div class="container d-flex flex-column px-xxl-5"> <!-- The Top Bar --> <header id="topbar-wrapper" aria-label="Top Bar"> <div id="topbar" class="d-flex align-items-center justify-content-between px-lg-3 h-100" > <nav id="breadcrumb" aria-label="Breadcrumb"> <span> <a href="/">首页</a> </span> <span>Cloudflare Workers 自定义域名配置</span> </nav> <!-- endof #breadcrumb --> <button type="button" id="sidebar-trigger" class="btn btn-link"> <i class="fas fa-bars fa-fw"></i> </button> <div id="topbar-title"> Cloudflare Workers 自定义域名配置 </div> <button type="button" id="search-trigger" class="btn btn-link"> <i class="fas fa-search fa-fw"></i> </button> <search id="search" class="align-items-center ms-3 ms-lg-0"> <i class="fas fa-search fa-fw"></i> <input class="form-control" id="search-input" type="search" aria-label="search" autocomplete="off" placeholder="搜索..." > </search> <button type="button" class="btn btn-link text-decoration-none" id="search-cancel">取消</button> </div> </header> <div class="row flex-grow-1"> <main aria-label="Main Content" class="col-12 col-lg-11 col-xl-9 px-md-4"> <!-- Refactor the HTML structure --> <!-- In order to allow a wide table to scroll horizontally, we suround the markdown table with `<div class="table-wrapper">` and `</div>` --> <!-- Fixed kramdown code highlight rendering: https://github.com/penibelst/jekyll-compress-html/issues/101 https://github.com/penibelst/jekyll-compress-html/issues/71#issuecomment-188144901 --> <!-- Change the icon of checkbox --> <!-- Handle images --> <!-- Add header for code snippets --> <!-- Create heading anchors --> <!-- return --> <article class="px-1"> <h1 class="dynamic-title"> Cloudflare Workers 自定义域名配置 </h1> <div class="content"> <p>在 Cloudflare Workers 上绑定自定义域名,不是去 DNS 面板手写一条 CNAME 指向 <code class="language-plaintext highlighter-rouge">*.workers.dev</code>,而是在 Workers 侧声明自定义域名后,由 Cloudflare <strong>自动</strong>帮你创建 DNS 记录 + 签发 SSL 证书。这条 DNS 记录直接把你的域名”指”向这个 Worker,绕开了传统的 CNAME 链条。</p> <blockquote> <p>⚠️ <strong>Custom Domain 不能在「已有 CNAME 记录的主机名」上创建</strong>,也不能用在非你拥有的 zone 上。所以如果你的域名已经托管在 Cloudflare,正确做法是走 Workers 控制台的 <strong>Add Custom Domain</strong>,而不是手动去 DNS 面板加 CNAME。</p> </blockquote> <h2 id="1-前置条件"><span class="me-2">1. 前置条件</span><a href="#1-前置条件" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <ul> <li>你的域名已经托管在 Cloudflare(NS 指向 Cloudflare,zone 状态为 <strong>Active</strong>)</li> <li>你已经部署了一个 Worker(<code class="language-plaintext highlighter-rouge">*.workers.dev</code> 默认地址能正常访问)</li> </ul> <p>这两个条件缺一不可。Custom Domain 的本质是「Cloudflare 在自己的 DNS 系统里给你的 Worker 登记为某个主机名的源站」,域名不在 Cloudflare 就没法这么玩。</p> <hr /> <h2 id="2-方法-1通过-cloudflare-控制台添加推荐"><span class="me-2">2. 方法 1:通过 Cloudflare 控制台添加(推荐)</span><a href="#2-方法-1通过-cloudflare-控制台添加推荐" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <ol> <li>登录 Cloudflare 控制台,进入 <strong>Workers &amp; Pages</strong></li> <li>在 Overview 里选中你的 Worker</li> <li>进入 <strong>Settings → Domains &amp; Routes</strong></li> <li>点击 <strong>Add → Custom Domain</strong></li> <li>输入你想绑定的域名或子域名,例如: <ul> <li>根域名:<code class="language-plaintext highlighter-rouge">example.com</code></li> <li>子域名:<code class="language-plaintext highlighter-rouge">api.example.com</code></li> </ul> </li> <li>点击 <strong>Add Custom Domain</strong></li> </ol> <p>Cloudflare 会自动做两件事:</p> <ul> <li>在你的 zone 里创建一条指向该 Worker 的 DNS 记录(你会在 DNS 面板看到它,类型是<strong>系统托管记录</strong>,<strong>不要手动改</strong>)</li> <li>为目标主机名签发一张 Advanced Certificate(SSL 证书自动管)</li> </ul> <p>等待状态变成 <strong>Active</strong>,通常 1–5 分钟。期间证书状态可能短暂显示 Pending,属于正常。</p> <blockquote> <p>💡 想同时支持 <code class="language-plaintext highlighter-rouge">www.example.com</code> 和根域名?Custom Domain 要求精确主机名匹配,<code class="language-plaintext highlighter-rouge">example.com</code> 不会自动接管 <code class="language-plaintext highlighter-rouge">www.example.com</code>。你需要<strong>分别为 www 再添加一次 Custom Domain</strong>,或者加一条 proxied 的 DNS 记录 + 重定向规则把 www 跳转到根域名。</p> </blockquote> <hr /> <h2 id="3-方法-2通过-wrangler-配置文件添加"><span class="me-2">3. 方法 2:通过 Wrangler 配置文件添加</span><a href="#3-方法-2通过-wrangler-配置文件添加" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <p>如果你用 <code class="language-plaintext highlighter-rouge">wrangler.toml</code> / <code class="language-plaintext highlighter-rouge">wrangler.jsonc</code> 管理部署,在 <code class="language-plaintext highlighter-rouge">routes</code> 里加上 <code class="language-plaintext highlighter-rouge">custom_domain = true</code>:</p> <div class="language-toml highlighter-rouge"><div class="code-header"> <span data-label-text="TOML"><i class="fas fa-code fa-fw small"></i></span> <button aria-label="copy" data-title-succeed="已复制!"><i class="far fa-clipboard"></i></button></div><div class="highlight"><code><span class="k">[[</span><span class="n">routes</span><span class="k">]]</span> <span class="n">pattern</span> <span class="o">=</span><span class="w"> </span><span class="s">"api.example.com"</span> <span class="n">custom_domain</span> <span class="o">=</span><span class="w"> </span><span class="kc">true</span> </code></div></div> <p>多个域名就写多条:</p> <div class="language-toml highlighter-rouge"><div class="code-header"> <span data-label-text="TOML"><i class="fas fa-code fa-fw small"></i></span> <button aria-label="copy" data-title-succeed="已复制!"><i class="far fa-clipboard"></i></button></div><div class="highlight"><code><span class="k">[[</span><span class="n">routes</span><span class="k">]]</span> <span class="n">pattern</span> <span class="o">=</span><span class="w"> </span><span class="s">"shop.example.com"</span> <span class="n">custom_domain</span> <span class="o">=</span><span class="w"> </span><span class="kc">true</span> <span class="k">[[</span><span class="n">routes</span><span class="k">]]</span> <span class="n">pattern</span> <span class="o">=</span><span class="w"> </span><span class="s">"shop-two.example.com"</span> <span class="n">custom_domain</span> <span class="o">=</span><span class="w"> </span><span class="kc">true</span> </code></div></div> <p>然后 <code class="language-plaintext highlighter-rouge">npx wrangler deploy</code>,Cloudflare 会创建对应的 Custom Domain。</p> <hr /> <h2 id="4-迁移从旧的-cname--route-方式切换到-custom-domain"><span class="me-2">4. 迁移:从旧的 CNAME / Route 方式切换到 Custom Domain</span><a href="#4-迁移从旧的-cname--route-方式切换到-custom-domain" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <p>这是很多人踩坑的地方。老的玩法是:在 DNS 里建一条 CNAME 指向 <code class="language-plaintext highlighter-rouge">100::</code>(IPv6 占位地址),再用 Route 规则 <code class="language-plaintext highlighter-rouge">example.com/*</code> 把请求转给 Worker。</p> <p>现在官方推荐用 <strong>Custom Domain</strong> 替换这套老机制,迁移步骤:</p> <ol> <li>到 DNS 面板删除 <code class="language-plaintext highlighter-rouge">example.com</code> 那条旧的 CNAME 记录</li> <li>到 Worker 的 <strong>Settings → Domains &amp; Routes → Add → Custom Domain</strong>,填入 <code class="language-plaintext highlighter-rouge">example.com</code></li> <li>回到 <strong>Domains &amp; Routes</strong>,删除旧的 <code class="language-plaintext highlighter-rouge">example.com/*</code> 路由</li> </ol> <blockquote> <p>⚠️ 如果不先删 CNAME,Custom Domain 会因为「主机名已有 CNAME 记录」而创建失败。</p> </blockquote> <hr /> <h2 id="5-几个容易忽略的细节"><span class="me-2">5. 几个容易忽略的细节</span><a href="#5-几个容易忽略的细节" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <h3 id="51-证书不会自动清理"><span class="me-2">5.1 证书不会自动清理</span><a href="#51-证书不会自动清理" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h3> <p>你删掉 Custom Domain 后,Cloudflare 不会自动清理那张 Advanced Certificate。需要去 <strong>SSL/TLS → Edge Certificates</strong> 手动删除,否则证书清单里会留下孤儿记录。</p> <h3 id="52-custom-domain-匹配整个主机名"><span class="me-2">5.2 Custom Domain 匹配整个主机名</span><a href="#52-custom-domain-匹配整个主机名" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h3> <p>不像 Route 可以只匹配 <code class="language-plaintext highlighter-rouge">/api/*</code>,Custom Domain 是 <code class="language-plaintext highlighter-rouge">api.example.com</code> 下的<strong>所有路径</strong>都进同一个 Worker。如果你想在 Custom Domain 上再细分路径给其他 Worker,可以在 Custom Domain 之上叠加 Route 规则——<strong>Route 会优先执行</strong>,并可通过 <code class="language-plaintext highlighter-rouge">fetch(request)</code> 调用 Custom Domain 背后的 Worker。</p> <h3 id="53-同-zone-内-worker-互调"><span class="me-2">5.3 同 zone 内 Worker 互调</span><a href="#53-同-zone-内-worker-互调" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h3> <p>一旦用了 Custom Domain,同 zone 内一个 Worker 可以直接 <code class="language-plaintext highlighter-rouge">fetch('https://api.example.com')</code> 调用另一个 Worker,不需要 Service Binding。这是 Custom Domain 相比传统 Route 的一个优势。</p> <hr /> <h2 id="6-常见问题"><span class="me-2">6. 常见问题</span><a href="#6-常见问题" class="anchor text-muted"><i class="fas fa-hashtag"></i></a></h2> <div class="table-wrapper"><table> <thead> <tr> <th>问题</th> <th>原因</th> <th>解决</th> </tr> </thead> <tbody> <tr> <td>添加后一直 Pending</td> <td>证书签发中</td> <td>等 1–5 分钟,通常会自动 Active</td> </tr> <tr> <td>提示「主机名已有 CNAME 记录」</td> <td>旧 CNAME 未删除</td> <td>先去 DNS 面板删除 CNAME,再重试</td> </tr> <tr> <td>根域名绑定成功但 www 不行</td> <td>Custom Domain 不自动包含 www</td> <td>再添加一次 www 的 Custom Domain</td> </tr> <tr> <td>删除 Custom Domain 后证书还在</td> <td>证书不会自动清理</td> <td>去 SSL/TLS → Edge Certificates 手动删除</td> </tr> <tr> <td>绑定后访问 404</td> <td>Worker 没有处理该路径</td> <td>检查 Worker 代码是否正确响应</td> </tr> </tbody> </table></div> <p>按上面步骤操作完,访问你的域名应该就能直接命中 Worker 了。如果一直没 Active,先检查 zone 是不是真的 Active、主机名是不是已经有别的 DNS 记录占着——这两个是最常见的卡点。</p> </div> <div class="pv-counter" id="pv-counter"> <span class="pv-item">阅读 <span id="pv-count" class="pv-num">—</span></span> <span class="pv-sep">·</span> <span class="pv-item">全站 <span id="pv-total" class="pv-num">—</span></span> </div> <script> (function() { var apiUrl = 'https://jekyll.andyvictory.dpdns.org/track'; var page = window.location.pathname; function render(count, total) { document.getElementById('pv-count').textContent = count; document.getElementById('pv-total').textContent = total; } fetch(apiUrl + '?page=' + encodeURIComponent(page), { method: 'POST', credentials: 'omit' }).then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) .catch(function() { fetch(apiUrl + '?page=' + encodeURIComponent(page) + '&dry=1') .then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) }); })(); </script> <style> .pv-counter { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--text-muted-color, #6c757d); padding: 0.5rem 0; margin-top: 0.5rem; } .pv-num { font-weight: 600; color: var(--text-color, #212529); } .pv-sep { opacity: 0.5; } </style> </article> </main> <!-- panel --> <aside aria-label="Panel" id="panel-wrapper" class="col-xl-3 ps-2 text-muted"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tocbot@4.27.20/dist/tocbot.min.css"> <script src="https://cdn.jsdelivr.net/npm/tocbot@4.27.20/dist/tocbot.min.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { var main = document.querySelector('main h2, main h3'); if (main) { tocbot.init({ tocSelector: '#toc', contentSelector: '.content', ignoreSelector: '[data-toc-skip]', headingSelector: 'h1, h2, h3, h4', orderedList: false, scrollSmooth: false }); document.getElementById('toc-wrapper').classList.remove('d-none'); } }); </script> <section id="toc-wrapper" class="d-none ps-0 pe-4"> <h2 class="panel-heading ps-3 mb-2">文章内容</h2> <nav id="toc"></nav> </section> </aside> </div> <div class="row"> <!-- tail --> <div id="tail-wrapper" class="col-12 col-lg-11 col-xl-9 px-md-4"> <!-- The comments switcher --> <!-- https://giscus.app/ --> <script type="text/javascript"> (function () { const origin = 'https://giscus.app'; const lightTheme = 'light'; const darkTheme = 'dark_dimmed'; let initTheme = lightTheme; const html = document.documentElement; if ( (html.hasAttribute('data-mode') && html.getAttribute('data-mode') === 'dark') || (!html.hasAttribute('data-mode') && window.matchMedia('(prefers-color-scheme: dark)').matches) ) { initTheme = darkTheme; } let giscusAttributes = { src: 'https://giscus.app/client.js', 'data-repo': 'vampire007/vampire007.github.io', 'data-repo-id': 'R_kgDOReCzMQ', 'data-category': 'Q&A', 'data-category-id': 'DIC_kwDOReCzMc4DC6Np', 'data-mapping': 'pathname', 'data-strict' : '0', 'data-reactions-enabled': '1', 'data-emit-metadata': '0', 'data-theme': initTheme, 'data-input-position': 'bottom', 'data-lang': 'zh-CN', 'data-loading': 'lazy', crossorigin: 'anonymous', async: '' }; let giscusScript = document.createElement('script'); Object.entries(giscusAttributes).forEach(([key, value]) => giscusScript.setAttribute(key, value) ); document.getElementById('tail-wrapper').appendChild(giscusScript); addEventListener('message', (event) => { if ( event.source === window && event.data && event.data.direction === ModeToggle.ID ) {const mode = event.data.message; const theme = mode === ModeToggle.DARK_MODE ? darkTheme : lightTheme; const message = { setConfig: { theme: theme } }; const giscus = document.getElementsByClassName('giscus-frame')[0].contentWindow; giscus.postMessage({ giscus: message }, origin); } }); })(); </script> <!-- The Footer --> <footer aria-label="Site Info" class=" d-flex flex-column justify-content-center text-muted flex-lg-row justify-content-lg-between align-items-lg-center pb-lg-3 " > <p>© <time>2026</time> <a href="https://github.com/vampire007">Andy</a>. <span data-bs-toggle="tooltip" data-bs-placement="top" title="除非另有说明,本网站上的博客文章均由作者按照知识共享署名 4.0 国际 (CC BY 4.0) 许可协议进行授权。" >保留部分权利。</span> </p> <p>采用 <a href="https://jekyllrb.com" target="_blank" rel="noopener">Jekyll</a> 主题 <a data-bs-toggle="tooltip" data-bs-placement="top" title="v7.0.1" href="https://github.com/cotes2020/jekyll-theme-chirpy" target="_blank" rel="noopener" >Chirpy</a> </p> </footer> </div> </div> <!-- The Search results --> <div id="search-result-wrapper" class="d-flex justify-content-center d-none"> <div class="col-11 content"> <div id="search-hints"> <!-- The trending tags list --> </div> <div id="search-results" class="d-flex flex-wrap justify-content-center text-muted mt-3"></div> </div> </div> </div> <aside aria-label="Scroll to Top"> <button id="back-to-top" type="button" class="btn btn-lg btn-box-shadow"> <i class="fas fa-angle-up"></i> </button> </aside> </div> <div id="mask" class="d-none position-fixed w-100 h-100 z-1"></div> <!-- JavaScripts --> <!-- JS selector for site. --> <!-- commons --> <!-- layout specified --> <!-- image lazy-loading & popup & clipboard --> <script src="https://cdn.jsdelivr.net/combine/npm/simple-jekyll-search@1.10.0/dest/simple-jekyll-search.min.js,npm/loading-attribute-polyfill@2.1.1/dist/loading-attribute-polyfill.umd.min.js,npm/glightbox@3.3.0/dist/js/glightbox.min.js,npm/clipboard@2.0.11/dist/clipboard.min.js,npm/tocbot@4.27.20/dist/tocbot.min.js"></script> <script src="/assets/js/dist/page.min.js"></script> <!-- Pageviews --> <script> (function () { var CODE_HEADER_BTN = '.code-header > button'; function resetIcon(btn) { var icon = btn.children[0]; if (icon) { icon.setAttribute('class', 'far fa-clipboard'); } } document.addEventListener('click', function (ev) { var btn = ev.target && ev.target.closest ? ev.target.closest(CODE_HEADER_BTN) : null; if (!btn || btn.tagName !== 'BUTTON') { return; } var block = btn.parentNode.nextElementSibling; if (!block) { return; } var code = block.querySelector('code'); if (!code) { return; } var source = code.querySelector('.rouge-code') || code; var text = source.textContent || ''; if (!text.trim()) { return; } if (!navigator.clipboard || !navigator.clipboard.writeText) { return; } ev.preventDefault(); navigator.clipboard.writeText(text).then(function () { var succeed = btn.getAttribute('data-title-succeed'); var icon = btn.children[0]; if (icon) { icon.setAttribute('class', 'fas fa-check'); } try { var tip = window.bootstrap && bootstrap.Tooltip && bootstrap.Tooltip.getInstance(btn); if (tip) { if (succeed) { btn.setAttribute('data-bs-original-title', succeed); } tip.show(); } } catch (e) {} setTimeout(function () { resetIcon(btn); try { var tip2 = window.bootstrap && bootstrap.Tooltip && bootstrap.Tooltip.getInstance(btn); if (tip2) { tip2.hide(); btn.removeAttribute('data-bs-original-title'); } } catch (e) {} }, 2000); }); }); })(); </script> <!-- Embedded scripts --> <!-- Jekyll Simple Search loader See: <https://github.com/christian-fei/Simple-Jekyll-Search> --> <script>SimpleJekyllSearch({ searchInput: document.getElementById('search-input'), resultsContainer: document.getElementById('search-results'), json: '/assets/js/data/search.json', searchResultTemplate: ' <article class="px-1 px-sm-2 px-lg-4 px-xl-0"> <header> <h2><a href="{url}">{title}</a></h2> <div class="post-meta d-flex flex-column flex-sm-row text-muted mt-1 mb-1"> {categories} {tags} </div> </header> <p>{snippet}</p> </article>', noResultsText: '<p class="mt-5">哎呀!没有找到结果。</p>', templateMiddleware: function(prop, value, template) { if (prop === 'categories') { if (value === '') { return `${value}`; } else { return `<div class="me-sm-4"><i class="far fa-folder fa-fw"></i>${value}</div>`; } } if (prop === 'tags') { if (value === '') { return `${value}`; } else { return `<div><i class="fa fa-tag fa-fw"></i>${value}</div>`; } } } }); </script> <!-- Music Player --> <!-- Song Collection Library for Chirpy Theme --> <style> .song-collect{position:fixed;bottom:24px;right:24px;z-index:1000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}.song-toggle{width:48px;height:48px;border-radius:50%;background:linear-gradient(145deg,#fff,#f0f0f0);box-shadow:0 6px 16px rgba(0,0,0,.1);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:1.4rem;transition:all .3s cubic-bezier(.175,.885,.32,1.275);color:#1e1e1e;position:relative}.song-toggle:hover{transform:scale(1.1);box-shadow:0 12px 24px rgba(0,0,0,.15)}.song-panel{position:absolute;bottom:60px;right:0;width:320px;background:rgba(255,255,255,.96);backdrop-filter:blur(12px);border-radius:16px;padding:20px;box-shadow:0 20px 40px rgba(0,0,0,.15);border:1px solid rgba(255,255,255,.3);margin-bottom:12px;opacity:0;visibility:hidden;transform:translateY(10px) scale(.97);transition:all .3s ease}.song-panel.show{opacity:1;visibility:visible;transform:translateY(0) scale(1)}.song-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.song-title{font-weight:600;font-size:1rem;color:#1e1e1e}.song-count{font-size:.8rem;color:#888}.song-search{width:100%;padding:8px 12px;border:1px solid #e8e8e8;border-radius:8px;font-size:.9rem;margin-bottom:12px;outline:none}.song-search:focus{border-color:#667eea}.song-list{max-height:300px;overflow-y:auto}.song-item{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;cursor:pointer;transition:background .15s;margin-bottom:4px}.song-item:hover{background:#f5f5f5}.song-item.active{background:#f0f4ff}.song-icon{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;color:#fff;font-size:.9rem;flex-shrink:0}.song-info{flex:1;min-width:0}.song-name{font-weight:500;font-size:.9rem;color:#1e1e1e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.song-artist{font-size:.78rem;color:#888;margin-top:2px}.song-actions{display:flex;gap:4px}.song-btn{background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;color:#888;transition:all .15s}.song-btn:hover{background:#e8e8e8;color:#667eea}.song-empty{padding:30px;text-align:center;color:#999;font-size:.9rem}.song-empty i{font-size:2rem;margin-bottom:8px;display:block;color:#ccc}@media(prefers-color-scheme:dark){.song-toggle{background:linear-gradient(145deg,#2d2d2d,#1a1a1a);box-shadow:0 6px 16px rgba(0,0,0,.4);color:#ccc}.song-toggle:hover{box-shadow:0 12px 24px rgba(0,0,0,.5)}.song-panel{background:rgba(26,26,26,.96);border-color:rgba(255,255,255,.08)}.song-title{color:#eee}.song-search{background:#2d2d2d;border-color:#444;color:#eee}.song-search:focus{border-color:#667eea}.song-item:hover{background:#2a2a2a}.song-item.active{background:#1a2634}.song-name{color:#eee}.song-artist{color:#888}.song-btn:hover{background:#333;color:#667eea}.song-icon{background:linear-gradient(135deg,#5566aa,#6644aa)}} </style> <div class="song-collect" id="songCollect"> <button class="song-toggle" id="songToggle" title="歌曲收藏库" aria-label="打开歌曲收藏库"> <span>&#x1F3B5;</span> </button> <div class="song-panel" id="songPanel"> <div class="song-header"> <span class="song-title">&#x1F3B8; 我的歌单</span> <span class="song-count" id="songCount">0 首</span> </div> <input type="text" class="song-search" id="songSearch" placeholder="搜索歌曲..."> <div class="song-list" id="songList"></div> </div> </div> <script> (function(){ 'use strict'; /* --- Song data (will be populated from Jekyll) --- */ var songs = [ ]; var STORAGE_KEY = 'songCollection'; var likedSongs = []; try { likedSongs = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch(e) {} /* --- Format time --- */ function fmt(s){ if(isNaN(s)) return '0:00'; var m = Math.floor(s/60), sec = Math.floor(s%60); return m + ':' + (sec < 10 ? '0' : '') + sec; } /* --- Check if song is liked --- */ function isLiked(songUrl) { return likedSongs.indexOf(songUrl) !== -1; } /* --- Toggle like --- */ function toggleLike(songUrl, btn) { var idx = likedSongs.indexOf(songUrl); if(idx === -1) { likedSongs.push(songUrl); btn.innerHTML = '&#x2764;&#xFE0F;'; btn.style.color = '#e74c3c'; } else { likedSongs.splice(idx, 1); btn.innerHTML = '&#x2661;'; btn.style.color = ''; } try { localStorage.setItem(STORAGE_KEY, JSON.stringify(likedSongs)); } catch(e) {} updateCount(); } /* --- Update count --- */ function updateCount() { document.getElementById('songCount').textContent = likedSongs.length + ' 首'; } /* --- Render song list --- */ function renderList(filter) { var container = document.getElementById('songList'); var filtered = filter ? songs.filter(function(s) { return s.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1; }) : songs; if(!filtered.length) { container.innerHTML = '<div class="song-empty"><i class="far fa-music"></i>' + (filter ? '没有找到匹配的歌曲' : '暂无歌曲,请在 assets/music/ 目录添加 MP3 文件') + '</div>'; return; } var html = ''; for(var i = 0; i < filtered.length; i++) { var song = filtered[i]; var liked = isLiked(song.url); html += '<div class="song-item" data-url="' + song.url + '">' + '<div class="song-icon">&#x1F3B5;</div>' + '<div class="song-info">' + '<div class="song-name">' + song.name + '</div>' + '<div class="song-artist">' + (song.artist || '未知艺术家') + '</div>' + '</div>' + '<div class="song-actions">' + '<button class="song-btn like-btn" data-url="' + song.url + '" title="收藏/取消收藏">' + (liked ? '&#x2764;&#xFE0F;' : '&#x2661;') + '</button>' + '</div>' + '</div>'; } container.innerHTML = html; /* Add click handlers */ container.querySelectorAll('.like-btn').forEach(function(btn) { btn.onclick = function(e) { e.stopPropagation(); toggleLike(this.getAttribute('data-url'), this); }; }); } /* --- Init --- */ function init() { var toggle = document.getElementById('songToggle'); var panel = document.getElementById('songPanel'); var search = document.getElementById('songSearch'); var panelOpen = false; /* Toggle panel */ toggle.onclick = function(e) { e.stopPropagation(); panelOpen = !panelOpen; panel.classList.toggle('show', panelOpen); }; /* Close on outside click */ document.addEventListener('click', function(e) { if(!e.target.closest('.song-collect')) { panel.classList.remove('show'); panelOpen = false; } }); /* Search */ search.oninput = function() { renderList(this.value); }; /* Initial render */ renderList(); updateCount(); } /* Wait for DOM */ if(document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script> </body> </html> </div> <div class="pv-counter" id="pv-counter"> <span class="pv-item">阅读 <span id="pv-count" class="pv-num">—</span></span> <span class="pv-sep">·</span> <span class="pv-item">全站 <span id="pv-total" class="pv-num">—</span></span> </div> <script> (function() { var apiUrl = 'https://jekyll.andyvictory.dpdns.org/track'; var page = window.location.pathname; function render(count, total) { document.getElementById('pv-count').textContent = count; document.getElementById('pv-total').textContent = total; } fetch(apiUrl + '?page=' + encodeURIComponent(page), { method: 'POST', credentials: 'omit' }).then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) .catch(function() { fetch(apiUrl + '?page=' + encodeURIComponent(page) + '&dry=1') .then(function(r) { return r.json(); }) .then(function(d) { render(d.count, d.total); }) }); })(); </script> <style> .pv-counter { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--text-muted-color, #6c757d); padding: 0.5rem 0; margin-top: 0.5rem; } .pv-num { font-weight: 600; color: var(--text-color, #212529); } .pv-sep { opacity: 0.5; } </style>

2. Giscus 评论区

2.1 前置条件

giscus 连接你的 GitHub 仓库需要满足以下三个硬性条件:

条件 说明
公开仓库 访客需要读取 Discussions 内容,私有仓库会导致评论区无法加载
已安装 Giscus App giscus 读写 Discussions 的授权凭证,未安装则无法评论
已启用 Discussions 在仓库 Settings → Features 中勾选 Discussions

2.2 安装 Giscus App

访问 Giscus GitHub App 安装页,选择你的博客仓库进行安装。

2.3 配置 Giscus

2.3.1 步骤 1:访问 giscus.app

打开 https://giscus.app,按照提示:

  1. 选择你的 GitHub 仓库
  2. 创建一个 Discussion 分类(如 Comments 或 Q&A)
  3. 获取配置参数:
    • data-repo:仓库名,格式为 username/repo
    • data-repo-id:仓库的 GraphQL ID
    • data-category:分类名称
    • data-category-id:分类的 GraphQL ID

2.3.2 步骤 2:获取 ID

在浏览器中查看 giscus.app 生成的代码,或右键页面查看源码,找到类似这样的内容:

data-repo-id="R_kgDOxxxxxx" data-category-id="DIC_kwDOxxxxxx"

2.4 在 Jekyll 中配置

2.4.1 方法一:使用 Chirpy 主题(推荐)

在 _config.yml 中添加:

comments: provider: giscus giscus: repo: username/repo-name repo_id: 'R_kgDOxxxxxx' category: Comments category_id: 'DIC_kwDOxxxxxx' mapping: pathname strict: 0 reactions_enabled: 1 emit_metadata: 0 input_position: bottom lang: zh-CN

2.4.2 方法二:手动添加

在 _layouts/post.html 中添加:

<div id="giscus-thread"></div> <script> const giscus = document.createElement('script'); giscus.src = 'https://giscus.app/client.js'; giscus.setAttribute('data-repo', 'vampire007/vampire007.github.io'); giscus.setAttribute('data-repo-id', 'R_kgDOReCzMQ'); giscus.setAttribute('data-category', 'Q&A'); giscus.setAttribute('data-category-id', 'DIC_kwDOReCzMc4DC6Np'); giscus.setAttribute('data-mapping', 'pathname'); giscus.setAttribute('data-strict', '0'); giscus.setAttribute('data-reactions-enabled', '1'); giscus.setAttribute('data-emit-metadata', '0'); giscus.setAttribute('data-input-position', 'bottom'); giscus.setAttribute('data-lang', 'zh-CN'); document.head.appendChild(giscus); </script>

2.5 配置说明

参数 说明 默认值
mapping 页面映射方式(pathname/url/title 等) pathname
strict 是否严格匹配标题(0=关闭,1=开启) 0
reactions_enabled 是否启用表情反应 1
emit_metadata 是否发送元数据 0
input_position 评论框位置(top/bottom) top
lang 界面语言 zh-CN

3. 常见问题

3.1 Q1: PV 统计不工作?

检查以下几点:

  • Worker 是否已部署(访问 https://your-worker.workers.dev/track?page=/' 看是否返回 JSON)
  • 浏览器控制台是否有 CORS 错误
  • _includes/pv-counter.html 中的 API URL 是否正确

3.2 Q2: 评论区不显示?

检查以下几点:

  • 仓库是否公开
  • Giscus App 是否已安装(在仓库 Settings → Applications 中查看)
  • Discussions 功能是否已启用(在仓库 Settings → Features 中勾选)
  • repo_id 和 category_id 是否正确

3.3 Q3: 如何实现防盗链?

在 Worker 代码中添加 Referer/Origin 检查:

const allowedHosts = ['yourdomain.com']; const isAllowed = allowedHosts.some(h => origin.includes(h) || referer.includes(h) ); if (!isAllowed) { return new Response('Forbidden', { status: 403 }); }

3.4 Q4: 私有仓库如何实现评论功能?

参考 giscus 官方文档:私有仓库解决方案

核心思路:新建一个公开仓库专门存放评论,在配置中指向这个公开仓库。


4. 参考链接

阅读 — · 全站 —
🎸 我的歌单 0 首