Publish a Static Site to GitHub, Cloudflare, or Netlify Pages
Versioned site deploys to GitHub/Cloudflare/Netlify Pages.
Written by Neura Market from the official Hermes Agent documentation for Publish Site. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThis skill takes a finished website, dashboard, or web app and puts it online on infrastructure the user owns. It is for the moment when the build is done and the user wants a URL they can share. The workflow is deliberately disciplined: 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.
What it does
You hand it a directory of static output, and it walks you through publishing that directory to a hosting provider. The default is GitHub Pages, which costs nothing and needs no extra account if you already have gh authenticated. When the user needs more, it escalates to Cloudflare Pages or Netlify. Those providers add custom domains, redirects, headers, and serverless Functions, which GitHub Pages cannot do.
The skill covers static sites and single-page application build output: plain HTML/CSS/JS, or the dist//build/ folder produced by Vite, Next.js export, Astro, and similar tools. It does not cover server-side runtimes. If the user wants a throwaway serverless deploy with zero account setup, the cloudflare-temporary-deploy optional skill is the better fit.
Every deploy follows the same five moves: build, preview for sign-off, commit and tag, deploy via the provider ladder, then verify the live URL with curl and report it. The versioning step is non-negotiable. Each deploy comes from a git commit, so it is reproducible and rollback is trivial.
Before you start
You need at least one authenticated provider CLI. Check them in this order:
- GitHub Pages (default):
gh auth statussucceeds. Needsgittoo. - Cloudflare Pages:
wrangler whoamisucceeds (orCLOUDFLARE_API_TOKENis set). Install withnpm i -g wrangleror usenpx wrangler@latest. - Netlify (fallback):
netlify statussucceeds. Install withnpm i -g netlify-cli.
You also need a directory of static output to publish. That is the site root or a dist//build/ folder. If the project needs a build step, run it first and publish the output directory, never the source. For local preview sharing, cloudflared is optional; python3 -m http.server covers local-only preview.
In practice, the provider ladder means you rarely need more than one CLI. If gh is already authenticated, you can skip the others entirely. The Cloudflare and Netlify CLIs are there for when the user asks for features GitHub Pages cannot provide.
How to run
All commands run via the terminal tool from the site's project directory. The pipeline is always the same five moves:
- Build → 2. Preview for sign-off → 3. Commit + tag (version-before-deploy) → 4. Deploy via the provider ladder → 5. Verify the live URL with
curland report it.
Quick reference
| 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: " && 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 |
| Netlify | netlify deploy --prod --dir dist |
| Rollback | git checkout -- . && redeploy (or provider dashboard) |
| Verify live | curl -sS -o /dev/null -w '%{http_code}' → expect 200 |
Procedure
1. Build and preview locally
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.
This step is where you catch problems before they become public. The local server serves exactly what will go live, so check the pages, the assets, and the routes. If the user is remote, the tunnel gives them the same view. Do not skip the sign-off; it is the cheapest review you will get.
2. Version before deploy, no exceptions
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.
The tag is your rollback handle. It records the exact state of the output at deploy time. Without it, rolling back means guessing which version was live, which is exactly the situation this skill is designed to prevent.
3. Deploy, provider ladder
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://.github.io//. 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.
The first command creates the repository if it does not exist. The second pushes the dist folder to the gh-pages branch. The third enables Pages on the repository, and only needs to run once. After that, every redeploy is just the subtree push.
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://.pages.dev URL. Custom domains attach via the Cloudflare dashboard (Pages → project → Custom domains).
This is the rung to choose when the user asks for a custom domain, wants to control redirects or headers, or needs serverless Functions. The CLI handles the upload; the dashboard handles the domain and environment configuration.
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.
The draft URL is a nice middle ground between the local tunnel and the production deploy. You can send it to the user for a final check before you run the --prod version.
4. Rollback
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.
The git-based rollback works everywhere. The dashboard rollback is faster on Cloudflare and Netlify, but it only works if you deployed through them. The git tag is the universal fallback.
5. Secrets and environment variables
- NEVER commit secrets, API keys, or
.envfiles, they'd be public on Pages hosting. Check withgit statusbefore the first commit and keep.env*in.gitignore. - Runtime env vars belong in the provider's dashboard: Cloudflare Pages → Settings → Environment variables; Netlify → Site settings → Environment variables. GitHub Pages is static-only, no server env; anything embedded in the bundle is public by definition. Warn the user if their build inlines a key.
This is the step people skip and regret. A committed .env file on a public Pages host is a credential leak. The git status check before the first commit is the safety net. For runtime variables, the provider dashboard is the only place they belong.
Pitfalls
- SPA routes 404 on GitHub Pages. Pages has no rewrite rules. Copy
index.htmlto404.htmlin 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). - GitHub Pages build lag. The site can take 1, 10 minutes to appear after the first enable, and ~1 minute per subsequent push. Don't declare failure on the first 404, poll
curla few times before investigating. - Case-sensitive paths. Pages hosts are case-sensitive Linux; a site that worked on macOS/Windows can 404 on assets referenced as
Logo.PNGbut committed aslogo.png. Grep the HTML for mismatched casing when an asset 404s. - Project-page base path.
https://.github.io//serves under//, absolute asset URLs like/app.jsbreak. Use relative paths or set the build tool's base (vite build --base=//). wranglerauth flow needs a browser.wrangler loginopens OAuth; in a headless session preferCLOUDFLARE_API_TOKEN(user creates it at dash.cloudflare.com → API Tokens) and never echo the token into logs.- DNS propagation on custom domains. New CNAMEs can take minutes to hours. Verify against the provider's default URL (
*.pages.dev,*.netlify.app,*.github.io) first, then check the custom domain separately, don't conflate the two failures. - Deploying source instead of build output. Publishing the repo root when the real site lives in
dist/yields a directory listing or raw JSX. Always confirm the output dir contains anindex.html.
These are the failures that look like provider problems but are usually configuration mistakes. The SPA 404 issue is the most common: GitHub Pages has no rewrite rules, so a deep link like /about returns 404 unless you copy index.html to 404.html. The build lag is the one that tempts you to debug a healthy deploy; the fix is patience and a polling loop.
Verification
Do NOT report success from the deploy log alone. Before telling the user anything:
curl -sS -o /dev/null -w '%{http_code}'returns200(retry over ~2 minutes for a first GitHub Pages deploy).curl -sS | head -30shows the expectedindex.htmlcontent, optionally confirm markup withweb_extracton the live URL.- For SPAs, also curl one deep route (e.g.
/about) and confirm it returns200, not404. 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.
The deploy log says the upload succeeded, not that the site works. The HTTP check confirms the server is serving your content. The deep-route check confirms SPA routing works. The tag check confirms you have a rollback point. Only when all four pass do you tell the user the site is live.
When not to use it
This skill is for static sites and SPA build output. It does not cover server-side runtimes. If the user needs a throwaway serverless deploy with zero account setup, use the cloudflare-temporary-deploy optional skill instead. That skill is the right choice when the deploy is ephemeral and the user does not want to create or manage a provider account.
Limits and gotchas
The main limits come from the providers. GitHub Pages is static-only, so no server-side environment variables; anything embedded in the bundle is public. It also has no rewrite rules, so SPAs need the 404.html trick. Cloudflare Pages and Netlify handle SPAs natively via _redirects, but they require a browser-based OAuth flow for wrangler login, which is a problem in headless sessions; use CLOUDFLARE_API_TOKEN instead. Custom domains on any provider can take minutes to hours to propagate, so verify the default URL first.
What pairs with this
This skill sits in the web-development category of optional skills. It pairs naturally with the skills that generate the static output you publish: site builders, dashboard generators, and documentation tools. For throwaway serverless deploys, the cloudflare-temporary-deploy skill is the companion. The verification step also references web_extract, which you can use to confirm the live markup.