website/docs/user-guide/skills/optional/web-development/web-development-publish-site.md
Versioned site deploys to GitHub/Cloudflare/Netlify Pages.
| Source | Optional — install with hermes skills install official/web-development/publish-site |
| Path | optional-skills/web-development\publish-site |
| Version | 1.0.0 |
| Author | Hermes Agent (Nous Research) |
| License | MIT |
| Platforms | linux, macos, windows |
| Tags | publish, deploy, hosting, github-pages, cloudflare-pages, netlify, static-site, versioning, rollback, web-development |
:::info The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. :::
Take a website, dashboard, or web app the user built (or you built for them) and put it online on infrastructure the user owns — GitHub Pages by default, Cloudflare Pages or Netlify when they need more. The discipline: preview locally for sign-off, version every deploy with a git tag, deploy through a provider ladder, verify the live URL with a real HTTP check, and keep rollback one command away.
This skill covers static sites and SPA build output (plain HTML/CSS/JS, or the dist//build/ folder from Vite/Next-export/Astro/etc.). It does not cover server-side runtimes — for throwaway serverless deploys with zero account setup, use the cloudflare-temporary-deploy optional skill instead.
Load this skill when the user asks to:
At least ONE authenticated provider CLI (check in this order):
gh auth status succeeds. Needs git too.wrangler whoami succeeds (or CLOUDFLARE_API_TOKEN is set). Install: npm i -g wrangler or use npx wrangler@latest.netlify status succeeds. Install: npm i -g netlify-cli.Plus:
dist//build/ folder). If the project needs a build step, run it first and publish the output directory, never the source.cloudflared (optional — python3 -m http.server covers local-only preview).All commands below run via the terminal tool from the site's project directory. The pipeline is always the same five moves:
curl and report it.| Step | Command |
|---|---|
| Local preview | python3 -m http.server 8080 --directory dist |
| Shareable preview | cloudflared tunnel --url http://localhost:8080 |
| Version a deploy | git add -A && git commit -m "deploy: <what>" && git tag deploy-YYYYMMDD-HHMM |
| GitHub Pages (branch mode) | git subtree push --prefix dist origin gh-pages |
| Enable Pages on repo | gh api repos/{owner}/{repo}/pages -X POST -f 'source[branch]=gh-pages' -f 'source[path]=/' |
| Cloudflare Pages | npx wrangler@latest pages deploy dist --project-name <name> |
| Netlify | netlify deploy --prod --dir dist |
| Rollback | git checkout <previous-tag> -- . && redeploy (or provider dashboard) |
| Verify live | curl -sS -o /dev/null -w '%{http_code}' <url> → expect 200 |
Build if needed (npm run build, etc.) and identify the output directory. Serve it:
python3 -m http.server 8080 --directory dist
For a shareable preview link (user on another machine, or you want their sign-off before going live), open a quick tunnel in a background terminal session:
cloudflared tunnel --url http://localhost:8080
Give the user the https://*.trycloudflare.com URL and get sign-off before deploying. Kill the tunnel afterwards.
Every deploy must come from a git commit, so every deploy is reproducible and rollback is trivial.
git init 2>/dev/null; git add -A
git commit -m "deploy: <short description>"
git tag "deploy-$(date +%Y%m%d-%H%M)"
If the project already has a repo, just commit + tag. Never deploy uncommitted files.
Rung 1 — GitHub Pages (default: free, zero extra accounts if gh is authed):
gh repo create <name> --public --source . --push # skip if repo exists
git subtree push --prefix dist origin gh-pages # publish build output
gh api "repos/{owner}/<name>/pages" -X POST \
-f 'source[branch]=gh-pages' -f 'source[path]=/' # first time only
Site appears at https://<owner>.github.io/<name>/. If the site is the repo root (no build dir), push main and set Pages source to main instead of using subtree. For build-step projects that will redeploy often, prefer the official actions/deploy-pages workflow so pushes auto-publish.
Rung 2 — Cloudflare Pages (when the user wants a custom domain, redirects/headers, or Functions):
npx wrangler@latest pages deploy dist --project-name <name>
First run creates the project and prints the https://<name>.pages.dev URL. Custom domains attach via the Cloudflare dashboard (Pages → project → Custom domains).
Rung 3 — Netlify (fallback, or when the user already lives there):
netlify deploy --prod --dir dist
netlify deploy --dir dist (no --prod) gives a draft URL — useful as a second preview stage.
Rollback = redeploy a previous tag. Never hand-edit live output.
git checkout deploy-<previous> -- . # or: git checkout deploy-<previous>; rebuild
# then rerun the same deploy command from step 3
Cloudflare Pages and Netlify also keep per-deploy history in their dashboards ("Rollback to this deploy"), which is faster when the CLI isn't handy.
.env files — they'd be public on Pages hosting. Check with git status before the first commit and keep .env* in .gitignore.index.html to 404.html in the output dir (cp dist/index.html dist/404.html) so client-side routing recovers. Cloudflare Pages and Netlify handle SPAs via _redirects (/* /index.html 200).curl a few times before investigating.Logo.PNG but committed as logo.png. Grep the HTML for mismatched casing when an asset 404s.https://<owner>.github.io/<name>/ serves under /<name>/ — absolute asset URLs like /app.js break. Use relative paths or set the build tool's base (vite build --base=/<name>/).wrangler auth flow needs a browser. wrangler login opens OAuth; in a headless session prefer CLOUDFLARE_API_TOKEN (user creates it at dash.cloudflare.com → API Tokens) and never echo the token into logs.*.pages.dev, *.netlify.app, *.github.io) first, then check the custom domain separately — don't conflate the two failures.dist/ yields a directory listing or raw JSX. Always confirm the output dir contains an index.html.Do NOT report success from the deploy log alone. Before telling the user anything:
curl -sS -o /dev/null -w '%{http_code}' <live-url> returns 200 (retry over ~2 minutes for a first GitHub Pages deploy).curl -sS <live-url> | head -30 shows the expected index.html content — optionally confirm markup with web_extract on the live URL./about) and confirm it returns 200, not 404.git tag --list 'deploy-*' shows the tag for this deploy.Then report the live URL to the user, along with the deploy tag they can roll back to.