bx-sites-deployment
Deploy or package a built bx-sites (ortus-boxlang/bx-sites) site - deployments/*.json targets (S3-compatible, Azure, GCS, Firebase, FTP/SFTP, rsync, Netlify, Vercel, Cloudflare Pages, local, GitHub Pages), secrets-in-env-vars conventions, bxSites package, the GitHub Actions multi-version-publishing workflow, and restricting who can reach a deployed site. Use this whenever a user wants to ship a built bx-sites site somewhere, set up CI/CD publishing, or gate access to a deployed site.
BxSites Deployment Reference
site/ is a plain static site - host it anywhere that serves static files.
bxSites deploy ships it there directly. Build it first (or let deploy
build it for you) - see bx-sites-build.
The deploy command - three invocation shapes
bxSites deploy --entry=production [--verbose] # 1. named deployments/<name>.json
bxSites deploy --target=local|github-pages [flags] [--verbose] # 2. flag-only, no deployments/ file
bxSites deploy [--verbose] # 3. every deployments/*.json entry
--entry=<name>dispatches to whateverdeployments/<name>.jsondeclares - every target exceptlocal/github-pagesneeds this (more config than a couple of flags can carry).--target=<name>with its own flags - shorthand for the two simplest targets only, nodeployments/folder at all:local(--destination=<path>) andgithub-pages([--branch] [--remote] [--message], same defaults asgh-deploy).- Neither flag - every
deployments/*.jsonentry deployed in turn, off one shared build (not rebuilt per target). One target failing doesn't stop the rest; the command exits non-zero only if at least one failed, and the summary reports the count (Deployed to 2/3 target(s) (1 failed)).
--verbose prints a progress line as the build and each target start/finish,
instead of just the final summary.
Secrets
Always from an environment variable, never a literal value in
deployments/*.json. Every field ending EnvVar names the env var
holding the real secret, resolved live at deploy time - so the JSON file
itself is always safe to commit. A field that's a path to a credential
file already managed separately (an SSH key, a downloaded GCP service-
account JSON) is the one exception - a plain field, since the file itself
(not its path) is what's kept out of version control. Locally, those env
vars can come from a .env file (BoxLang loads it automatically); in CI,
set them as real runner secrets.
Targets
local
No deployments/ entry needed - the only target that doesn't.
bxSites deploy --target=local --destination=/path/to/somewhere
github-pages
The same push gh-deploy does, reachable from the unified command too - no
deployments/ entry needed either.
bxSites deploy --target=github-pages [--branch=gh-pages] [--remote=origin] [--message="..."]
s3
Real AWS S3, or any S3-compatible service. Set endpoint for non-AWS, and
forcePathStyle: true for most non-AWS providers.
{
"target": "s3",
"bucket": "my-docs-site",
"region": "us-east-1",
"prefix": "",
"accessKeyIdEnvVar": "AWS_ACCESS_KEY_ID",
"secretAccessKeyEnvVar": "AWS_SECRET_ACCESS_KEY"
}
{
"target": "s3",
"bucket": "my-docs-site",
"endpoint": "https://nyc3.digitaloceanspaces.com",
"forcePathStyle": true,
"accessKeyIdEnvVar": "SPACES_KEY",
"secretAccessKeyEnvVar": "SPACES_SECRET"
}
Same shape (custom endpoint + forcePathStyle: true) also covers
Cloudflare R2 (https://<accountid>.r2.cloudflarestorage.com), Backblaze
B2, and MinIO/Wasabi.
azure
{ "target": "azure", "account": "mystorageaccount", "container": "site", "accountKeyEnvVar": "AZURE_STORAGE_KEY" }
Authenticate with exactly one of a SAS token, an account key, or a full connection string.
gcs
{ "target": "gcs", "bucket": "my-docs-site", "serviceAccountKeyPath": "/path/to/service-account.json" }
Downloaded service-account JSON key (Google Cloud Console โ IAM & Admin โ Service Accounts โ Keys).
firebase
{ "target": "firebase", "siteId": "my-firebase-site", "serviceAccountKeyPath": "/path/to/service-account.json" }
ftp / sftp
{ "target": "sftp", "host": "example.com", "username": "deploy", "remotePath": "/var/www/html", "key": "/home/me/.ssh/id_rsa" }
SFTP accepts a password or an SSH key. Preserves the site's folder structure.
rsync
{ "target": "rsync", "host": "example.com", "username": "deploy", "remotePath": "/var/www/html", "identityFile": "/home/me/.ssh/id_rsa" }
Real rsync binary over SSH - only transfers what changed, faster than
FTP/SFTP for a full rebuild. Requires rsync and ssh on the machine
running bxSites.
netlify
{ "target": "netlify", "siteId": "my-site-id-or-name.netlify.app", "authTokenEnvVar": "NETLIFY_AUTH_TOKEN" }
vercel
{ "target": "vercel", "projectId": "my-project", "authTokenEnvVar": "VERCEL_TOKEN" }
cloudflare-pages
{ "target": "cloudflare-pages", "accountId": "your-account-id", "projectName": "my-project", "apiTokenEnvVar": "CLOUDFLARE_API_TOKEN" }
Cloudflare has no documented REST API for direct-upload deploys (only
wrangler) - this target reverse-engineers Wrangler's own upload flow and
needs a BLAKE3 hash implementation most default JVMs don't ship. Treat as
the roughest-edged target; verify a real deploy before relying on it.
package - a plain archive instead
bxSites package
bxSites package --output=dist/my-site.zip
Builds, then zips site/'s own contents (not a wrapping site/ folder)
into one file - for attaching to a GitHub release, a host that only accepts
a zip upload, or any target the pluggable list above doesn't reach.
--output defaults to <projectRoot>/site.zip; parent directories are
created automatically.
GitHub Actions: multi-version publishing
bx-sites ships a ready-to-use .github/workflows/pages.yml pattern
publishing main and development as two independently-live versions of
the same site to GitHub Pages:
- Installs BoxLang + bx-markdown (and whatever else the project needs)
- Registers the repo as a module so
boxlang bxSites buildresolves - On any branch but
main, pointsbaseURLat.../<branch-name>/for just that build - Runs
boxlang bxSites build - Pushes
site/togh-pages-mainto the site root,developmentto/development/, each withkeep_files: trueand its owndestination_dirso neither overwrites the other
Also triggerable manually (workflow_dispatch) for a one-off republish.
One-time setup (after the workflow has run at least once, since the
first run creates the gh-pages branch): Settings โ Pages โ Build and
deployment โ Source: "Deploy from a branch" โ Branch: gh-pages / (root).
Adding a third branch (e.g. release/2.0): add it to
on.push.branches and give it its own if: github.ref_name == '...' deploy
step with destination_dir: release-2.0.
Project Pages sub-path - https://<user>.github.io/<repo>/ (as opposed
to a <user>.github.io user site) needs baseURL set to that full URL so
every internal link/asset/nav entry gets the /<repo>/ prefix and a real
sitemap.xml is generated (see bx-sites-configuration). A user site or a
custom domain mapped to the root can leave baseURL at its default (/).
Restricting who can reach a deployed site
No built-in access control - a plain static site/ has no concept of "who's
asking." robots: false tells well-behaved crawlers not to index it, but
it's a polite request, not a lock - the URL still works for anyone who has
it. Real access restriction has to happen in front of the static files, at
whichever host serves them:
- Cloudflare Pages/Access - a Cloudflare Access policy (email allowlist, SSO, one-time PIN), no application code.
- Netlify - built-in password protection per site/deploy, from site settings alone.
- A tiny reverse-proxy (any host) - HTTP Basic Auth in front of the static files - keeps out search engines and casual visitors, not real per-user identity.
None of these are bx-sites features - they're host-level settings.