Deploying
The build is a folder of static files. Here is how to put it on GitHub Pages, Netlify, Vercel, Cloudflare Pages, a server of your own, or a container.
wf build writes build/: HTML, CSS, JavaScript, images, sitemap.xml, robots.txt, _headers, and a .gz beside every text file. That folder is the whole site. Any host that serves static files can serve it; there is no server process to run.
Before the first deploy:
- Set
meta.site_urlto the address the site will have, andbuild.base_pathif it lives under a sub-path (/docs). - Decide static or single-page (chapter 26): a single-page build needs the host to answer every path with
index.html. - Run
wf buildandwf verify— the second loads every page in a real browser and fails on errors.
Installing wf on a build machine
Every recipe below runs the same two lines on the host's build machine:
bash
Pin a version so a release cannot change a deploy under you: WF_VERSION=v4.0.1. Or build locally and upload build/ — it is only files.
GitHub Pages
A project site is served under https://<you>.github.io/<repo>/, so set "base_path": "/<repo>" and "site_url": "https://<you>.github.io/<repo>". Then .github/workflows/pages.yml:
yaml
name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- run: curl -sSL https://raw.githubusercontent.com/monzeromer-lab/WebFluent/master/install.sh | WF_VERSION=v4.0.1 bash
- run: ~/.webfluent/bin/wf build
- uses: actions/upload-pages-artifact@v3
with:
path: build
- id: deployment
uses: actions/deploy-pages@v4
In Settings → Pages, set the source to GitHub Actions. GitHub Pages serves 404.html for unknown paths, so a static build needs nothing else. It ignores _headers; the Content-Security-Policy still ships as a <meta> tag in every page.
Netlify
netlify.toml at the project's root:
toml
[build]
command = "curl -sSL https://raw.githubusercontent.com/monzeromer-lab/WebFluent/master/install.sh | bash && ~/.webfluent/bin/wf build"
publish = "build"
Netlify reads _headers from the build, so the security headers apply. For a single-page build, add public/_redirects holding /* /index.html 200.
Cloudflare Pages
Build command as for Netlify, output directory build. Cloudflare reads _headers and _redirects the same way.
Vercel
vercel.json:
json
{
"buildCommand": "curl -sSL https://raw.githubusercontent.com/monzeromer-lab/WebFluent/master/install.sh | bash && ~/.webfluent/bin/wf build",
"outputDirectory": "build",
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
Vercel does not read _headers: copy what the build wrote there into headers, including the Content-Security-Policy line. For a single-page build add "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }].
nginx
nginx
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/site;
gzip_static on; # serve the .gz files the build wrote
location / {
try_files $uri $uri/ $uri.html =404; # a static build
# try_files $uri $uri/ /index.html; # a single-page build
}
error_page 404 /404.html;
location = /sw.js { add_header Cache-Control "no-cache"; }
location ~* \.(webp|avif|jpg|jpeg|png)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header X-Frame-Options DENY always;
# add_header Content-Security-Policy "…as in build/_headers…" always;
}
Upload with anything: rsync -a --delete build/ server:/var/www/site/.
Apache
public/.htaccess is copied to the build's root:
apache
Options -MultiViews
RewriteEngine On
# A single-page build:
# RewriteCond %{REQUEST_FILENAME} !-f
# RewriteRule ^ /index.html [L]
ErrorDocument 404 /404.html
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
For the precompressed files, turn on mod_deflate, or serve .gz with MultiViews and AddEncoding gzip .gz.
Caddy
text
example.com {
root * /var/www/site
encode gzip
file_server {
precompressed gzip
}
try_files {path} {path}/ {path}.html /index.html
header X-Content-Type-Options nosniff
}
Docker
dockerfile
FROM debian:stable-slim AS build
RUN apt-get update && apt-get install -y curl ca-certificates
RUN curl -sSL https://raw.githubusercontent.com/monzeromer-lab/WebFluent/master/install.sh | bash
WORKDIR /site
COPY . .
RUN /root/.webfluent/bin/wf build
FROM nginx:alpine
COPY --from=build /site/build /usr/share/nginx/html
Add the nginx settings above as /etc/nginx/conf.d/default.conf.
Cache headers
| Files | Cache-Control | Why |
|---|---|---|
|
HTML, |
|
their names do not change between builds |
|
|
|
a stale worker holds a stale site (Offline) |
|
resized images ( |
|
the name changes when the file does |
|
|
your choice | they keep the names you gave them |
no-cache still lets the browser keep a copy: it asks, and a 304 answers without sending the file again.
Security headers
With build.csp on, every page carries its Content-Security-Policy as a <meta> tag, and build/_headers holds the same policy plus X-Content-Type-Options, Referrer-Policy, X-Frame-Options and frame-ancestors — which only work as real headers. Netlify and Cloudflare Pages read the file; elsewhere set the headers as above. Serve over HTTPS, with HSTS once you are sure. Security has the checklist.
An API on the same origin
The simplest way to call a backend from a WebFluent site is to serve both from one origin — / the site, /api the backend — so there is no CORS and a session cookie just works:
nginx
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
Then api Backend(base: "/api") needs nothing else. Netlify, Vercel and Cloudflare have the same with a rewrite to an external URL.
After deploying
- Open a deep link directly and reload it: a 404 means the host is not falling back to
index.html(single-page) or thebase_pathis wrong. - View a page's source: the text should be in the HTML (static builds).
- Check the response headers in the browser's network panel.
- Run a link-preview debugger on a page, and submit
sitemap.xmlto search consoles.
On this page
Installing wf on a build machine GitHub Pages Netlify Cloudflare Pages Vercel nginx Apache Caddy Docker Cache headers Security headers An API on the same origin After deployingChecked by the test suite
Every code block in the guide is parsed, checked and type-checked on each release.