使用 Vercel API 构建自定义域名管理:优点、缺点与 DNS 传播
Source: Dev.to
当我开始构建 WikiBeem 时,自定义域名看起来很简单。只需将域名添加到 Vercel,向用户展示一些 DNS 记录,然后砰——完成。对吧?
不对。 结果发现,边缘情况、时序问题以及 DNS 传播延迟的世界会让你在凌晨 2 点质疑自己的人生选择。
下面是我实际的构建过程、出现的故障以及我学到的经验。

为什么选择 Vercel 的 API?
我在 Vercel 上托管 WikiBeem,所以使用他们的域名管理 API 很合适。它:
- 自动处理 SSL 证书
- 管理 DNS 路由
- 与 Vercel 基础设施直接集成
另一种做法是使用 AWS Route 53 或 Cloudflare 从头开始构建——工作量大得多,却基本得到相同的结果。
Vercel 提供了官方 SDK(@vercel/sdk),对 API 进行了封装,使集成更简洁。不过,文档需要大量的试错才能弄清在生产环境中实际可行的用法。
基本流程
当用户想要为其站点添加自定义域名时,需要完成以下步骤:
- 用户输入他们的域名(例如
docs.yourcompany.com) - 通过 API 将域名添加到 Vercel
- Vercel 返回需要配置的 DNS 记录
- 用户在域名注册商处配置 DNS
- 轮询 Vercel,检查 DNS 是否已生效
- SSL 证书在验证成功后颁发
- 站点在自定义域名上正常工作
理论上很简单,实际操作却并非如此。
设置 Vercel 客户端
我创建了一个围绕 Vercel SDK 的轻量包装器。这让我可以在一个地方统一处理错误并确保凭证配置正确。
import { Vercel } from '@vercel/sdk'
export class VercelClient {
private vercel: Vercel
private projectId: string
private teamId?: string
constructor() {
const token = process.env.VERCEL_TOKEN || ''
this.projectId = process.env.VERCEL_PROJECT_ID || ''
this.teamId = process.env.VERCEL_TEAM_ID
if (!token || !this.projectId) {
console.warn(
'Vercel credentials not configured. Custom domain features will not work.'
)
}
this.vercel = new Vercel({ bearerToken: token })
}
}
Tip: 始终在使用之前检查所需的环境变量是否存在。缺少凭证会产生难以调试的晦涩错误。
添加域名
第一个 API 调用很直接——只需告诉 Vercel 您想要添加域名:
async addDomain(domain: string) {
const response = await this.vercel.projects.addProjectDomain({
idOrName: this.projectId,
requestBody: { name: domain },
...(this.teamId && { teamId: this.teamId })
})
return response
}
处理不一致的验证记录
Vercel 返回验证记录,但它们并不总是在同一个位置。有时它们直接在响应对象中;另一些时候需要单独获取域名配置。我最终检查了两者:
// Try to get verification records from domain config first
let verificationRecords: { type: string; name: string; value: string }[] = []
try {
const domainConfig = await vercelClient.getDomainConfig(domain)
if (domainConfig?.verification) {
verificationRecords = domainConfig.verification.map(v => ({
type: v.type,
name: v.domain || domain,
value: v.value
}))
}
} catch (e) {
// Fallback to response verification if config fails
if (vercelDomain.verification) {
verificationRecords = vercelDomain.verification.map(v => ({
type: v.type,
name: v.domain || domain,
value: v.value
}))
}
}
经验教训: API 响应可能不一致。使用回退机制可以避免神秘的失败。
DNS 验证:等待的游戏
这往往是用户感到沮丧的地方。他们在注册商处配置 DNS 记录,点击 Verify,然后……没有任何反应——至少不是立刻。
- DNS 传播可能需要 几分钟到 48 小时。
- 子域名通常更快(5‑30 分钟)。
- 顶级域名(apex domain)可能会慢得多。
我实现了一个轮询机制,每隔几秒检查一次验证状态,但也确保不会频繁请求 Vercel 的 API。
// Front‑end polling every 5 seconds
const pollDomainStatus = async () => {
const response = await fetch(`/api/domain?siteId=${siteId}`)
const data = await response.json()
if (data.domain?.isVerified) {
setPolling(false) // Stop polling when verified
return
}
setTimeout(pollDomainStatus, 5000) // Check again in 5 seconds
}
我还添加了手动 Check Status 按钮,因为用户不想被动等待。给他们控制权可以提升体验。
SSL 证书竞赛
一旦 DNS 验证通过,Vercel 会自动为域名配置 SSL 证书,但仍会有另一段延迟——即使验证成功,证书的签发也可能需要几分钟。
我会将 SSL 状态单独于验证状态进行跟踪:
const sslStatus = vercelDomain.verified ? 'issued' : 'pending'
实际上,API 可能会落后一小步:它可能返回 verified: true,而证书仍在签发中。因此 UI 应该显示“pending”状态,直到证书被确认已激活。
要点
| 领域 | 我学到的内容 |
|---|---|
| Vercel SDK | 适合快速集成,但文档缺口需要通过实验来弥补。 |
| Credential handling | 必须防范环境变量缺失;在出现问题时要快速失败并给出明确警告。 |
| API inconsistencies | 需要对多个端点返回的数据进行校验,并提供回退方案。 |
| Polling | 在避免触发速率限制的同时保持用户知情,需要平衡轮询频率。 |
| User experience | 添加手动“检查状态”操作,并为“验证中”“SSL 待定”“已就绪”等状态提供清晰的 UI 显示。 |
| Edge cases | DNS 传播时间差异极大;设计流程时要能够容忍长时间等待。 |
构建一个可靠的自定义域名系统更多是要应对 未知,而不是只写“顺利”路径的代码。掌握上述模式后,即使网络变慢,仍能为用户提供流畅的体验。
SSL 配置缓冲
SSL 证书实际上尚未准备好,因此我添加了一些缓冲时间,并向用户显示 “SSL provisioning” 状态。
多租户路由:真正的挑战
这是最棘手的部分。当有人访问 docs.yourcompany.com 时,我们如何判断要显示哪个站点?
Vercel 负责 DNS 路由和 SSL,但实际的请求路由由我们自行处理。在 Next.js middleware 中,我检查 host 请求头,以判断它是否为自定义域名:
// middleware.ts
import { NextResponse, NextRequest } from 'next/server';
export default function middleware(request: NextRequest) {
const host = request.headers.get('host') || '';
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const mainDomain = new URL(appUrl).hostname;
// Is this a custom domain?
const isCustomDomain =
host !== mainDomain &&
!host.startsWith('localhost') &&
!host.startsWith('127.0.0.1');
// Pass host to pages so they can look up the site
const response = NextResponse.next();
response.headers.set('X-Host', host);
return response;
}
在页面组件中查找站点
// page.tsx (or any server component)
import { headers } from 'next/headers';
import prisma from '@/lib/prisma';
export default async function Page() {
// Get host from header
const host = headers().get('x-host') || '';
// Look up domain in database
const domain = await prisma.domain.findUnique({
where: { domain: host },
select: { siteId: true },
});
// Get the site
const site = await prisma.site.findUnique({
where: { id: domain?.siteId },
});
// …render the page using `site`
}
边缘情况
- 数据库中不存在的域名 – 返回 404。
- 已配置 DNS 但域名未验证 – 显示 “域名未配置” 的提示信息。
URL 结构差异
自定义域名的 URL 结构与默认路由不同。
| 路由类型 | 示例 |
|---|---|
| 默认 | wikibeem.com/yoursite/docs/getting-started |
| 自定义 | docs.yourcompany.com/docs/getting-started |
- 在主域名下,第一段(
yoursite)是站点的 slug。 - 在自定义域名下,路径中没有站点 slug——域名本身即标识站点,因此文档路径会直接开始。
我重构了路由逻辑以同时处理这两种情况:
if (isCustomDomain) {
// Custom domain: domain identifies the site, no site slug in path
const domain = await prisma.domain.findUnique({
where: { domain: host },
});
// Document slug is everything after the domain
fullDocSlug = [siteSlug, ...docSlugArray].join('/');
} else {
// Default route: siteSlug is first segment, rest is document path
fullDocSlug = docSlugArray.join('/');
}
路由的边缘情况非常隐蔽,这段代码花的时间比我愿意承认的要长。
错误处理:预期所有东西都会出错
常见生产错误
| 错误 | 处理方式 |
|---|---|
| 域名已存在 | 告知用户该域名已被占用;避免返回通用的 500 错误。 |
| DNS 未配置 | 显示所需的 DNS 记录并提示用户添加它们。 |
| 传播超时 | 经过几分钟的轮询后,显示:“DNS 传播可能需要长达 48 小时。稍后再检查或验证您的 DNS 设置。” |
| SSL 证书失败 | 单独检查 SSL 状态并呈现明确的错误信息。 |
| 竞争条件 | 在用户在验证进行中移除域名时进行适当的清理;防止并发操作。 |
我会做的不同之处
- 添加 Webhook – Vercel 支持域名事件的 Webhook。监听验证事件比轮询更简洁。
- 更好的状态信息 – 要具体:“检查 DNS… → DNS 已验证,正在配置 SSL… → SSL 证书已签发,1‑2 分钟内准备就绪。”
- 在调用 API 前进行验证 – 验证域名格式,检查是否已有使用,并(在可能的情况下)在调用 Vercel 的 API 前确认所有权。
- 重试逻辑 – 为不稳定的 API 调用实现指数退避。
- 测试 – 使用预发布域名进行端到端完整流程测试。DNS 传播很烦人,但值得投入。
结果
经过所有这些工作,自定义域名现在可以可靠地使用。用户可以:
- 添加他们的域名。
- 配置 DNS。
- 在几分钟内看到他们的网站在自定义域名上通过 SSL 正常运行。
用户体验仍有提升空间——Webhook 支持和更丰富的状态信息已列入路线图,但核心流程已经稳固,用户也感到满意。
如果你想看到自定义域名的实际效果,请访问 WikiBeem。 只需几次点击,你就可以使用自己的域名发布 ClickUp 文档。