
折腾了一天，把一个静态博客从零搭起来，部署到已备案域名、国内加速，而且每篇文章都有一个**纯 Markdown 链接**可以直接丢给 AI 读。这篇文章记录完整过程和踩过的坑，希望能帮人少走弯路。

## 目标

- 内容用 Markdown 写
- 给人看：渲染好的 HTML 页面
- **给 AI 看**：复制一个链接，AI 直接读到纯 Markdown（不带 HTML 模板噪音）
- 部署到已备案域名，国内访问快
- 成本尽可能低

## 方案选型

对比了三种方案：

| 方案 | 回源 | 国内速度 | 备注 |
|---|---|---|---|
| GitHub Pages + EdgeOne 加速 | 有源站（GitHub 美国），未命中要跨洋回源 | 一般 | 回源配置复杂（Host/SNI 分离坑） |
| EdgeOne Pages 直发 | 无源站 | 好 | 较新产品，国内加速需确认免费版 |
| **COS + EdgeOne** | COS 作源站，同生态 | 好 | 成本近零，配置成熟 ← 选这个 |

最终选 **COS + EdgeOne**：腾讯云备案域名天然互通，COS 存储便宜，EdgeOne 回源 COS 走内部 CDN 回源流量（比公网便宜），静态内容缓存命中率高、回源极少。

## 整体架构

```
访客 ──HTTPS──▶ EdgeOne 边缘节点(国内) ──回源HTTPS──▶ COS 桶(源站)
                     │
                     ├─ 命中缓存 → 直接返回(快)
                     └─ 未命中   → 回源 COS(一次性)
```

## 部署步骤（精简版）

用 `tccli` 全自动化。前置：域名腾讯云备案、EdgeOne 站点 Area=global、tccli 配好 cos/teo/dnspod。

### 1. 建 COS 桶（公有读）

```bash
tccli cos create_bucket --bucket blog-<APPID> --region ap-beijing --acl public-read
```

### 2. 建 EdgeOne 加速域名，回源 COS

```bash
tccli teo CreateAccelerationDomain \
  --ZoneId <zone-id> --DomainName blog.example.com \
  --OriginInfo '{"OriginType":"IP_DOMAIN","Origin":"<bucket>.cos.ap-beijing.myqcloud.com","HostHeader":"<bucket>.cos.ap-beijing.myqcloud.com"}' \
  --OriginProtocol HTTPS --HttpsOriginPort 443
```

### 3. 加 CNAME（DNSPod）

```bash
tccli dnspod CreateRecord --Domain example.com --SubDomain blog \
  --RecordType CNAME --RecordLine "默认" \
  --Value "blog.example.com.eo.dnse2.com" --TTL 600
```

### 4. 配 URL 重写（关键，见坑2）

### 5. 申请 + 绑定免费证书（见坑5）

### 6. 验证

## 踩坑实录（核心价值）

以下每个坑都是真实踩过、花了时间排查的。

### 坑1：回源 Host 必须是 COS 桶域名

EdgeOne 回源时，`HostHeader` 默认是加速域名（`blog.example.com`），但 **COS 靠 Host 路由到桶**——Host 错了 COS 返回 400。

**解决**：建加速域名时显式设 `HostHeader` 为 COS 桶域名（`<bucket>.cos.<region>.myqcloud.com`）。好消息是 COS 桶域名的 Host 和 SNI 天然一致，不像 GitHub Pages 那样有 Host/SNI 分离的坑。

### 坑2：COS 根路径 `/` 返回 403

访客访问 `blog.example.com/`，EdgeOne 回源 COS 的 `/`，而 **COS 桶根没有对象，且 public-read 不允许列桶** → 403。

**解决**：配 EdgeOne 回源 URL 重写 `/` → `/index.html`。这其实是所有"对象存储托管静态站"的通病。

### 坑3：通配符 DNS 记录的缓存

如果域名有 `*` 通配符 A 记录（很多人用来兜底），新加的精确 CNAME 在生效前会被通配符兜底（返回旧 IP）。

**排查**：直接查权威 NS 的 CNAME 类型，能确认数据已正确（`dig @权威NS <sub> CNAME`），只是递归解析器缓存未过期。

**解决**：等 TTL 过期；或验证时用 `curl --resolve <domain>:443:<EdgeOne IP>` 绕过缓存立即测试。

### 坑4：EdgeOne 调度域名 dig 返回 SERVFAIL 是正常的

EdgeOne 给的 CNAME 目标形如 `*.eo.dnse2.com`，**直接 dig 它会返回 SERVFAIL**。别误判为配置错误——它只对 CNAME 跟随有效（作为 CNAME 目标被解析时正常返回边缘 IP）。

### 坑5：免费证书签发后不会自动部署

`ApplyFreeCertificate` 签发证书后，域名的 `Certificate.Mode` 默认是 `disable`，**边缘会返回默认的 `*.cdn.myqcloud.com` 证书**，导致 TLS 域名不匹配。

**解决**：必须手动绑定：

```bash
tccli teo ModifyHostsCertificate --ZoneId <zone-id> \
  --Hosts '["blog.example.com"]' --Mode eofreecert
```

绑定后约 30 秒~几分钟部署到边缘。

### 坑6：tccli 规则引擎 API 的两个坑

配 URL 重写规则时：

1. `CreateL7AccRules --Rules '<json>'` 直接传 JSON 会触发 **tccli 递归 bug**，必须用 `--cli-input-json file://` 传文件。
2. Condition 表达式：连接符是 `and` 不是 `&&`；路径匹配变量名不固定（`http.request.path`、`http.request.uri` 都被拒），**优先用 regex 在动作里做路径精确匹配**，而不是在 Condition 里匹配路径。

### 坑7：`UpstreamURLRewrite` 的 schema 试出来的

回源 URL 重写的 Action 合法值（API 报错才会列出）：

```
[replace, addPrefix, rmvPrefix, regexReplace]
```

regexReplace 时字段：**`Regex` 放外层**（不是嵌套在 Path 对象里），配 `Value`。

### 坑8：目录索引的反向引用语法

Hugo 这类多页静态站，每个目录都有 `index.html`（如 `/posts/xxx/index.html`）。访问 `/posts/xxx/` 要重写到 `/posts/xxx/index.html`。用 regexReplace：

```json
{"Regex": "^(.*/)$", "Value": "$1index.html"}
```

**关键**：反向引用必须用 `$1`，**不能用 `${1}`**——后者会被 EdgeOne 误判为"预设变量"报错。

### 坑9：EdgeOne 会缓存 404

规则生效前回源失败的 404 会被边缘缓存。规则改好后**必须 purge 那些目录 URL**，否则持续返回旧 404。

## 让 AI 读到纯 Markdown（Hugo 配置）

这是整个项目最有意思的部分。Hugo 有 `markdown` output format，能让每篇文章同时输出 HTML（人看）和原始 .md（AI 看）。

### 配置

`hugo.toml`：

```toml
[outputFormats.markdown]
  mediaType = "text/markdown"
  isPlainText = true
  baseName = "index"
  suffix = "md"

[outputs]
  page = ["HTML", "markdown"]
```

### 模板（最大的坑）

`isPlainText=true` 的 output format，模板文件用 **`.md` 扩展名**（不是 `.html`）：

```
layouts/_default/single.markdown.md
```

内容就一行：

```
{{ .RawContent }}
```

输出的是纯正文 Markdown（不含 frontmatter）。

### 三个 Hugo 坑

1. **文章 date 不能是未来**：UTC 时差导致本地今天写的文章可能被判为未来，Hugo 默认不渲染。date 设为当天或之前，或加 `buildFuture = true`。
2. **模板扩展名是 `.md`**：`single.markdown.html` 不被识别（前面试了好久），`single.markdown.md` 才对。
3. **COS 对 .md 默认 Content-Type 是 `application/octet-stream`**（当二进制）！AI 读到可能不当文本。必须覆盖上传时设 `text/markdown`，且 `sync_upload` 不按扩展名区分，要对 .md 单独 upload。

### 最终效果

每篇文章两个地址：

- `/posts/xxx/` → 渲染 HTML（人看）
- `/posts/xxx/index.md` → 纯 Markdown（AI 读，`Content-Type: text/markdown`）

## 成本实测

| 项目 | 单价 | 实际 |
|---|---|---|
| COS 存储 | 0.118 元/GB/月 | 几 MB → 约 0.001 元 |
| CDN 回源流量 | 0.15 元/GB | 有缓存命中，极少 |
| EdgeOne 边缘流量 | 走套餐 | 个人版套餐内 |
| **合计** | | **< 0.2 元/月** |

注意：**EdgeOne 回源 COS 不是免费内网**。腾讯云"同地域内网免费"规则明确除外 CDN/EdgeOne，走的是单独的 CDN 回源流量计费项（0.15 元/GB）。但比外网下行（0.50）便宜 70%，且静态站缓存命中率高，实际成本可忽略。

## 日常使用

写完一篇一键部署（脚本自动处理所有坑）：

```bash
cd ~/ai-blog && ./deploy.sh 新文章名
```

脚本自动：Hugo build → 上传 COS → 修正 .md 的 Content-Type → purge 缓存。然后把输出的链接 `https://blog.example.com/posts/新文章名/index.md` 丢给 AI 即可。

## 总结

- **技术上**：COS + EdgeOne 搭国内加速静态站完全可行，成本近零，但坑集中在"回源 Host、根路径 403、URL 重写 schema、证书绑定、Content-Type"这几处。
- **给 AI 看 md**：Hugo 的 markdown output format 能做到，关键是模板扩展名（`.md`）和 COS Content-Type（`text/markdown`）。
- **最大的教训**：EdgeOne 的规则引擎 API schema 不够透明，很多字段值要靠 API 报错反向猜；把这些固化成文档/脚本后，后续部署就是无脑一条命令。

如果你也在搭类似的东西，希望这篇能帮你省掉几个小时的踩坑时间。

