Skip to content

Deploy

Publish public/ to GitHub Pages, Cloudflare Pages or any static host — matching baseURL, Content Security Policy, the acceptance checklist and rollback.

An OINK site’s output is a plain static directory, deployable anywhere that hosts static files, with no Node runtime, no server-side rendering and no build plugin. The host’s side is three things: run one command with the right Hugo version, publish public/, and keep baseURL matching the final address.

The prerequisite is a warning-free production build locally.

Getting baseURL right

baseURL is the commonest source of failure, and it fails quietly: the page opens, but the search index 404s, page action links point at the wrong place, and some assets do not load.

Deploying at a domain root:

hugo.yml
baseURL: https://oink.pgsty.com

Deploying to a subpath (https://example.com/docs/), the path must be in baseURL:

hugo.yml
baseURL: https://example.com/docs/

It can also be overridden at build time, so one source deploys to several places:

Terminal
hugo --gc --minify --baseURL "https://example.com/docs/"
Do not fix a subpath with canonifyURLs

Hugo’s canonifyURLs defaults to false; keep that default. OINK’s templates and content links all resolve against baseURL: a wrong path means a wrong baseURL, and turning canonifyURLs on rewrites the relative links that were already correct, making the problem harder to locate.

To tell whether it matches, look at the search index request path after a build: the browser should fetch <baseURL>/offline-search-index.en.json, and fetching it from anywhere else means baseURL is wrong.

Choosing a host

With the source on GitHub, one Actions workflow is enough: the build runs in Actions and the output is published through the Pages deployment API, with no gh-pages branch to maintain.

Commit the following file:

.github/workflows/pages.yml
 1name: Deploy Oink site to GitHub Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13concurrency:
14  group: pages
15  cancel-in-progress: false
16
17env:
18  GO_VERSION: 1.26.6
19  HUGO_VERSION: 0.164.0
20  # a workspace from a sibling checkout must never take part in a CI build
21  GOWORK: off
22  HUGO_MODULE_WORKSPACE: off
23  HUGO_CACHEDIR: ${{ github.workspace }}/.hugo_cache
24  GOMODCACHE:
25    ${{ github.workspace }}/.hugo_cache/modules/filecache/modules/pkg/mod
26
27jobs:
28  build:
29    name: Build Pages artifact
30    runs-on: ubuntu-latest
31    steps:
32      - name: Checkout
33        uses: actions/checkout@v7
34        with:
35          fetch-depth: 0
36
37      - name: Set up Go
38        uses: actions/setup-go@v6
39        with:
40          go-version: ${{ env.GO_VERSION }}
41
42      - name: Set up Pages
43        id: pages
44        uses: actions/configure-pages@v6
45
46      - name: Install Hugo Extended
47        run: |
48          curl --fail --location --silent --show-error \
49            --output "${RUNNER_TEMP}/hugo.deb" \
50            "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
51          sudo dpkg -i "${RUNNER_TEMP}/hugo.deb"
52
53      - name: Download Hugo module
54        run: go mod download github.com/pgsty/oink
55
56      - name: Build site
57        run: |
58          hugo --cleanDestinationDir --gc --minify --environment production \
59            --printPathWarnings --panicOnWarning \
60            --baseURL "${{ steps.pages.outputs.base_url }}/"
61
62      - name: Upload Pages artifact
63        uses: actions/upload-pages-artifact@v5
64        with:
65          path: public
66
67  deploy:
68    name: Deploy to GitHub Pages
69    environment:
70      name: github-pages
71      url: ${{ steps.deployment.outputs.page_url }}
72    runs-on: ubuntu-latest
73    needs: build
74    steps:
75      - name: Deploy
76        id: deployment
77        uses: actions/deploy-pages@v5

That is the workflow this site uses. Several pieces cannot be removed:

  • fetch-depth: 0 — with enableGitInfo on, “last modified” and contributor information need the full Git history, and a shallow clone leaves them empty.
  • setup-go plus go mod download — with the theme as a Hugo Module, Hugo needs Go to resolve it. A site installing the theme as a submodule uses submodules: recursive instead, and one using an offline archive commits themes/oink/; either way both steps go.
  • GOWORK: off and HUGO_MODULE_WORKSPACE: off — keep a local development go.work from taking part in the CI build, so CI verifies the published tag pinned in go.mod.
  • --baseURL "${{ steps.pages.outputs.base_url }}/" — a project site’s URL is https://<OWNER>.github.io/<REPO>/, and configure-pages computes it, so it need not be hard-coded.
  • --panicOnWarning — a warning means no publish.

In the repository, set Settings → Pages → Build and deployment → Source to GitHub Actions, push to main, and watch the first run on the Actions tab.

A custom domain goes in the Custom domain field on that same settings page, with DNS configured as prompted, after which baseURL in hugo.yml becomes that domain. Where the publishing flow needs a CNAME file in the output, put it at static/CNAME and Hugo copies it into public/ unchanged.

Cloudflare Pages builds from a connected GitHub / GitLab repository and creates a preview deployment per review branch. The build happens on the platform side, so no workflow is needed in the repository.

Import the repository under Workers & Pages and choose the production branch:

Build command
hugo --gc --minify --printPathWarnings --panicOnWarning
Build output directory
public
HUGO_VERSION
0.164.0 (or another version the theme has verified)
GO_VERSION
Needed only for the Hugo Module method; pin a version the build image supports
SKIP_DEPENDENCY_INSTALL
1

Four notes:

  1. HUGO_VERSION must be set explicitly, in both the Production and Preview environments. The Cloudflare v3 build image’s default Hugo is older than OINK’s required 0.160.1, and leaving it unpinned changes the toolchain silently when the image updates.
  2. SKIP_DEPENDENCY_INSTALL=1 turns off the generic dependency install step. A consuming OINK site needs no Node.js, and a package.json present only for maintenance tooling should not be installed by the platform.
  3. Where the Hugo site is not at the repository root, set Root directory to the site directory; the output directory resolves against it.
  4. A preview deployment is not a production release. Where a preview needs the generated Pages URL as its base URL, use hugo --gc --minify --baseURL "$CF_PAGES_URL", and rebuild for production with the canonical domain.

Check the first build log: a healthy consuming OINK build is one Hugo command, with no npm, PostCSS or Autoprefixer step and no download of the theme’s own browser assets.

Netlify — build command hugo --gc --minify, publish directory public, environment variable HUGO_VERSION. The same settings can live in the repository:

netlify.toml
[build]
command = "hugo --gc --minify --printPathWarnings --panicOnWarning"
publish = "public"

[build.environment]
HUGO_VERSION = "0.164.0"

With the theme as a submodule, enable recursive submodule checkout; with a Hugo Module, the build environment needs Git and Go. Production and preview should use one Hugo version, unless the preview environment exists to test an upgrade.

Vercel — the same three things: build command hugo --gc --minify, output directory public, environment variable HUGO_VERSION. It likewise needs no npm install.

Any static server (Nginx / Caddy) — lay the contents of public/ down as they are:

/etc/nginx/conf.d/docs.conf
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/oink;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
}

The site is purely static; there is no path to forward to an application server.

Object storage — Hugo has a deploy command; put the target in the configuration:

hugo.yml
deployment:
  targets:
    - name: aws
      URL: 's3://www.your-domain.tld'
      cloudFrontDistributionID: E9RZ8T1EXAMPLEID

Run hugo deploy after a build: it compares the remote with public/, uploads only what changed, and invalidates the CDN cache when given a cloudFrontDistributionID. Without --target it uses the first target, and --dryRun shows what would change first. Two prerequisites: a Hugo binary built withdeploy (visible in hugo version), and cloud credentials supplied through the standard environment variables or configuration file (on AWS, confirm with aws s3 ls first).

Offline packaging — in a network-isolated environment, build on a connected machine and carry the output across as one package:

Terminal
hugo --gc --minify --baseURL "https://docs.internal.example.com/"
tar -czf oink-site-$(date +%Y%m%d).tar.gz -C public .

# on the target machine
tar -xzf oink-site-20260817.tar.gz -C /var/www/oink

Build with the target environment’s baseURL from the start; the absolute links in the output cannot be changed after unpacking.

A host without Go — the Hugo Module method needs Go in the build environment. Where a platform does not provide it, switch to a Git submodule (running git submodule update --init before the build) or an offline archive (committing themes/oink/) — see From scratch and other install methods.

Keeping preview deployments unindexed

Hugo’s -e / --environment selects build-time behaviour and does not change the site’s content, but three things in OINK follow it: only production emits <meta name="robots" content="index, follow">, only it makes robots.txt read Allow: /, and only it renders the Google Analytics template. Do not build PR previews and staging with --environment production:

Terminal
hugo --gc --minify --environment staging --baseURL "$PREVIEW_URL"

The output then carries noindex, nofollow and Disallow: /, and reports nothing to an analytics service.

Content Security Policy

The runtimes, fonts and icons the theme ships are all same-origin assets, so a strict Content Security Policy is workable. The theme provides no general policy: which directives you need depends on what the site enabled.

Five things change the directives needed:

  • Inline HTML and inline scripts written by authors, which are the author’s responsibility under renderer.unsafe: true.
  • ECharts $fn: callbacks: the callback functions are registered by the site on window.OinkEchartsFunctions, and the registering script’s origin belongs in script-src.
  • Analytics scripts: the script the site inserts, and the destination it reports to.
  • Remote API specifications and self-hosted diagram services: these land in connect-src and img-src.
  • giscus: script-src and frame-src must both permit it.

Start from a minimal policy covering only reviewed features and permit things one at a time: keep ECharts options pure data where no callback is needed, review inline scripts written by authors, and add a remote origin only for an integration the site deliberately enabled. Subresource origins in the output can be swept first with the script in Verifying an offline build.

Acceptance checklist

Walk this table after deploying. The first four are build-time; the rest have to be checked on the real URL.

A warning-free build
The build command carries --printPathWarnings --panicOnWarning and the log has Total in …
baseURL is correct
<link rel="canonical"> in the page source points at the real production address, subpath included
Sitemap
<baseURL>/sitemap.xml resolves; a multilingual site has an index pointing at /en/sitemap.xml and /zh/sitemap.xml
robots
<baseURL>/robots.txt reads Allow: / with a Sitemap: line; a preview deployment should read Disallow: /
Search index
The browser can fetch <baseURL>/offline-search-index.<language>.json, and site search returns results
Markdown output
Appending index.md to any page URL returns plain text (where the site enabled markdown under outputs.page)
llms.txt
<baseURL>/llms.txt and <baseURL>/zh/llms.txt resolve (where the site enabled LLMS under outputs.home)
Both languages
Documentation, blog and home pages open in both, and switching language lands on the corresponding page rather than the home page
Appearance and interaction
The light/dark toggle, the print view and representative components (callouts, tabs, code block copy) all work
404
Visiting a path that does not exist shows the site’s own 404 page

The switches for sitemap.xml, robots.txt, .md and llms.txt are in Configuration, and the agent output details are in AI-agent support.

Rollback

Rolling back a static site means republishing the last known-good commit; never edit files by hand in production.

  • GitHub Pages: find the last successful Deploy Oink site to GitHub Pages run in Actions and click Re-run all jobs; or git revert the offending commit and push again.
  • Cloudflare Pages / Netlify / Vercel: pick the last successful deployment from the list and use the platform’s Rollback / Publish deploy to make it production again.
  • A self-hosted static server: keep the previous tar.gz and unpack it over the top. The dated suffix in offline packaging exists for exactly this.

Where the problem is a theme upgrade rather than the content, what rolls back is the version pinned in go.mod — see Upgrade.

  • Local preview — the full production build command, clearing caches and offline verification
  • Troubleshooting — 404s, empty search, platform-specific faults
  • Analytics and SEO — being indexed correctly after launch
  • Upgrade — upgrading the theme version and rolling back
  • ConfigurationbaseURL, outputs and the other site keys