Deploy
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:
Deploying to a subpath (https://example.com/docs/), the path must be in
baseURL:
It can also be overridden at build time, so one source deploys to several places:
canonifyURLsHugo’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:
That is the workflow this site uses. Several pieces cannot be removed:
fetch-depth: 0— withenableGitInfoon, “last modified” and contributor information need the full Git history, and a shallow clone leaves them empty.setup-goplusgo mod download— with the theme as a Hugo Module, Hugo needs Go to resolve it. A site installing the theme as a submodule usessubmodules: recursiveinstead, and one using an offline archive commitsthemes/oink/; either way both steps go.GOWORK: offandHUGO_MODULE_WORKSPACE: off— keep a local developmentgo.workfrom taking part in the CI build, so CI verifies the published tag pinned ingo.mod.--baseURL "${{ steps.pages.outputs.base_url }}/"— a project site’s URL ishttps://<OWNER>.github.io/<REPO>/, andconfigure-pagescomputes 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:
Four notes:
HUGO_VERSIONmust be set explicitly, in both the Production and Preview environments. The Cloudflare v3 build image’s default Hugo is older than OINK’s required0.160.1, and leaving it unpinned changes the toolchain silently when the image updates.SKIP_DEPENDENCY_INSTALL=1turns off the generic dependency install step. A consuming OINK site needs no Node.js, and apackage.jsonpresent only for maintenance tooling should not be installed by the platform.- Where the Hugo site is not at the repository root, set Root directory to the site directory; the output directory resolves against it.
- 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:
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:
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:
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:
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:
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 onwindow.OinkEchartsFunctions, and the registering script’s origin belongs inscript-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-srcandimg-src. - giscus:
script-srcandframe-srcmust 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 --panicOnWarningand the log hasTotal in … baseURLis correct<link rel="canonical">in the page source points at the real production address, subpath includedSitemap<baseURL>/sitemap.xmlresolves; a multilingual site has an index pointing at/en/sitemap.xmland/zh/sitemap.xmlrobots<baseURL>/robots.txtreadsAllow: /with aSitemap:line; a preview deployment should readDisallow: /Search index- The browser can fetch
<baseURL>/offline-search-index.<language>.json, and site search returns results Markdown output- Appending
index.mdto any page URL returns plain text (where the site enabledmarkdownunderoutputs.page) llms.txt<baseURL>/llms.txtand<baseURL>/zh/llms.txtresolve (where the site enabledLLMSunderoutputs.home)Both languages- Documentation, blog and home pages open in both, and switching language lands on the corresponding page rather than the home 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 Pagesrun in Actions and click Re-run all jobs; orgit revertthe 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.gzand 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.
Related
- 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
- Configuration —
baseURL,outputsand the other site keys