This is the multi-page printable view of this section. .
Operations
- 1: Local preview
- 2: Deploy
- 3: Comments
- 4: Analytics and SEO
- 5: Upgrade
- 6: Troubleshooting
This section covers what happens after the content is written: previewing locally, building and deploying the output, wiring up comments and analytics, following theme versions, and locating faults. The previous five sections decide how the site looks and what it says; this one decides whether it builds, where it is deployed, and how a problem is diagnosed.
Find it by task
| What you want to do | Where to go |
|---|---|
| See a change on your own machine | Local preview |
Build a deployable public/ |
Local preview |
| Deploy to GitHub Pages / Cloudflare / Netlify | Deploy |
Deploy to a subpath such as example.com/docs/ |
Deploy |
| Let readers comment at the bottom of a page | Comments |
| Connect Google Analytics or a self-hosted alternative | Analytics and SEO |
| Get indexed correctly by search engines | Analytics and SEO |
| Upgrade the theme, or migrate from Docsy or 0.4 | Upgrade |
| A build error, no search results, a 404 | Troubleshooting |
1 - Local preview
Two commands cover the daily work: hugo server previews changes locally, and
hugo produces a public/ deployable to any static host. The prerequisite is
Hugo Extended (0.160.1 or newer) on the machine, plus Go
when the theme comes in as a Hugo Module. The build depends on no Node.js, npm
or PostCSS — those serve only this repository’s own regression checks.
The preview server
From the site root (the directory holding hugo.yml):
Open http://localhost:1313/. Saving a file rebuilds and refreshes the browser, and switching Git branches triggers a rebuild too. The first start is slower: with the theme as a Hugo Module, Hugo has to download the module through Go into its cache, and every start after that reads the cache.
The switches worth knowing
-D/--buildDrafts,- Also builds pages with
draft: true -F/--buildFuture,- Also builds pages whose
date/publishDateis in the future -E/--buildExpired,- Also builds pages whose
expiryDatehas passed --disableFastRender,- Re-renders the whole site on every change instead of incrementally
-M/--renderToMemory,- Renders in memory only, writing no
public/ -N/--navigateToChanged,- The browser jumps to whichever page you saved
--bind,- The listen address; use
0.0.0.0to reach it from a LAN or outside a container -p/--port,- The listen port
--minify,- Minifies the preview too, to reproduce production rendering
--printPathWarnings,- Warns when two pages write to the same target path
The combination used while developing this site:
-DFE is shorthand for -D -F -E, building drafts, future and expired pages
together so a newly created page is visible while writing.
A change that did not take effect
Hugo enables fast render by default, rebuilding only what it judges affected.
When editing layouts, configuration, data/, or a file pulled in by include,
that judgement can miss, and the page appears unchanged. Three steps:
- Restart with
--disableFastRenderand see whether it comes back. - Hard-refresh the browser (
Cmd/Ctrl+Shift+R) to rule out browser cache. - If it still does not, clear the caches and restart.
Reaching it from another device
hugo server listens on 127.0.0.1 only, so no other device can reach it. To
preview on a phone or another machine:
--baseURL must be an address the other device can reach, or the page opens
while CSS and the search index — anything using an absolute path — point at
localhost.
Production build
Build deployable output with hugo, not hugo server:
The output goes to public/, which can be deployed independently of the source
tree. Each of the four switches does one thing:
--gc- Clears cached resources in
resources/_genthat are no longer referenced --minify- Minifies the HTML, CSS, JS and XML output
--printPathWarnings- Warns when two pages collide on one output path, the commonest silent error on a multilingual site
--panicOnWarning- Fails the build on the first WARNING
--panicOnWarning deserves its own note. Most of OINK’s degradation paths warn
rather than error: a missing required giscus key, an unsupported
params.comments.type, a configuration key Hugo has deprecated — each prints one
WARNING and moves on. CI logs are rarely read line by line, so those reach
production. Putting this switch in the build command makes zero warnings the
condition for a passing build.
This site’s CI build step (.github/workflows/pages.yml) is
hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --panicOnWarning, so any warning stops the deployment at
the build stage.
baseURL and the build environment
baseURL lives in hugo.yml and can be overridden on the command line:
When deploying to a subpath, --baseURL must include that path segment; the
details are in Deploy.
The build environment is chosen with -e / --environment; hugo defaults to
production and hugo server to development. That choice has three visible
consequences in OINK:
- Only
productionemits<meta name="robots" content="index, follow">; other environments emitnoindex, nofollow. - Under
productionrobots.txtisAllow: /; elsewhere it isDisallow: /. - Only
productionrenders Hugo’s Google Analytics template, and only there are static assets fingerprinted with SRI.
Build preview deployments (PR previews, staging) with a non-production environment, and the output declines indexing and analytics by itself:
Previewing in a container
A container is not required. Two situations suit one: a team that needs a pinned toolchain version, or one that would rather not install Hugo on every developer machine.
Go is in the image because Hugo needs it to resolve and download the module when the theme comes in as a Hugo Module. A site using a submodule, an offline archive or a plain clone can drop Go, and the image gets much smaller.
public/A container process is root by default, the generated public/ belongs to
root, and the host cannot delete it. In a shared environment, map the user ID
with --user "$(id -u):$(id -g)" (the production build command above already
does).
The image needs no Node.js, npm or PostCSS, and should have no step fetching remote browser assets. A network-isolated environment needs the base image and those two packages mirrored in advance.
Clearing caches
Hugo’s intermediate output lives in three places; clear them lightest first:
public/,- A page was deleted but is still live; or let the build clear it with
hugo --cleanDestinationDir resources/_gen/,- Image processing parameters, fonts or the accent colour changed and the page still looks old
hugo mod clean,- The theme version changed but the old one still resolves; add
--allto clear the whole module cache
Both public/ and resources/ belong in .gitignore; generated output is
never committed.
Working on the theme alongside
This section applies only when changing the theme and the site together. Point
the module at a local checkout temporarily with HUGO_MODULE_REPLACEMENTS,
leaving go.mod untouched:
This site’s Makefile wraps those commands and expects the theme checkout at
the sibling ../oink:
Whether through the environment variable or a Go workspace
(go work init plus HUGO_MODULE_WORKSPACE=go.work), CI and production builds
read only go.mod; go.work records a developer machine’s paths and is never
committed. To judge whether a release tag works, drop the replacement and build
once against the version in go.mod.
Verifying an offline build
Acceptance in a network-isolated environment has to cover both the build stage and the browser stage. Six steps:
- Start from a verified theme archive and an empty module cache (
hugo mod clean --all). - Block outbound HTTP, HTTPS and the Go module proxy.
- Run the production build
hugo --gc --minify --printPathWarnings --panicOnWarning. - Browse pages in both languages: a documentation page, a blog page, the home page, the 404.
- Exercise search, the light/dark toggle, diagrams and content components.
- Check subresource origins and confirm there is no unexpected remote host.
The last step uses a script from the theme repository that does not depend on the site’s test framework:
The script scans every href / src / srcset / poster and form action in
all four outputs, requiring each to be a site-relative path or http /
https / mailto / tel, and rejecting inline on* handlers and
javascript: URLs. An <iframe>, <script>, <link>, <img>, <video>,
<audio>, <embed>, <object> or <source> pointing at another host is an
error; where a site genuinely embeds third-party content, --third-party
permits it, and a multi-domain language configuration adds first-party hosts
with --allow-host.
One pass proves that commit in that environment. Run it again for every theme candidate and after every bundled-dependency update.
Verify
A clean production build should look like this:
It passes on Total in … with no ERROR and no WARNING. Then confirm:
- The log has no npm, PostCSS, Autoprefixer or browser-asset download step. One appearing means upstream Docsy’s process has crept into the configuration.
public/hassitemap.xmlandrobots.txt, androbots.txtreadsAllow: /.- On a site with local search,
public/hasoffline-search-index.<language>.jsonat its root. - Open representative pages with
hugo server: one documentation page, one blog page, the home page and the 404, in both languages and both colour schemes.
For a failing build or a wrong result, see Troubleshooting.
Related
- Deploy — getting
public/to GitHub Pages, Cloudflare or elsewhere - Troubleshooting — the four common fault classes: build, language, search, platform
- From scratch and other install methods — weighing Hugo Module, submodule and offline archive
- Configuration — every key in
hugo.yml
2 - 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:
Build commandhugo --gc --minify --printPathWarnings --panicOnWarningBuild output directorypublicHUGO_VERSION0.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_INSTALL1
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
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 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
3 - Comments
OINK’s comments run on giscus: each page maps to one GitHub Discussion, readers sign in with a GitHub account to post, and maintainers moderate in GitHub Discussions. The theme provides no comment backend of its own and bundles no provider other than giscus.
The prerequisite is a public GitHub repository; a visitor cannot read a private repository’s Discussions.
A page with comments enabled loads a script and an iframe from
https://giscus.app, which does not work in a network-isolated environment.
It is off by default and loads only when explicitly enabled. Where a site has
a privacy policy, this external data boundary belongs in it.
Preparing the GitHub repository
-
Choose a public repository to hold the comment threads; the site’s source repository works.
-
In the repository’s Settings → General → Features, tick Discussions.
-
Install the giscus GitHub App for that repository. Without the App, visitors cannot comment or react.
-
Choose a Discussion category. giscus recommends the Announcements type: only maintainers and the giscus bot can open a Discussion there, so readers cannot start one by accident.
The repository ID and category ID are public identifiers, not credentials. Never put a personal access token, an OAuth secret or a password in Hugo configuration.
Generating the configuration
Open giscus.app, fill in the repository, mapping and
category, and the page generates a <script> block below. Copy four of its
attributes into the OINK configuration:
data-reporepodata-repo-idrepoIddata-categorycategorydata-category-idcategoryId
The mapping decides which page corresponds to which Discussion. OINK defaults
to pathname, which suits a site with stable published paths and one repository
serving several domains or preview environments. Changing mapping or moving a
page after comments have accumulated makes giscus look for a different
Discussion: the existing comments are not deleted, but the page can no longer
find them. Settle the mapping before launch; where a URL really must change,
keep a redirect or rename the Discussion at the same time.
Enabling it site-wide
Write the generated identifiers into the site configuration:
That is this site’s live configuration. All four of repo, repoId,
category and categoryId are required: if any is missing or only whitespace,
Hugo prints one WARNING and skips giscus without failing the build — which is
why a production build carries --panicOnWarning. type accepts only giscus
today, and any other value likewise warns and skips. The params.comments key
names match Hextra’s, so a configuration migrated from Hextra transfers as it
stands.
The remaining keys (strict, reactionsEnabled, emitMetadata, term,
lang, lightTheme, darkTheme, ariaLabel, errorMessage) all have
defaults, defined fully in Configuration. A feature
switch takes either a YAML boolean or giscus-style 0 / 1.
Per-page control
comments in front matter overrides the site switch in either direction, and
the value nearest the page wins.
To enable comments on selected pages only, turn the site switch off while keeping the full repository configuration, then let chosen pages opt in:
To disable them on selected pages, leave the site switch on and let unsuitable pages opt out:
Use a cascade to set a whole section at once. This site writes comments: true
in the cascade of content/docs/_index.md, which is why a real giscus section
sits at the bottom of this page.
Where a site also configures services.disqus.shortname, giscus wins: an active
giscus suppresses Disqus, comments: false turns off both, and if a required
giscus key is missing it warns, skips, and lets Disqus take over.
Multilingual text
giscus’s interface language follows the current Hugo language automatically:
Simplified, Traditional and Hong Kong Traditional Chinese each map to the
corresponding giscus locale, and an unsupported language falls back to English.
Set lang explicitly only where the automatic choice is wrong.
What does need translating is the two strings on OINK’s side: the comment section’s accessible label and the loading-failure message. They are configured per language and merged with the global repository configuration:
A language layer only needs the differences; repo / repoId / category /
categoryId stay in params.comments.
Following light and dark
With theme: auto, the giscus iframe follows OINK’s light/dark control and the
browser’s prefers-color-scheme, so the comment section changes with the rest
of the page.
For a closer match to the site’s palette, give lightTheme / darkTheme two
giscus themes; each value is a built-in giscus theme name or CSS hosted by the
site. This site does the latter:
A fixed theme name in theme stops it following the toggle.
The giscus iframe loads from giscus.app, so reading a CSS file on your site
requires CORS to allow it. This site adds
Access-Control-Allow-Origin: '*' under server.headers in hugo.yml for
local preview; in production it is the host’s response header configuration.
Privacy and CSP
- OINK never asks for or stores a reader’s GitHub password or access token; signing in and posting happen entirely on the giscus / GitHub side.
- The comment initialization script is a same-origin asset shipped with the theme, added only to pages with comments enabled; a page without them has no such script.
- With
loading: lazy, the iframe loads only as the reader scrolls near the comment section. - Where a site has a strict Content Security Policy, both
script-srcandframe-srcmust permit giscus — merged into the existing policy rather than replacing other directives (the general rules are in Content Security Policy):
When the external script fails to load or no iframe is created, OINK ends the
loading state and shows errorMessage in a live status region rather than
leaving the page on “loading”.
Verify
Then confirm each of these:
- Open a page that should have comments: giscus appears at the bottom, showing “Sign in with GitHub”, with its interface in the current page’s language.
- Toggle OINK’s light/dark control and the comment section follows (with
theme: auto). - Open a page with
comments: falseand confirm there is neither giscus nor any other comment component. - Post a test comment, return to GitHub, and confirm a Discussion appeared in the chosen category and can be managed there.
Before the first comment or reaction creates a Discussion, a browser console message saying the Discussion was not found is expected.
When something is wrong, check in this order: WARNINGs in the build log (the
four required keys) → params.comments.enable and type → the page’s
comments front matter → whether the repository is public, Discussions are
enabled and the giscus App is installed → the browser console and response
headers (whether a CSP blocked giscus.app). If existing threads have gone
missing, restore the original mapping and page path first.
Related
- Repository links and page info — edit this page, open an issue, contributors and the “was this helpful?” widget
- Analytics and SEO — the other capability needing an external service
- Deploy — Content Security Policy and external integrations in preview deployments
- Configuration — every
params.comments.*key - Page parameters —
commentsin front matter
4 - Analytics and SEO
The theme loads no analytics, form or advertising script by default, and makes
no outbound request until configured. Connecting one takes explicit
configuration, and that external data boundary belongs in the site’s privacy
statement. SEO is the opposite: canonical, hreflang, the robots meta, Open
Graph and Twitter cards are generated per page by the theme, and what you have
to get right is baseURL and each page’s description.
Connecting Google Analytics
Use Hugo’s built-in service configuration with a GA4 measurement ID:
The theme renders that script in the production environment only (a hugo
build defaults to production, and hugo server to development). Local previews
and preview deployments therefore report nothing, and need no extra switch.
Do not also set the deprecated top-level googleAnalytics key. Where analytics
is not wanted, delete the block rather than filling in a fake ID.
Once configured, page views and events go to Google. A strict same-origin Content Security Policy also has to permit it — see Content Security Policy. This is a site decision, not a theme default.
Connecting another analytics service
Plausible, Umami, Matomo and the like need only a script inserted. The theme provides two injection points; create a file of the same name in the site repository and no theme change is needed:
layouts/_partials/hooks/head-end.html,- Analytics scripts, cookie consent scripts, meta tags the theme does not provide
layouts/_partials/hooks/body-end.html,- Third-party code affecting interaction rather than the first paint
Do not omit the hugo.IsProduction guard: without it, everyone’s local preview
reports into your analytics.
That is deliberate: a cookie consent script has to run before the analytics script to actually hold it back.
The “was this page helpful?” feedback widget is a separate matter: off by default, making no network request, and configured in Repository links and page info.
Page descriptions
<meta name="description"> takes the first non-empty value of:
- The page’s
descriptionfront matter - The page summary Hugo computes (
.Summary) params.descriptionin the site configuration
Writing one description per page is the only SEO action an author has to take.
It serves three purposes at once: the search engine snippet, the card subtitle
on a section index, and the result preview in site search.
A multilingual site writes one per language; do not copy the English description onto a Chinese page. The site-level default is per language too:
canonical and hreflang
The theme emits one canonical and a set of hreflang alternates per page, with
no configuration:
The hreflang codes come from each language’s locale (en-US / zh-CN on
this site), and the links from Hugo’s translation relationships. Where a page
has no counterpart in the other language, Hugo cannot find a translation and
falls back to that language’s home page. That is expected behaviour, and it also
tells you whether Hugo recognized the translation pairing.
The canonical is assembled from baseURL. A wrong baseURL points search
engines at addresses that do not exist, which is harder to notice than a build
failure. Run through the
deployment checklist before launching.
Full multilingual configuration is in Languages.
Social cards
The theme calls Hugo’s built-in Open Graph and Twitter card templates, and the title, description, URL, language and site name are all automatic:
To give a shared link an image, set images in front matter:
For a site-wide fallback, write the same key under params:
With an image, twitter:card changes from summary to summary_large_image
and og:image and twitter:image appear. This site sets neither, which is why
the rendered output above has no image tags.
Sitemap
Hugo generates it automatically, and a multilingual site gets an index:
Both the site default and per-page overrides are Hugo’s own:
changefreq and priority are hints rather than promises, and a search engine
may ignore them. What is worth doing before publishing is confirming that
drafts, private content and non-canonical copies stayed out of the sitemap, and
that each language’s file was generated.
robots.txt and staying unindexed
Hugo generates robots.txt only when the site configuration turns it on:
The template the theme supplies gives two results by build environment, with no content for you to write:
The robots meta in the page follows the same switch: index, follow in
production and outside print output, noindex, nofollow otherwise. Do not build
preview deployments with --environment production; a non-production build
declines indexing by itself.
The theme has no per-page noindex switch. Where a page should not be indexed,
the reliable answer is not to publish it (draft: true, or Hugo’s _build
options). To publish it and still keep it out, emit your own tag through the
head-end.html hook; the theme already emits one robots meta, and how a
search engine reconciles two is its own decision.
Checking indexing
A week or two after launch, confirm in this order that what search engines see matches what you think:
- Crawl permission: open
<baseURL>/robots.txtand confirmAllow: /rather thanDisallow: /. - Page inventory: open
<baseURL>/sitemap.xml, follow into a language sitemap, and check the page count. - Indexed count: search
site:yourdomainand check the order of magnitude; a page-by-page reconciliation is not needed. - Canonical addresses: results should land on the canonical URL, not a version with a
?parameter or an old domain. - Active submission: add the site in Google Search Console / Bing Webmaster Tools and submit the
sitemap.xmladdress, which is faster than waiting to be crawled.
Search metadata cannot compensate for the content itself: a thin, duplicated or
stale page stays that way however well its description is written.
Verify
Then check these in the output:
Confirm once more in a browser: open a representative page and look at the network panel — a site with no analytics should make no request to a third-party domain.
Related
- Deploy —
baseURL, the checklist, and keeping preview deployments unindexed - Repository links and page info — the feedback widget, edit links and last-modified time
- Languages — language configuration decides
hreflangand translation pairing - AI-agent support — the
.mdoutput andllms.txtwritten for models - Configuration —
services,sitemap,enableRobotsTXTand the rest
5 - Upgrade
Upgrading OINK is changing one pinned module version and confirming the site still builds warning-free. Most content needs no change; where it does — 0.4 shortcodes becoming v5’s native Markdown forms — a dry-run-first migration tool does it, so hundreds of files need not be edited by hand.
An upgrade changes rendered output. Create an upgrade branch before starting, and the cost of backing out is discarding a branch.
Read the release notes first
Every version’s changes, breaking changes and upgrade notes are in its release notes; read the target version’s before upgrading:
- The release series in this site’s project blog
- The Releases page on GitHub
The notes say whether content has to change, whether a configuration key was removed, and whether a default behaviour moved. Skipping this step means guessing afterwards why a page looks different.
Upgrading the Hugo Module
A production site pins a release tag or an immutable commit, follows no branch,
and does not use @latest:
The last command must show that tag itself resolving, not a pseudo-version
(v0.0.0-2026...-abcdef) or main. The pinned version lands in go.mod and is
committed with the code:
make dev and make check set HUGO_MODULE_REPLACEMENTS for that command
only, using the sibling theme checkout. To judge whether a release tag works,
use make build without a replacement; otherwise what is verified is the
local copy.
One line for each other install method. Git submodule: fetch the new ref with
git submodule update --remote themes/oink and commit the submodule pointer.
Offline archive and clone: replace themes/oink/ wholesale with the new
version’s unpacked tree, and confirm theme: still matches the directory name.
Weighing the three is in
From scratch and other install methods.
What to do after upgrading
That does three things at once: clears possibly stale caches, rebuilds with the new version, and turns any warning into a failure.
--logLevel info is there to surface Hugo’s deprecation notices. Hugo
deprecates in two stages: first a WARN (still usable), then an ERROR in the
next version (the build fails). Carrying --panicOnWarning finds them a version
early and leaves you the time to fix them.
Once the build passes, look with your own eyes: the home page, a documentation page, a blog page, the 404, both languages, both colour schemes, the print view, and anywhere the site customized something.
The content migration toolkit
A batch of 0.4 shortcodes became native Markdown forms in v5. The theme repository ships a tool for that, depending only on the Python standard library:
Four things to remember while using it:
- A dry run is the default, and only
--writetouches disk. Dry-run, read the diff, then write. - A second run should change nothing. A second
--writestill reporting changes means a transformation is not converging; stop and look at those files. - Text inside fences is untouched, so a documentation site demonstrating the old syntax is not damaged.
- A construct it cannot express is left as it stands and listed with
file:lineand a reason, as a manual work list rather than a failure.
To convert one class first, use --only with the keys in the table’s last
column:
Rebuild afterwards (with --panicOnWarning) and look at the rendered pages: the
tool guarantees correct syntax, not that the meaning is what you intended.
The 0.4 → v5 syntax map
{{%/* alert color= title= */%}},{{%/* details */%}},{{%/* pageinfo */%}}, hand-written<details><summary>,callout{{</* tabpane */>}}+{{%/* tab header= */%}},{{</* code-group */>}}+{{</* code-tab */>}},tabs{{</* filetree */>}}withfiletree/folderandfiletree/file,filetree{{</* gallery */>}}withgallery/image,gallery{{</* echarts */>}},{{</* infographic */>}},datafencedoc-cards/doc-card,nav-cards/nav-card,card/cardpane,doc-carousel,cards{{</* imgproc */>}},{{</* image */>}},image{{</* readfile file= */>}},includeThe fence attribute,{filename="x"}fencetitle{{</* badge outline= */>}},badge{{</* example */>}}+ a fence,{{</* book-figures kind="tbl" */>}},eg{{%/* _param x */%}},iframe,conditional-text,blocks/*,netlify, a kindlessxref,reportonly
What each new form looks like and what parameters it takes is on its page under Components.
Migrating from Docsy
OINK is a hard fork of Docsy: the content model, the td- naming, the Sass
variables and most front matter are still there. The core of a migration is
deleting the copies of the shared shell in the site and letting the theme’s
implementation take over — not rewriting the prose.
-
Pin the target version. Change
go.modto an OINK release tag, or use a complete versioned archive. During evaluation, an uncommittedgo.workcan point at a local checkout. -
Inventory the overrides. Sort every site-level file under
layouts/,assets/andstatic/into four classes: copies of the shared shell (delete after verifying), components OINK already provides (delete or rename mechanically), brand customization (keep, reduced to the smallest hook), and business-specific data and interaction (stays in the site). Delete by reference order, and do not emptylayouts/at once: the home page and download page may still call a partial you are removing. -
Move the configuration.
title,languages.*,github_repo,github_branch,page_widthandparams.ui.*all stay in their existing semantic positions; OINK opens no namespace of its own. Search and the logo are just keys to turn on:hugo.ymlDocsy’s camelCase search keys have been renamed in OINK:
offlineSearch,offlineSearchIndex,offlineSearchMaxResults,offlineSearchOnServeandofflineSearchSummaryLengthall become their underscored forms. Rename them deliberately — the migration registry that used to stop the build and name the replacement has been removed, so an old key is now simply a key nobody reads, and search stays off with no message at all. -
Fonts and styling compatibility. The Docsy Sass variables in the site’s
assets/scss/_variables_project.scssstill work as the seed values for the font roles, and need not be deleted to upgrade:$td-fonts-serif,$font-family-sans-serif,$headings-font-familyand$font-family-codeeach feed their role. Docsy’s Google Fonts switches$td-enable-google-fonts,$td-google-font-nameand$td-web-font-pathare no longer read by the theme; leaving them breaks nothing and does nothing, because OINK ships Inter, Chakra Petch and IBM Plex Mono and neither preset requests anything from Google Fonts. To change fonts, go through the token layer — see Brand and appearance. -
Convert the shortcodes. Docsy’s
alert,pageinfo,tabpaneandcardfamilies all have a v5 counterpart; convert them in bulk with the migration toolkit above, one--onlyclass at a time. -
Delete one group at a time, building after each. Rehearse on a scratch copy, recording the theme commit, the Hugo version, which files were removed and how many HTML files came out; only after confirming equivalence, repeat it on the production branch.
The “delete after verifying” class in step two is usually these files:
layouts/baseof.htmland the shared docs / blogbaseof*.html;- The navbar, footer, sidebar, TOC, search and head CSS partials and their hooks;
- The old brand documentation shell partials;
- Copies of the
asciinema,echarts,infographic,doc-carousel,details,tab/tabpane, card andparamshortcodes; - The JavaScript, Lunr copy, carousel code and SCSS that served only those implementations;
- PostCSS and Autoprefixer steps no site asset needs any more.
Two kinds of problem surface after the deleting.
A site’s own script reports $ is not defined: the theme does not bundle
jQuery, which Docsy used to load in every page’s <head>. Nothing in the theme
needs it, and a site that still does loads it itself:
A home page built from Docsy’s blocks/* fails the v5 build with
template for shortcode "blocks/cover" not found: the theme has no such
shortcode family. Switch to home page sections in
data/home/<language>.yaml, or give the page layout: landing — see
Home and landing pages.
Upgrading from 0.4
0.4 changed several defaults. If the page gained or lost something after the upgrade, check these first:
-
Sequential paging is on by default.
docs,bookandblogpages all have previous / next at the page end; documentation follows the sidebar tree and the blog follows time. A page deliberately outside any sequence opts out withpager: false. -
The navbar shows on every layout. Its compact state is one row of icon navigation, with no second mobile accordion menu, so local scripts and tests that depend on the old mobile menu have to go. A whole section without a navbar uses
navbar_enabled: falsein a cascade. -
The footer defaults to
fatsite-wide. Onlyfat/slim/noneare accepted, and footer data must live indata/footer/<language>.yaml(ordata/footer.yamlon a single-language site); a leftoverfooterkey indata/homefails the build with the new location. -
Single-key navigation is on by default:
/opens full search and\command-only mode. Training material describing the old behaviour needs updating. Page actions have also moved to a split button beside the breadcrumbs. -
The code block DOM changed. A
.td-codewrapper now encloses the original.highlight(both.highlightand.chromaare kept), so a direct child selector such as.td-content > .highlightin site CSS becomes the descendant selector.td-content .highlight. -
Two ICP footer parameters were removed:
footer_icpandfooter_icp_urlbecame one string accepting inline Markdown.hugo.yml -
Mathematics needs the site to enable passthrough. Hugo does not merge a theme’s
markupconfiguration, so a site using\(…\),\[…\]or$$…$$must enable the Goldmark passthrough extension in its ownhugo.yml— see Math.
The complete configuration for all of these is in Configuration and Layouts and page types.
Verify
An upgrade is not finished at “the build passed”. Look at each surface:
Documentation / Book- Sidebar order, paging, headings, page actions, numbering and cross-references
Blog- Chronological paging, RSS ownership, navbar and footer
Home / landing- Content without JS, the compact menu, print
Release pages- Derived download URLs, checksums, publication state
Components- One page each for the components the site uses most
Accessibility- A keyboard-only pass, focus order, both colour schemes, forced-colors mode
Deployment- Internal links and assets all keep the base path prefix
This site’s full gate is:
Another site runs the equivalent build, link, output and browser checks; the details are in Troubleshooting.
The source building, the tag being signed and resolvable through the Go proxy, the site pinning that tag, and production being deployed are four things, each recorded separately. Do not let one green local build stand in for them.
The last step happens in the real environment: deploy a preview, verify the pages and the browser’s network requests on the real URL, merge once reviewed, and smoke-test production afterwards.
Rollback
What rolls back is the version pin, not the working tree:
Three principles:
- Keep the pre-upgrade module pin, the site commit and the known-good deployment artifact, and restore all three together.
- Do not roll back only part of it. Putting a few old layout copies back on top of a new theme produces a hybrid harder to diagnose than either complete version.
- Keep the upgrade branch and its acceptance evidence. A rollback restores production first; it does not throw away the work already done.
Rolling back the deployed output itself (republishing the previous deployment) is in Deploy.
Related
- Deploy — rolling back deployed output
- Troubleshooting — reading a build error after an upgrade
- Local preview — clearing caches and the
go.workworkspace - From scratch and other install methods — weighing the four install methods
- Components — each component’s v5 form
6 - Troubleshooting
When something goes wrong, run a clean production build first and read from the first error; the ones after it are usually cascades:
An npm, PostCSS, Autoprefixer or browser-asset download step in the log means upstream Docsy’s process has crept into the configuration. A consuming OINK build is one Hugo command.
The four tables below are organized as symptom → cause → fix. Find the symptom row; there is no need to read from the top.
Build
| Symptom | Cause | Fix |
|---|---|---|
| The build demands a newer Hugo | The standard build is installed rather than Extended, or the version is below 0.160.1 | hugo version output must contain extended. With several Hugos installed, check PATH and any version pinning before installing another |
module "github.com/pgsty/oink" not found |
The theme did not resolve | Hugo Module: check hugo mod graph, go.mod, go.sum, and any stray workspace or replace. Submodule: does CI run git submodule update --init before Hugo. Archive / clone: theme: must match the directory name under themes/ |
| Module download hangs or times out | The Go module proxy is unreachable | Hugo pulls modules through Go, so GOPROXY applies. In mainland China, export GOPROXY=https://goproxy.cn,direct; in an isolated environment, use an offline archive or commit themes/oink/ |
{.cards}, {.steps}, {caption=…} appear as literal text |
The site has not enabled Goldmark block attributes | The three settings below must be in the site’s own hugo.yml; Hugo does not merge a theme’s markup configuration |
An image with an attribute line is wrapped in <p> and the caption does nothing |
wrapStandAloneImageWithinParagraph: false is missing |
As above; add all three together |
| Inline HTML is escaped into text | renderer.unsafe: true is missing |
As above |
\(…\) $$…$$ display literally |
The site has not enabled Goldmark passthrough | See Math; math: true is not the switch |
shortcode "tabs" must be closed or self-closed |
A {{< tabs >}} has no matching {{< /tabs >}} |
The error carries file:line:column; add the closing marker there |
template for shortcode "tabs" not found |
The body calls a shortcode that does not exist, or quotes shortcode syntax without escaping it | Documentation that explains shortcode syntax must escape it: add /* and */ inside the opening and closing markers so Hugo treats it as text rather than a call. A misspelled name is simply corrected |
... attributes: unknown attribute "witdh" at ... |
An attribute-line key is misspelled or not permitted | An attribute line accepts that component’s allowed keys plus class, data-* and aria-*; style and on* always fail the build. The allowed keys are in the error’s parentheses |
shortcode "field": unsupported parameter "colour" at ... |
A shortcode parameter name is wrong | A component parameter — a shortcode parameter or an attribute-line key — always fails the build and never degrades silently. The error is always “which shortcode → which parameter → which file and line” |
invalid params.ui.page_width "widee" (allowed: normal | wide | full) -- using "normal" |
A configuration or front matter value is not one of the accepted ones | Configuration degrades instead of stopping, so one typo does not serve HTTP 500 on every URL under hugo server. The message names the key, the value and the fallback used. Build with --panicOnWarning and it cannot ship |
| A page setting has no effect and nothing is reported | The key was written inside a ui: block in front matter |
Page keys sit at the top level of the front matter — the site key with ui. dropped. A ui: block there is read by nobody and reported by nobody; see Page parameters |
| The build passes but production is missing something | A WARNING nobody read | Add --panicOnWarning to the build command. An invalid configuration value, a missing required giscus key, an unsupported comments.type and Hugo’s deprecation notices are all warnings |
The three Goldmark settings:
The two commonest shortcode errors look like this; note the trailing
file:line:column:
Language
| Symptom | Cause | Fix |
|---|---|---|
| A translated page does not appear | Four possibilities, in order | ① hugo.yml has languages.zh with a weight; ② the filename is page.zh.md, with zh lowercase; ③ the translation’s front matter has no draft: true and no future date; ④ routing metadata matches the source file |
| Switching language lands on the home page | Hugo found no translation | This is by design: with no translation it falls back to the target language’s home page. Landing on the corresponding page requires that translation file to exist |
| An anchor link opens the page but does not scroll | The translated heading text differs, so the generated ID does too | Write the English ID explicitly on the translated heading: ## 安装 {#installation}. Where a heading contains a shortcode or inline HTML, do not guess the ID from the text — read the English page’s rendered HTML |
| Menus / home page sections are untranslated | They are not in pages but in configuration and data files | Menus are in languages.<lang>.menus, home sections in data/home/<lang>.yaml, interface strings in i18n/<lang>.yaml — see Languages |
A Chinese page’s hreflang points at the English home page |
That page has no English counterpart | Add the English page, or accept the fallback: it doubles as a probe for whether Hugo recognized the pairing |
Search
| Symptom | Cause | Fix |
|---|---|---|
| A search box that never returns results | No index was generated | With params.offline_search: true, the output root should have offline-search-index.<language>.json, one per language. Its absence means it is not enabled |
| The index file 404s | A wrong baseURL |
On a subpath deployment, a wrong baseURL is the commonest cause of a 404 index. Look in the browser’s network panel to see where it fetches the index — see Deploy |
Search fails under hugo server but works in a build |
The site turned the preview index off | params.offline_search_on_serve defaults to true, so preview matches production; an explicit false skips index generation during preview — remove it or set it back to true |
| Chinese queries find nothing | Usually not a tokenization problem | A CJK query uses the theme’s substring fallback. First confirm the Chinese page’s content reached the Chinese index (open offline-search-index.zh.json), then consider tokenization |
| A new page is not found while old ones are | The index is build output | Rebuild. Under hugo server, wait for the rebuild after editing |
params.search.algolia requires explicit appId, apiKey, and indexName values |
The three Algolia keys are incomplete | All three must be given explicitly; the theme will not use another project’s DocSearch credentials. If Algolia is not wanted, delete the block |
| The command palette finds no content | It and full-text search are two things | With the index unavailable the palette still opens, saying so, while page actions and commands work as usual — see Command palette |
Platform
| Symptom | Cause | Fix |
|---|---|---|
| A 404 or missing styles on GitHub Pages | A project site’s URL carries the repository path and baseURL does not |
Use --baseURL "${{ steps.pages.outputs.base_url }}/" from the workflow rather than hard-coding it. The full workflow is in Deploy |
| “Last modified” and contributors are empty on GitHub Pages | The checkout is shallow | Add fetch-depth: 0 to actions/checkout: enableGitInfo needs the full history |
| A Cloudflare Pages build says Hugo is too old | The build image’s default Hugo is older than the theme requires | Set HUGO_VERSION in both the Production and Preview environments, and set SKIP_DEPENDENCY_INSTALL=1 |
| The host’s build cannot fetch the theme | The build environment has no Go | Hugo Modules need Go. Where a platform does not provide it, use a submodule or commit themes/oink/ |
| CI output differs from local | go.work took part in the CI build |
Set GOWORK: off and HUGO_MODULE_WORKSPACE: off in CI so it reads only the version pinned in go.mod |
| A preview deployment got indexed | The preview was built in the production environment too | Do not pass --environment production for previews; a non-production build carries noindex and Disallow: / — see Analytics and SEO |
| macOS reports too many open files | Live preview watches more files than the shell limit allows | Exclude generated and irrelevant directories from the watch first — usually the real cause — and only then consider ulimit -n |
| Slow, or missed changes, under WSL | Working across a Windows mount point | Let Hugo work on paths inside the Linux filesystem; cross-filesystem change notification and permission behaviour break live reload |
| Bootstrap / Font Awesome / Lunr / Mermaid assets are missing | An incomplete distribution | Do not paper over it with a CDN URL. Confirm assets/third_party/, assets/js/third_party/, static/webfonts/ and VENDOR.json are all present, and re-fetch the same pinned version if one really is missing |
Checks a site can run
Beyond the build itself, a site can run these. The first two work on any OINK site; the rest are this repository’s npm scripts, and another site runs the equivalent.
A zero-warning build,- Duplicate output paths, invalid parameters, incomplete external integrations
Output trust check,- Every
href/srcin all four outputs is site-relative orhttp(s)/mailto/tel; nojavascript:URL and no inlineon*handler; a cross-site<iframe>,<script>or<img>needs an explicit--third-party Translation parity,- Whether each English page has a Chinese counterpart, and whether the rendered heading IDs line up; misaligned anchors surface here
The full gate,- Runs the six below in sequence
What each of the six covers:
test:base— builds once, then runs the Markdown style, translation parity, rendered Markdown and link checks.test:hugo-build— build assertions: blog metadata, RSS, content components, and a deprecation-free build.test:md-output— byte-level golden comparison of the Markdown andllms.txtoutput. Changing a component’s Markdown shape fails here.test:alt-site— builds once per alternate configuration intests/fixtures/*.yml, confirming the combinations still come up.test:favicons— golden comparison of the head output.test:release-pin-contract— whether the version the site advertises matches the one pinned ingo.mod.
Browser behaviour is a separate suite: npm run test:browser runs the
Playwright accessibility (axe WCAG AA), responsive shell, keyboard navigation,
content component, code block and scenario component suites in turn.
check-output-security.py lives in the theme repositoryIt sits under the theme’s bin/, is a product-level trust check any OINK site
can run, and depends on no site test framework. Clone the theme repository and
point it at your own public/; the arguments and usage are in
Verifying an offline build.
Diagnostic habits
For problems the tables do not cover, dig along these lines:
- Reproduce with a pinned Hugo Extended version rather than judging in an environment where the version floats.
- Clear
public/andresources/_genand rebuild, to rule out stale caches. - Compare the development and production configuration layers; many production-only problems are environment differences.
- Read the first error, not the last.
- Separate “theme behaviour” from “site override” with a minimal page: isolate the suspect content on its own page and re-enable site overrides in batches until one is implicated.
- Look at the failing page’s browser console and network panel, especially the paths of any 404 resources.
Getting help
Opening an issue with these saves a round trip: the Hugo version (the full
hugo version output), the theme version (hugo mod graph | grep oink), the
first complete error, and a minimal page or site that reproduces it.
- Theme and documentation issues: https://github.com/pgsty/oink/issues
- Issues with this site’s content: https://github.com/pgsty/oink.pgsty.com/issues
- Upstream Docsy compatibility discussion: https://github.com/google/docsy/discussions
Related
- Local preview — clean builds, clearing caches, containers and workspaces
- Deploy —
baseURL, the checklist and rollback - Upgrade — problems an upgrade introduces, and the migration toolkit
- Search — index scope, ranking and Algolia
- Languages — language configuration and the anchor alignment process