# Lov Deploy To Vercel

> Deploy frontend projects to Vercel with automatic custom domain setup. Handles Vite, Next.js, CRA, and static sites. Auto-configures Cloudflare DNS CNAME records and Vercel domain aliases. Supports SPA routing via vercel.json. Trigger when user says "deploy to vercel", "部署到 vercel", "vercel deploy", or mentions a *.example.com / custom domain with vercel deployment.

- Skill: `lovstudio/lov-deploy-to-vercel` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add lovstudio/lov-deploy-to-vercel`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lovstudio/lov-deploy-to-vercel/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: lovstudio (https://skillmd.com/u/lovstudio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lovstudio/lov-deploy-to-vercel

---


# Vercel 部署助手 · Vercel Deployer

Deploy frontend projects to Vercel with automatic custom domain and DNS setup.

## When to Use

- User says "deploy to vercel" or "部署到 xxx.example.com"
- After building a frontend project that needs hosting
- When setting up a custom domain on an existing Vercel deployment

## Arguments

Pass via `$ARGUMENTS`:

| Argument | Example | Description |
|----------|---------|-------------|
| `<domain>` | `sbti.example.com` | Custom domain to configure |
| `--preview` | | Deploy preview only (skip `--prod`) |
| `--no-dns` | | Skip Cloudflare DNS auto-config |
| `--link-only` | | Only link project, don't deploy |

## Workflow

### Step 1: Detect Project Type

```bash
if [ -f "vite.config.ts" ] || [ -f "vite.config.js" ]; then
  FRAMEWORK="vite"
elif [ -f "next.config.js" ] || [ -f "next.config.mjs" ]; then
  FRAMEWORK="next"
elif grep -q "react-scripts" package.json 2>/dev/null; then
  FRAMEWORK="cra"
else
  FRAMEWORK="static"
fi
```

### Step 2: Ensure vercel.json for SPA

For Vite/CRA (SPA) projects, create `vercel.json` if missing:

```json
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/" }
  ]
}
```

**Skip for Next.js** — it handles routing natively.

### Step 3: Deploy to Vercel

Before running a production deployment, use `AskUserQuestion` if the target
project, production/non-production mode, or custom domain is unclear. If the
user already explicitly requested production deployment for this project, proceed.

```bash
# Check CLI
vercel --version || npm i -g vercel

# Deploy (use project name from package.json "name" field)
# IMPORTANT: package.json "name" must be lowercase, no special chars
PROJECT_NAME=$(node -p "require('./package.json').name" 2>/dev/null || basename "$PWD")
vercel --yes --prod
```

**Known issue**: If `package.json` name contains uppercase or invalid chars,
vercel will error with "Project names must be lowercase". Fix the name first.

### Step 4: Configure Custom Domain (if provided)

```bash
DOMAIN="<user-provided-domain>"  # e.g. sbti.example.com

# 1. Add domain to Vercel project
vercel domains add "$DOMAIN"

# 2. Set alias to point domain to latest deployment
PROD_URL=$(vercel ls --prod 2>&1 | grep -oE 'https://[^ ]+\.vercel\.app' | head -1)
vercel alias set "$PROD_URL" "$DOMAIN"
```

**CRITICAL**: `vercel domains add` alone is NOT enough. You MUST also run
`vercel alias set` to actually route traffic. Without it, the domain returns
ERR_CONNECTION_CLOSED.

### Step 5: Auto-Configure Cloudflare DNS

**Requires**: `CLOUDFLARE_API_KEY` env var (API Token with DNS edit permission).

```bash
# Extract base domain and subdomain
# e.g. "sbti.example.com" → base="example.com", sub="sbti"
BASE_DOMAIN=$(echo "$DOMAIN" | awk -F. '{print $(NF-1)"."$NF}')
SUBDOMAIN=$(echo "$DOMAIN" | sed "s/\.$BASE_DOMAIN$//")

# 1. Get zone ID
ZONE_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=$BASE_DOMAIN" \
  -H "Authorization: Bearer $CLOUDFLARE_API_KEY" \
  -H "Content-Type: application/json" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])")

# 2. Check if record already exists
EXISTING=$(curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$DOMAIN&type=CNAME" \
  -H "Authorization: Bearer $CLOUDFLARE_API_KEY" | python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')")

# 3. Create or update CNAME → cname.vercel-dns.com
if [ -z "$EXISTING" ]; then
  curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
    -H "Authorization: Bearer $CLOUDFLARE_API_KEY" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"CNAME\",\"name\":\"$SUBDOMAIN\",\"content\":\"cname.vercel-dns.com\",\"ttl\":1,\"proxied\":false}"
else
  curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$EXISTING" \
    -H "Authorization: Bearer $CLOUDFLARE_API_KEY" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"CNAME\",\"name\":\"$SUBDOMAIN\",\"content\":\"cname.vercel-dns.com\",\"ttl\":1,\"proxied\":false}"
fi
```

**IMPORTANT**: `proxied` must be `false` (DNS only). Cloudflare proxy conflicts
with Vercel's SSL certificate provisioning.

If `CLOUDFLARE_API_KEY` is not set, print manual DNS instructions instead:
```
Add DNS record:
  Type: CNAME
  Name: <subdomain>
  Target: cname.vercel-dns.com
  Proxy: OFF (DNS only)
```

### Step 6: Verify

```bash
# Wait for DNS + SSL propagation
sleep 5
HTTP_CODE=$(curl -sI "https://$DOMAIN" -o /dev/null -w '%{http_code}')
if [ "$HTTP_CODE" = "200" ]; then
  echo "✓ $DOMAIN is live"
else
  echo "⚠ HTTP $HTTP_CODE — SSL may still be provisioning, try again in 1-2 min"
fi
```

### Step 7: Output Summary

```
✓ Framework: vite
✓ Deployed: https://xxx.vercel.app
✓ Domain: https://sbti.example.com
✓ DNS: CNAME sbti → cname.vercel-dns.com (Cloudflare)
✓ Settings: https://vercel.com/<scope>/<project>/settings
```

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| ERR_CONNECTION_CLOSED | Domain added but no alias set | Run `vercel alias set <url> <domain>` |
| "Project names must be lowercase" | package.json name invalid | Fix name field |
| SSL not provisioning | Cloudflare proxy ON | Set DNS to "DNS only" (no orange cloud) |
| 404 on sub-routes | SPA missing rewrites | Add vercel.json with rewrites |
| DNS resolves to 198.18.x.x | Local proxy (Clash etc.) | Normal — check with `dig @8.8.8.8` |
| `CLOUDFLARE_API_KEY` not found | Token not in env | Add to `~/.zshrc`: `export CLOUDFLARE_API_KEY=...` |

## Runtime context (shared)

运行前读取本 Skill 包的 `skill.yaml`，由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为：当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。

- 只使用 Manifest 声明的字段；Profile 保存公开品牌事实，Preferences 保存个人工作偏好。
- `required: true` 字段缺失时，按 Manifest 的问题配置向用户提出一个聚焦问题；用户明确同意后再保存回答。
- 报错提供可复制的 `context_id`、字段路径与来源，诊断内容避开秘密、完整私人路径和原始配置。

## 通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时，继续当前产物前必须执行：

1. 先判断意见是 `task-specific`（仅本次）还是 `reusable`（可跨任务复用）。
2. `task-specific` 只修改当前任务，不改 Skill。
3. `reusable` 先确定作用域：领域规则先更新对应 canonical Skill；适用于所有 Skill 的规则先更新共享规范。
4. 完成规则更新、版本、lint 与分发核验后，再把修改应用到当前任务。
5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效；完成当前产物修改和回读后必须停下，等待用户下一步指示，不自动进入发布、提交或其他外部写入。

