---
name: vercel-github-static-deploy
description: Push a local Vite/static prototype to GitHub, deploy it to Vercel with the CLI, connect the Git repo, and verify the public deployment/domain.
version: 1.0.0
metadata:
  hermes:
    tags: [vercel, github, deployment, static-site, vite]
    related_skills: [github-auth, github-repo-management, google-cloud-domains-registration]
---

# Vercel + GitHub Static Deploy

Use when the user wants a local static/Vite prototype deployed through GitHub first, then Vercel.

## Preconditions

- Local project builds successfully (`npm test` / `npm run build` as applicable).
- `git`, `gh`, and `npm`/`npx` available.
- GitHub auth works: `gh auth status`.
- Vercel auth works: `npx --yes vercel@latest whoami`, or run `npx --yes vercel@latest login`.
- If attaching a custom domain, the domain is owned and DNS/nameservers are ready.

## Workflow

1. Verify project state before publishing:

```bash
cd /path/to/project
git status --short --branch || true
npm test
npm run build
```

2. Ensure repo hygiene before first commit:

```bash
cat > .gitignore <<'EOF'
node_modules/
dist/
.DS_Store
.env
.env.*
.vercel/
.vercel
EOF
```

Add a small `README.md` if missing, with product purpose and local commands.

3. Initialize and commit locally:

```bash
git init -b main
git add .
git commit -m "Initial prototype"
```

4. Authenticate GitHub if needed:

```bash
gh auth status || gh auth login --hostname github.com --git-protocol https --web
```

If the Hermes PTY/browser auth flow is unreliable or times out, ask the user to run `gh auth login` locally and reply when done. Then continue.

5. Create private GitHub repo and push:

```bash
gh repo create REPO_NAME \
  --private \
  --description 'Short description' \
  --source . \
  --remote origin \
  --push
```

If the repo already exists:

```bash
git remote add origin https://github.com/OWNER/REPO_NAME.git 2>/dev/null || \
  git remote set-url origin https://github.com/OWNER/REPO_NAME.git
git push -u origin main
```

Verify:

```bash
gh repo view OWNER/REPO_NAME --json nameWithOwner,url,isPrivate,defaultBranchRef
```

6. Log in to Vercel if needed:

```bash
npx --yes vercel@latest whoami || npx --yes vercel@latest login
```

7. Deploy with Vercel CLI from the correct project root:

```bash
# single-app repo
npx --yes vercel@latest --prod --yes

# repo with app in a subdirectory such as landing/
cd /path/to/repo/landing
npx --yes vercel@latest --prod --yes
```

For a subdirectory app, verify the Vercel project root directory is set to that subdirectory (`landing`, `web`, etc.) after GitHub connection. A deployment can succeed from the CLI but future GitHub-triggered builds will fail or deploy the wrong thing if the Vercel root directory remains the repo root.

Observed behavior: for a Vite project, the CLI may auto-detect settings, create/link a Vercel project, create `.vercel/`, connect the GitHub repository, run the cloud build, and output both the production deployment URL and stable project alias.

8. If `.vercel` modified `.gitignore`, commit/push the ignore update but do not commit `.vercel/project.json`:

```bash
git status --short
git add .gitignore
git commit -m "Ignore Vercel project metadata" || true
git push
```

9. Verify deployment:

```bash
npx --yes vercel@latest projects inspect PROJECT_NAME
python3 - <<'PY'
import urllib.request
url='https://PROJECT_ALIAS.vercel.app'
with urllib.request.urlopen(url, timeout=20) as r:
    text=r.read().decode('utf-8', 'replace')
    print(r.status, r.geturl())
    print('EXPECTED_BRAND_TEXT' in text)
PY
```

Also perform browser QA on the public URL and check console errors.

### Local browser QA when dev/preview servers are unreliable

If local `npm run dev`, `vite preview`, `serve`, or `python -m http.server` crashes/refuses connections, still QA the built Vite app before deploying:

1. Run `npm run build`.
2. Create a file-based QA HTML from `dist/index.html`:
   - safer first option: rewrite `/assets/...` URLs to `./assets/...` and open via `file://.../dist/qa-module.html`;
   - fallback option: inline built CSS/JS into `dist/qa-inline.html`.
3. If inlining Vite JS, preserve `type="module"` on the inline script. Replacing a module script with plain `<script>` can make the page go blank because bundled module code may include top-level syntax that requires module mode.
4. After regenerating QA HTML, navigate fresh and verify actual body text/flow, not just screenshot state. A stale browser page can make vision QA report a blank page even after build succeeded.
5. If browser console reports an empty `source: exception` but the app renders and a page-level error probe records no `window.error`/`unhandledrejection` after reload, treat it as an environment/browser-tool artifact; still verify via user-visible flow and production QA.

## Custom domain

If nameservers already point to Vercel, add the domain to the Vercel project:

```bash
npx --yes vercel@latest domains add DOMAIN PROJECT_NAME
npx --yes vercel@latest domains inspect DOMAIN
```

For external registrar DNS such as GoDaddy parking records, see `references/vercel-external-registrar-dns.md`; for the GoDaddy UI pattern where `A @` shows `Parked`, use `references/vercel-godaddy-parking-to-vercel.md`. Preserve mail/verification records (Mailgun, Gmail, SPF/DKIM/DMARC, ownership TXT) and change only apex/`www` web-serving records unless the user explicitly asks to modify email DNS. If Vercel domain-add is blocked by Hermes safety approval with an explicit denial, do not retry in the same turn; give manual Vercel Dashboard steps. If the user later explicitly asks you to try the domain mutation yourself, run the Vercel domain-add once, inspect aliases/domains, then guide/verify DNS, SSL, homepage, CTA, and telemetry.

For apex/root domains whose DNS is managed outside Vercel, also inspect Vercel's current recommended record in the dashboard/API before editing DNS. Recent Vercel domain screens may recommend an apex A record like `216.150.1.1` rather than older `76.76.21.21` / Google parking records. When DNS is in Google Cloud DNS:

```bash
gcloud dns managed-zones list --format='table(name,dnsName,visibility)'
gcloud dns record-sets list --zone=<zone> --name='<domain>.' --format='table(name,type,ttl,rrdatas)'
# Use a transaction to replace the apex A/AAAA records with Vercel's required A record.
gcloud dns record-sets transaction start --zone=<zone>
gcloud dns record-sets transaction remove --zone=<zone> --name='<domain>.' --type=A --ttl=<ttl> <old-a-values...>
gcloud dns record-sets transaction remove --zone=<zone> --name='<domain>.' --type=AAAA --ttl=<ttl> <old-aaaa-values...> # if present and not desired
gcloud dns record-sets transaction add --zone=<zone> --name='<domain>.' --type=A --ttl=300 <vercel-required-ip>
gcloud dns record-sets transaction execute --zone=<zone>
```

Verification should cover both control plane and serving path:

```bash
for ns in ns-cloud-c1.googledomains.com ns-cloud-c2.googledomains.com ns-cloud-c3.googledomains.com ns-cloud-c4.googledomains.com; do dig +short A <domain> @${ns}; done
for resolver in 1.1.1.1 8.8.8.8 9.9.9.9; do dig +short A <domain> @${resolver}; done
curl -I --max-time 20 https://<domain>
```

If Vercel says the project domain is `verified: true` and HTTP works, but HTTPS fails with `SSL_ERROR_SYSCALL`, unexpected EOF, or `no alternative certificate subject name matches`, issue or refresh the Vercel certificate explicitly, then retry both curl and browser checks because edge propagation can be briefly inconsistent:

```bash
vercel certs issue <domain>
vercel certs issue www.<domain>
for i in 1 2 3 4; do curl -I --max-time 20 https://<domain>; sleep 10; done
```

Vercel API nuance: `/v6/domains/<domain>/config` can show `misconfigured: false` while `ipStatus: required-change` because Vercel still lists legacy recommended IPv4 values. Trust the dashboard's required record plus actual DNS/HTTPS verification over that one field alone.

See `references/vercel-google-cloud-dns-apex-domain.md` for the condensed transcript-derived recipe.

Important: in Hermes, domain-add commands can trigger security approval or be blocked (for example due lookalike-TLD/domain safety scans). If blocked with `BLOCKED: User denied. Do NOT retry.`, stop and give the user exact manual steps in Vercel Dashboard:

1. Open Vercel dashboard.
2. Open the project.
3. Settings → Domains.
4. Add the custom domain.
5. Verify Vercel sees the configured nameservers/DNS.

Do not retry blocked domain-add commands unless the user explicitly asks you to try the domain mutation yourself again. If the user does explicitly ask, make one direct CLI attempt, then immediately verify with `vercel domains inspect`, `vercel alias ls`, DNS lookups, and `curl -I` checks. Report the control-plane result separately from DNS propagation; adding the domain/alias can succeed while external registrar DNS still points at parking records.

When handing off GoDaddy/external-DNS work to the user, keep it concise and action-only. Tell them exactly which web-serving records to delete/add and explicitly say not to touch Mailgun/MX/TXT/SPF/DKIM/DMARC/verification records.

### Troubleshooting Vercel custom-domain invalid configuration

When Vercel shows `Invalid Configuration` for an apex/root domain managed by external DNS (for example Google Cloud DNS), fix and verify both DNS and certificate state instead of stopping at the dashboard warning.

1. Identify authoritative DNS and current records:

```bash
dig +short NS example.com
dig +short A example.com @8.8.8.8
dig +short AAAA example.com @8.8.8.8
```

2. If DNS is managed in Google Cloud DNS and Vercel requires the new apex record, update the managed zone transactionally. For Vercel's current external apex target, the expected record may be:

```text
A @ 216.150.1.1
```

Remove stale Google/Sites-style apex A/AAAA records such as `216.239.32.21`, `216.239.34.21`, `216.239.36.21`, `216.239.38.21` and matching IPv6 records when they conflict with the Vercel-required record.

3. Verify Cloud DNS readback and public propagation separately:

```bash
gcloud dns record-sets list --zone=ZONE --name='example.com.'
for ns in ns-cloud-c1.googledomains.com ns-cloud-c2.googledomains.com ns-cloud-c3.googledomains.com ns-cloud-c4.googledomains.com; do dig +short A example.com @$ns; done
for resolver in 1.1.1.1 8.8.8.8 9.9.9.9; do dig +short A example.com @$resolver; done
```

4. Use the Vercel API/CLI to verify domain state. The Vercel CLI may report intended nameservers with confusing red crosses for third-party DNS even when the project domain is verified; prefer project-domain API/config checks when available.

5. If HTTP works but HTTPS fails with TLS EOF/SSL errors after DNS is correct, issue a Vercel certificate manually:

```bash
vercel certs issue example.com
curl -I https://example.com
```

6. Final verification should include browser navigation to the custom domain and `curl -I https://example.com` returning 200/3xx from Vercel.

## Managing Vercel environment variables from the agent host

Use this flow when a repo has working local `.env*` values and the user wants them added to the Vercel project:

1. Install/verify the CLI. Prefer the user-local prefix if global npm is not writable:

```bash
command -v vercel || npm install -g --prefix "$HOME/.local" vercel@latest
vercel --version
```

`npx --yes vercel@latest ...` remains a good fallback for one-off commands, but installing to `$HOME/.local/bin/vercel` makes repeated env/deploy work cleaner.

2. Authenticate with device login if no credentials exist:

```bash
vercel whoami || vercel login
```

When `vercel login` asks whether to install the Claude Code/Vercel plugin, answer `n` unless the user explicitly wants it; it is not required for deployments or env var management.

3. Link the repo to the intended project before editing env vars:

```bash
cd /path/to/repo
vercel link --yes --project <project-name>
vercel project ls
```

4. Add env vars without printing secret values. Read values from local env files, then use `vercel env add <KEY> <environment> --value "$VALUE" --yes`. Verify only with `vercel env ls`, which shows encrypted presence.

5. For Preview env vars, the CLI may ask: `Add <KEY> to which Git branch? (leave empty for all Preview branches)?`. To target all preview branches in an interactive/PTY session, submit a blank line. Non-interactive `--non-interactive` can still emit `git_branch_required`; switch to PTY/interactive instead of looping. Do **not** pipe a blank line into `vercel env add ... --value "$VALUE"` to answer this prompt: the CLI can consume stdin as the env value, warn `Value is empty`, and fail to add the Preview var. If production is the current goal, set Production/Development with `--force --value`, redeploy, and skip Preview unless preview links matter for the experiment.

6. Redeploy production after adding production env vars so the live deployment has access to them:

```bash
vercel --prod --yes
curl -I --max-time 20 https://<production-domain>
```

See `references/vercel-env-vars-cli.md` for a concise transcript-derived recipe.

## Pitfalls / lessons

- Reference: `references/vercel-github-linked-deploy-blocks.md` captures a Next.js deployment where Vercel blocked a production deployment because the pushed commit author was not associated with a GitHub/Vercel user, plus the safe workaround.
- If Vercel marks a deployment `BLOCKED` with `readyStateReason: The Deployment was blocked because there was no git user associated with the commit`, inspect the deployment via the Vercel API/CLI before retrying deploys. Fix the Git author to a GitHub-associated noreply email and create/push a new commit authored by the linked user, or add a small follow-up commit through the GitHub API as the authenticated account. This gives Vercel a deployable GitHub-triggered commit without weakening project protection.
- If a Vercel project has `ssoProtection.deploymentType: all_except_custom_domains`, `.vercel.app` deployment URLs may 302 to Vercel SSO while custom domains remain public. Verify the custom domain separately before assuming production is inaccessible.
- `printf '--- heading ---' can fail in bash because the format begins with `--`; use `printf '%s\n' '--- heading ---'`.
- Vercel CLI may not be globally installed; `npx --yes vercel@latest ...` works.
- If `npm install -g vercel@latest` fails with `/usr/local` permissions, install user-local: `npm install -g --prefix "$HOME/.local" vercel@latest` and ensure `$HOME/.local/bin` is on PATH.
- A stale/invalid Vercel token can make `vercel whoami` fail; interactive `vercel login` can repair auth.
- Keep Vercel work focused on Vercel. Do not bring Cloudflare Tunnel/cloudflared into env-var deployment explanations unless the actual task involves exposing a local service, webhook, or OAuth callback.
- Preview env vars may need an interactive blank branch response to apply to all preview branches; verify with `vercel env ls` because failed/empty preview attempts can exit deceptively cleanly.
- Node versions can differ locally and in Vercel. Vercel may use Node 24.x for a Vite project even if local Node is different.
- Direct deployment can still connect the GitHub repo if the local repo has an `origin` remote and Vercel has GitHub integration access.
- Keep `.vercel` out of git; it contains local project linkage metadata.
- After deploying or moving a Next.js app to a custom production domain, verify `/sitemap.xml` and `/robots.txt` on the public domain, not just the homepage. It is easy for `sitemap.ts` to use the new domain while `robots.ts` still advertises an old sitemap host, or for both routes to return branded Next.js 404 HTML with `noindex`.
- For Next.js serverless routes on Vercel, do not write runtime files under `process.cwd()` / `/var/task` such as `.data/uploads` or SQLite DBs. `/var/task` is read-only at runtime and can throw `ENOENT`/500 on dynamic routes. Use `/tmp/<app-name>` for temporary probe data, or a real hosted datastore/blob store for durable production data. After fixing, verify dynamic pages like `/create` and `/login`, not only the static homepage.
- To identify the current production URL quickly, compare repo homepage, `.vercel/repo.json`, and `vercel project ls` / `vercel project inspect`; repo homepage can lag behind the actual custom domain.
