# pages.sr.ht Deployment pages.sr.ht hosts static websites. Each sourcehut user gets a subdomain at `username.srht.site` for free, plus unlimited custom domains. TLS is provisioned automatically. ## The core publish workflow ```yaml image: alpine/edge packages: - hut oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site sources: - https://git.sr.ht/~username/my-site tasks: - build: | cd my-site # whatever produces the static output tar -czf ../site.tar.gz -C public . - publish: | hut pages publish -d "$site" site.tar.gz ``` Three things to get right: 1. **`oauth: pages.sr.ht/PAGES:RW`** — gives the build a short-lived OAuth token with publish permission. The worker pre-writes `~/.config/hut/config` with the token, so `hut` Just Works (it does **not** read `$OAUTH2_TOKEN`; the env var is also set, but it's for `curl --oauth2-bearer`). Do *not* use a personal access token as a secret for this — it's worse in every way. 2. **The tarball must contain only files at the top level**, not a wrapping directory. `tar -C .` does this. If you tar `public/` instead, you'll get a directory listing at `/public/` on the site instead of an index page. 3. **`hut pages publish -d `** — `-d` takes the fully qualified domain, exactly as configured. Domains are case-sensitive in some edge cases; stick to lowercase. ## Tarball requirements pages.sr.ht is strict about what it accepts: - **Format**: `.tar.gz` (gzipped tar). Plain `.tar`, `.tar.bz2`, `.tar.zst`, `.zip` are not accepted. - **Only directories and regular files**, mode 644. Symlinks are rejected. - **No executables**: file mode must be 644. Don't `chmod +x` anything before tarring. - **Files at the top level**: `index.html` at the tar's root, not nested in a directory. - **Size limit**: 1 GiB per site. - **Invalid data is silently discarded** (per the API docs). If your tarball has problems, pages.sr.ht may accept the publish call but serve nothing or stale content. When debugging, list the tarball: `tar -tzf site.tar.gz | head`. Common tar invocation patterns: ```bash # Site files are in ./public/ (Hugo, Gatsby default, Eleventy default) tar -czf site.tar.gz -C public . # Site files are in ./_site/ (Jekyll default) tar -czf site.tar.gz -C _site . # Site files are in ./out/ (Next.js export default, Astro default for static) tar -czf site.tar.gz -C out . # Site files are in ./book/ (mdBook) tar -czf site.tar.gz -C book . # Already at the project root (raw HTML, no generator) tar -czf site.tar.gz . ``` ## `hut pages publish` flags ``` hut pages publish -d [-s ] [--protocol https|gemini] [--site-config ] ``` - `-d ` — required. Fully qualified domain. - `-s ` — optional. Publish to a sub-path. Files at the root of the tarball end up at `https://domain//`. The rest of the site is preserved (this lets you update part of a site without re-uploading everything). Maps to the GraphQL `publish(subdirectory: ...)` argument. - `--protocol https|gemini` — defaults to `https`. Set to `gemini` for Gemini-protocol publishing (the same `pages.sr.ht` GraphQL endpoint serves both; the `Protocol` enum has `HTTPS` and `GEMINI`). - `--site-config ` — optional. Path to a JSON file controlling per-site options. Schema (all fields optional): ```json { "notFound": "404.html", "fileConfigs": [ { "glob": "*.png", "options": { "cacheControl": "max-age=15552000" } }, { "glob": "*.css", "options": { "cacheControl": "max-age=7200" } } ] } ``` `notFound` is the path (inside the tarball) of the file to serve for 404s. `fileConfigs[].glob` is matched against the request path; `cacheControl` sets the response header verbatim. Maps to the GraphQL `publish(siteConfig: ...)` argument. ## Custom domains DNS setup depends on whether it's an apex domain (`example.com`) or a subdomain (`www.example.com`, `docs.example.com`): ### Apex domains — A + AAAA CNAME on the apex is not legal under RFC 1034 (and many registrars reject it). pages.sr.ht's solution is fixed IPs you point an A/AAAA record at: ``` example.com. IN A 46.23.81.157 example.com. IN AAAA 2a03:6000:1813:1337::157 ``` This is what the upstream `srht.site` docs publish. The IPs are stable but **not guaranteed forever** — sourcehut reserves the right to change them with 30 days' email notice to the affected accounts. Keep your meta.sr.ht email current. If your self-hosted pages instance (e.g. on `srht.bigb.es`) uses different IPs, check your instance's `pages.sr.ht/origin` or the operator's docs — the IPs above are only correct for the upstream `pages.sr.ht.` service. ### Subdomains — CNAME ``` www.example.com. IN CNAME pages.sr.ht. docs.example.com. IN CNAME pages.sr.ht. ``` The trailing period is significant (it makes the target absolute). Some lousy registrars choke on it — drop it if they do; most still infer correctness. ### Publishing After DNS resolves, publish to the domain by name: ```bash hut pages publish -d example.com site.tar.gz ``` 3. **TLS is auto-provisioned** on first request. The first load may take a few seconds while a cert is obtained. **Multi-subdomain publishing**: `example.com` and `www.example.com` are *separate sites*. You must publish to both: ```yaml tasks: - publish-apex: | hut pages publish -d example.com site.tar.gz - publish-www: | hut pages publish -d www.example.com site.tar.gz ``` There's no redirect-from-www feature; if you only want one canonical hostname, publish to the canonical one and don't configure DNS for the other. The `srht.site` recommendation is to skip `www.` entirely. ## Static site generator recipes ### Hugo ```yaml image: alpine/edge packages: - hugo - hut oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site sources: - https://git.sr.ht/~username/my-site tasks: - build: | cd my-site hugo --minify tar -czf ../site.tar.gz -C public . - publish: | hut pages publish -d "$site" site.tar.gz ``` If your theme uses Dart Sass, Alpine's `hugo` package may not include the extended build. Either use the Hugo binary from upstream releases, or switch to `archlinux` where `hugo` is the extended build by default. ### Zola ```yaml image: alpine/edge packages: - zola - hut oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site sources: - https://git.sr.ht/~username/my-site tasks: - build: | cd my-site zola build tar -czf ../site.tar.gz -C public . - publish: | hut pages publish -d "$site" site.tar.gz ``` ### Jekyll Jekyll wants Ruby; Alpine works but Ruby builds tend to be smaller/faster on Debian. ```yaml image: debian/stable packages: - ruby - ruby-dev - build-essential sources: - https://git.sr.ht/~username/my-site oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site tasks: - install-hut: | # hut isn't in Debian; build from source or download a release curl -L https://git.sr.ht/~xenrox/hut/refs/download/v0.6.0/hut-0.6.0-linux-amd64 -o /tmp/hut chmod +x /tmp/hut sudo mv /tmp/hut /usr/local/bin/hut - install-jekyll: | sudo gem install jekyll bundler - build: | cd my-site bundle install bundle exec jekyll build tar -czf ../site.tar.gz -C _site . - publish: | hut pages publish -d "$site" site.tar.gz ``` Check the hut release URL — the version pin should be current; `git.sr.ht/~xenrox/hut/refs` lists tags. ### mdBook ```yaml image: alpine/edge packages: - cargo - hut oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site sources: - https://git.sr.ht/~username/my-book tasks: - install-mdbook: | cargo install mdbook echo 'export PATH=$HOME/.cargo/bin:$PATH' >> ~/.buildenv - build: | cd my-book mdbook build tar -czf ../site.tar.gz -C book . - publish: | hut pages publish -d "$site" site.tar.gz ``` `cargo install` puts binaries in `~/.cargo/bin`, which isn't on the default PATH in subsequent tasks. Appending to `~/.buildenv` makes it available in the next task. ### Raw HTML / no generator ```yaml image: alpine/edge packages: - hut oauth: pages.sr.ht/PAGES:RW environment: site: username.srht.site sources: - https://git.sr.ht/~username/my-site tasks: - publish: | cd my-site tar -czf ../site.tar.gz . hut pages publish -d "$site" ../site.tar.gz ``` ## Verifying the publish After `hut pages publish` succeeds, the site is live. The CLI prints the new version hash on success. To verify what's actually served: ```bash curl -sI https://username.srht.site | head -n 3 ``` If you've just configured a new custom domain, the first HTTPS request will take a few extra seconds while a TLS cert is provisioned. ## Listing and removing sites ```bash # List all your published sites hut pages list # Remove a site hut pages unpublish -d example.com ``` ## Site limits and rules From the pages.sr.ht docs: - **1 GiB total storage per site, after decompression.** Hard limit enforced at publish time. - **Static content only** — no server-side execution, no PHP, no CGI. - **TLS only** (HTTPS); plain HTTP redirects to HTTPS. Certs are issued automatically per-domain. - **Content-Security-Policy is fixed** for every published site. Among its effects: - Scripts and styles must be loaded from your own site, not a third-party CDN (`script-src 'self' 'unsafe-eval' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`). - Inline `