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 中:
- 进入 Workers & Pages → 选择你的 Worker
- 点击 Triggers → Add Custom Domain
- 输入你的子域名(如
pv.yourdomain.com) - 在 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 & Pages</strong></li>
<li>在 Overview 里选中你的 Worker</li>
<li>进入 <strong>Settings → Domains & 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 & Routes → Add → Custom Domain</strong>,填入 <code class="language-plaintext highlighter-rouge">example.com</code></li>
<li>回到 <strong>Domains & 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>🎵</span>
</button>
<div class="song-panel" id="songPanel">
<div class="song-header">
<span class="song-title">🎸 我的歌单</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 = '❤️';
btn.style.color = '#e74c3c';
} else {
likedSongs.splice(idx, 1);
btn.innerHTML = '♡';
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">🎵</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 ? '❤️' : '♡') + '</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,按照提示:
- 选择你的 GitHub 仓库
- 创建一个 Discussion 分类(如
Comments或Q&A) - 获取配置参数:
data-repo:仓库名,格式为username/repodata-repo-id:仓库的 GraphQL IDdata-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 官方文档:私有仓库解决方案
核心思路:新建一个公开仓库专门存放评论,在配置中指向这个公开仓库。