使用 Vercel API 构建自定义域名管理:优点、缺点与 DNS 传播

发布: (2026年1月1日 GMT+8 23:52)
12 min read
原文: Dev.to

Source: Dev.to

当我开始构建 WikiBeem 时,自定义域名看起来很简单。只需将域名添加到 Vercel,向用户展示一些 DNS 记录,然后砰——完成。对吧?

不对。 结果发现,边缘情况、时序问题以及 DNS 传播延迟的世界会让你在凌晨 2 点质疑自己的人生选择。

下面是我实际的构建过程、出现的故障以及我学到的经验。

我如何使用 Vercel 的 API 为 WikiBeem 构建自定义域名管理系统。了解 DNS 验证、SSL 证书、多租户路由以及让我抓狂的边缘情况。

为什么选择 Vercel 的 API?

我在 Vercel 上托管 WikiBeem,所以使用他们的域名管理 API 很合适。它:

  • 自动处理 SSL 证书
  • 管理 DNS 路由
  • 与 Vercel 基础设施直接集成

另一种做法是使用 AWS Route 53 或 Cloudflare 从头开始构建——工作量大得多,却基本得到相同的结果。

Vercel 提供了官方 SDK(@vercel/sdk),对 API 进行了封装,使集成更简洁。不过,文档需要大量的试错才能弄清在生产环境中实际可行的用法。

基本流程

当用户想要为其站点添加自定义域名时,需要完成以下步骤:

  1. 用户输入他们的域名(例如 docs.yourcompany.com
  2. 通过 API 将域名添加到 Vercel
  3. Vercel 返回需要配置的 DNS 记录
  4. 用户在域名注册商处配置 DNS
  5. 轮询 Vercel,检查 DNS 是否已生效
  6. SSL 证书在验证成功后颁发
  7. 站点在自定义域名上正常工作

理论上很简单,实际操作却并非如此。

设置 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 casesDNS 传播时间差异极大;设计流程时要能够容忍长时间等待。

构建一个可靠的自定义域名系统更多是要应对 未知,而不是只写“顺利”路径的代码。掌握上述模式后,即使网络变慢,仍能为用户提供流畅的体验。

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 传播很烦人,但值得投入。

结果

经过所有这些工作,自定义域名现在可以可靠地使用。用户可以:

  1. 添加他们的域名。
  2. 配置 DNS。
  3. 在几分钟内看到他们的网站在自定义域名上通过 SSL 正常运行。

用户体验仍有提升空间——Webhook 支持和更丰富的状态信息已列入路线图,但核心流程已经稳固,用户也感到满意。

如果你想看到自定义域名的实际效果,请访问 WikiBeem 只需几次点击,你就可以使用自己的域名发布 ClickUp 文档。

Back to Blog

相关文章

阅读更多 »