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/site-checks.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:
CI and production builds can still inherit replacements or workspaces.
Do not commit a workspace containing developer-machine paths. To verify a
published tag, unset HUGO_MODULE_REPLACEMENTS and set both GOWORK=off and
HUGO_MODULE_WORKSPACE=off. Also inspect persistent replacements in go.mod
and Hugo configuration, and any _vendor/ copy; confirm the exact version in
hugo mod graph under the same environment before building.
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.
For the last step, use the output checker from a theme checkout matching your pinned release. It needs Python 3, not this documentation site’s test framework. Replace both absolute paths below and use the same base URL as the build:
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: /.- With local search enabled, each language has an index in
public/: production filenames areoffline-search-index.<language>.<hash>.json, while development omits the hash. Open search and confirm the actualdata-td-index-srcURL returns 200. - 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.
With local search enabled, open search and inspect its index request in the
browser’s Network panel. It must use the correct language and deployment
subpath and return 200. The page’s data-td-index-src attribute supplies the
actual URL: production filenames are
offline-search-index.<language>.<hash>.json; development filenames have no
hash. Do not test a guessed filename.
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. OINK Starter already includes the file below;
copy it only when assembling a site manually.
That is the workflow shipped by OINK Starter. 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.yaml 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.
OINK Starter ships .github/workflows/cloudflare-pages.yaml, a Direct Upload
workflow. The strict build stays in GitHub Actions and Wrangler uploads the
same public/ artifact to a Cloudflare Pages project.
- Create a Direct Upload Pages project. By default its name matches the
repository; override it with repository variable
CLOUDFLARE_PROJECT_NAME. - Add repository secrets
CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKEN. The token needs Account → Cloudflare Pages → Edit. - Run Deploy to Cloudflare Pages manually once. Set repository variable
CLOUDFLARE_PAGES_ENABLED=trueto deploy every push tomain. - The canonical URL defaults to
https://<project>.pages.dev/. SetCLOUDFLARE_SITE_URLwhen a custom domain becomes production.
The workflow pins Hugo Extended 0.165.0, reads Go from go.mod, disables local
module workspaces, and builds with --panicOnWarning before upload. It is the
recommended reproducible path for Starter users.
Cloudflare Git integration remains valid as a separate mode: configure build
command hugo --gc --minify --printPathWarnings --panicOnWarning, output
directory public, Hugo 0.165.0, and Go 1.27. Use Git integration or
the Direct Upload workflow for one project, not both. A preview deployment is
still not production proof; rebuild with its own URL and keep it unindexed.
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) — serve an unchanged copy of public/.
The example below uses a current symlink to a release directory; create it
with the offline packaging steps below. Set the host’s document root to that
link when configuring Nginx or Caddy:
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. Choose your own archive path
and a new release name for every artifact. On the Linux host, these commands
require write access to /var/www/oink and GNU mv; current must be absent or
a symlink, and current.next must not already exist. Configure Nginx or Caddy to
serve current, as above; this is a host setting, not a Hugo option.
Only after the build and packaging succeed, transfer this archive to the Linux host and run the following there. Use a new release name for every artifact:
The command chain switches current only after a new directory is created,
extraction succeeds and index.html exists. Check the deployed pages afterward
and retain the previous release directory for rollback. Never unpack a release
over an existing release directory.
Build with the target environment’s baseURL from the start; changing it requires
a new build.
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 shipped runtimes, fonts and icons are same-origin assets, but
script-src 'self'; style-src 'self' alone does not cover an ordinary OINK
page. The theme emits inline theme initialization and shell prepaint scripts,
inline styles for the initial canvas, theme colors and font roles, and style
attributes in some components. Markmap adds inline configuration and styles.
The theme provides neither a general policy nor automatic CSP hashes or nonce
injection; the deployment owns a policy derived from its actual built output.
Additional features 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.
Hash each permitted inline script/style block from the final deployed bytes,
or have the hosting layer inject a fresh nonce into both the response policy
and the corresponding tags. A nonce on a style tag does not authorize style
attributes; review those separately under style-src-attr. Recheck hashes when
content, configuration, minification, or the theme changes. Begin with
Content-Security-Policy-Report-Only and exercise light/dark startup, shell
state, menus, and every enabled component before enforcing it. An origin scan
alone cannot establish CSP compatibility.
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- With local search enabled, the URL from
data-td-index-srcreturns 200; production filenames contain a hash, and site search returns results Markdown output- Appending
index.mdto any page URL returns plain text (where the site enabledmarkdownunderoutputs.page) llms.txt- The primary and every enabled language root publish
llms.txtwhere the site enabledLLMSunderoutputs.home Enabled languages- Documentation, blog and home pages open in each, 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 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: point
currentback to the previous release directory from offline packaging. Do not overlay the old archive on the new files: paths added by the newer release would remain live.
On the Linux host, replace the example path with the retained known-good release.
The same symlink and GNU mv prerequisites apply:
Check a representative old page at the public URL and confirm that a path added only by the rejected release is no longer served.
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
Copy all four repository/category fields from the configuration generated for your own repository; replace every placeholder below before enabling comments:
All four of repo, repoId, category and categoryId must describe the
same repository and its selected Discussion category. They 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. Replace G-YOUR_MEASUREMENT_ID
with your own GA4 measurement ID before enabling this block:
The theme renders that script only in the production environment. A normal
hugo server uses development and does not report, but a hugo build defaults
to production even on a preview host. Select a non-production environment for
PR and staging deployments, with PREVIEW_URL set to their actual address:
See preview deployment settings.
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
For Plausible, replace your-site.example with the domain registered in your
own account before adding this hook:
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 per page and hreflang alternates for actual
translations, 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. In the 1.2.0 implementation, a missing translation is omitted from both
hreflang and og:locale:alternate. The visible language switcher may still
lead to that language’s home page; that navigation fallback is not a translation.
Each paginated Blog index has its own canonical URL. From page 2 onward, language alternates are omitted because pagination does not establish matching translated pages. OINK 1.2.0 includes these corrections; 1.1.0 retains the earlier behavior.
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
Run from your site’s root. Replace PAGE with a generated page on your site,
including any language prefix:
In the browser’s Network panel, check that any configured analytics request uses
your own measurement ID or registered domain. Repeat on a deployment built with
--environment staging: it should make no analytics request. If no analytics
is configured, there should be no analytics request in either environment;
other explicitly enabled integrations may still use their own remote services.
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 the current 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 published release tag or a deliberately selected
immutable commit, follows no branch, and does not use @latest. The example
below upgrades to the published v1.2.0 tag. For a later release, verify its
publication and module resolution before selecting that tag:
When selecting a tag, confirm that the module graph shows that exact version.
A deliberately selected immutable commit is normally recorded as a Go
pseudo-version; that is valid if it resolves to the intended commit, but it is
not evidence of a named release. Commit the resulting go.mod and go.sum.
For the published tag in this example, go.mod contains:
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,
remove that environment replacement and disable both GOWORK and
HUGO_MODULE_WORKSPACE. Inspect persistent replacements and _vendor/ too;
make build by itself does not prove which theme was resolved. See the
preview guide.
For a Git submodule, check that it has no local edits, fetch the tags, and check out the exact published version rather than following its remote branch:
Commit the updated submodule pointer after validation. For an offline archive
or clone, replace themes/oink/ with the selected version’s complete tree and
confirm that theme: still matches the directory name. The install-method
tradeoffs are 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 includes informational diagnostics, while
--panicOnWarning treats warnings as failures. Review the deprecation messages
emitted by the pinned Hugo version before upgrading it; the severity and
removal schedule depend on the deprecated feature.
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.
Upgrading from 1.0 to 1.1
This checklist covers the published v1.1.0 release. Each consumer still needs to update its dependency pin, rebuild and deploy; theme publication does not upgrade an existing site automatically.
No source migration is required from 1.0.0. Hugo Extended 0.160.1 remains the
floor; CI uses the pinned 0.165.0 toolchain. The module’s Go 1.27.0 directive is
unchanged from 1.0.0. On Hugo 0.160.x, a non-default generic zh language
alongside the regional Chinese catalogs needs locale: zh-CN.
Review the affected surfaces before choosing the new pin:
Languages- All 32 interface catalogs have the same native-message schema. Check the site’s language labels, plural counts, and RTL direction; authored translations remain the site’s responsibility.
Taxonomies- Root pages become term-card directories with a taxonomy switcher. Review any taxonomy-template or CSS overrides, author portraits, and localized breadcrumbs.
Sidebars- Cached trees preserve effective page settings and remain usable without JavaScript. Exercise collapse, hover restore, mobile drawer, and keyboard focus; hidden content must leave the focus order.
Groupssidebar_divider: trueretains a section’s children. Addbuild.render: neveronly when that group’s own outputs are intentionally omitted; verify child navigation, breadcrumbs, paging, Print, and Book contents.Root menus- Explicit
sidebar_root_menu: falsenow applies to self-root sections too. The current linkable root remains a location marker. Custom scripts- Feature-detect
OinkSidebarandOinkCommandPalette.registerSearchTailif an integration must also support 1.0.0. Restore branch state through the API rather than changing classes or ARIA attributes directly. Copying articles- With image zoom enabled, copy an image and its caption as plain text and rich HTML. Preview instructions must not enter the copied article; zoom and its keyboard controls must still work.
Print and Redoc- Check page and Book aggregate Print, heading and tab links, and local Redoc specifications under the real deployment prefix. Local specification paths are rooted under
static/.
params.ui.image_zoom and params.offline_search remain off by default. The
new search hook does not enable a remote provider or add query telemetry.
params.ui.scroll_spy and page-level scroll_spy remain accepted no-ops in
1.x; removing the obsolete patch does not disable normal outline tracking.
Update or remove affected site-level copies of theme code after comparing them with the new implementation. A copied old image-zoom script or sidebar partial will otherwise continue to hide the upstream fix.
To test local theme changes with the documentation site, use its sibling theme checkout without committing a filesystem replacement:
These commands validate the local checkout. For release acceptance, pin the published version, build without a module replacement, then validate the deployed pages. The authoring and API details live in content groups, the sidebar contract, search actions, and image zoom.
Upgrading from 1.1 to 1.2
Paper is the default in OINK 1.2.0. Set params.ui.preset: slate before
adopting this change if the site must retain its existing appearance.
preset_menu: true enables reader choice; its default remains false.
Ink and Terminal require an explicit preset or menu list. Their buttons do
not carry experiment badges; the configuration opt-in remains unchanged.
Review custom dark brand selectors as described in Brand.
OINK 1.2.0 is available as a published tag. Apart from the default appearance change above, no content migration is required. Hugo Extended 0.160.1 remains the compatibility floor. Update the module, then verify the site:
- Check the chosen preset, light/dark icons, keyboard and mobile menus, saved preferences, and custom font/accent overrides. Sun means light; moon means dark. Switching styles must not change the saved light/dark preference.
- Recheck explicit navigation, hidden subtrees, page links, Blog pagination canonicals, and SEO alternates for pages without translations.
- Check CJK keyword-only search summaries and outline links with literal percent signs. Compare source-derived Edit, History, and Create child links on Windows or mounted content; mappings must yield repository-relative paths.
- Check Landing content with JavaScript disabled or blocked, preserved metric formatting, dialogs and keyboard shortcuts, copy fallback, Draw.io controls, and numbered equations at narrow widths.
- Run a warning-fatal build for diagram endpoints and resource alt metadata;
malformed values now produce a warning and use a safe fallback. To disable
a PlantUML or Draw.io endpoint intentionally, use
falseor an empty string. - Use the revised PDF and migration tools when testing publication or content conversion. Review PDF remote-resource opt-ins and migration diffs, including code examples nested in lists. The consumer-upgrade helper ships with 1.2.0 for inventory, module updates and exact-version validation.
The Architecture, Components, Shell, and Migration contracts describe the published 1.2.0 behavior.
The content migration toolkit
A batch of 0.4 shortcodes became the current native Markdown forms. 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 → current 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 current 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 an OINK 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/homewarns with the new location, and strict publishing rejects it. -
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.
A validated source commit, a published tag that resolves through the module proxy, a consumer pin with its checksum, and a verified production deployment are separate states. One green local build does not prove the others.
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 current 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 | The warning names the allowed keys and ignores the bad attribute; style and on* are likewise dropped. --panicOnWarning turns it into a publishing failure |
shortcode "field": unsupported parameter "colour" at ... |
A shortcode parameter name is wrong | The warning names the shortcode, parameter, file, and line, then ignores the unsupported parameter or component. Ordinary preview remains usable; strict publishing fails |
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 page’s hreflang points at another language’s home page |
The published 1.1.0 behavior or a copied older SEO partial can reuse the language-switcher fallback | The 1.2.0 development implementation omits missing translations from SEO alternates. Check the resolved theme version and template overrides; add the corresponding page when a translation is intended. The visible language switcher’s home-page fallback remains valid |
Search
| Symptom | Cause | Fix |
|---|---|---|
| A search box that never returns results | No index was generated | Check params.offline_search and open search. Inspect the page’s data-td-index-src URL in Network: production uses offline-search-index.<language>.<hash>.json, development omits the hash. If absent, also check offline_search_on_serve during hugo server |
| 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 the Chinese page’s actual data-td-index-src URL), 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
Run the build in your site’s root. The output checker is a separate script
from a theme checkout matching your pinned release: replace /path/to/oink,
/path/to/my-site/public, and the base URL with your values. The remaining
commands are examples from this documentation repository’s test harness,
not commands supplied to every OINK consumer.
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. Keep the theme-tool path and
your site’s output path distinct; the complete example is 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