Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Get started

Start from the official OINK Starter, establish a working local baseline, then customize content, language, brand, integrations, and deployment in that order.

The recommended path for a new site starts from pgsty/oink-starter, not from a copy of this documentation and regression repository. The Starter is a public GitHub template: it pins a published OINK release, builds as-is, and contains only neutral project content and deployment workflows.

Two version numbers have different jobs

OINK’s declared compatibility floor is Hugo Extended 0.160.1. The current Starter and its CI use Hugo Extended 0.165.0 and Go 1.27. Use that pinned Starter toolchain for the path below; use the lower floor only when maintaining an existing site that deliberately supports it.

Choose a path

Starting point Recommended path Result
New documentation or project site OINK Starter A small three-language Docs, Blog, and Book site with two deployment workflows
Existing Hugo site From scratch Add the OINK module and required Goldmark settings without replacing content
Existing Docsy or older OINK site Upgrade Preserve content, migrate supported syntax, and review site overrides

Five-minute baseline

  1. Install the tools

    Install Git, Go 1.27 or newer, and Hugo Extended 0.165.0 or newer. The Hugo output must contain extended:

    $ go version
    go version go1.27.0 darwin/arm64
    $ hugo version
    hugo v0.165.0+extended+withdeploy darwin/arm64
    

    On macOS, brew install git go hugo supplies them. On Linux and Windows, use the official Hugo installation guide and Go downloads; choose Hugo Extended.

  2. Create or clone the site

    For a repository you intend to keep, open the Starter and select Use this template, then clone the repository GitHub created for you. To evaluate the untouched original locally:

    git clone https://github.com/pgsty/oink-starter.git my-docs
    cd my-docs
    hugo server
  3. Open the baseline

    Open http://localhost:1313/. The default Starter also publishes Chinese at /zh/ and French at /fr/. Confirm that Docs, Blog, Book, search, language switching, and light/dark mode all work before editing anything.

  4. Make one visible change

    Change the title and canonical URL at the top of hugo.yaml, then edit one sentence in data/home/en.yaml. A browser reload that shows both changes is the first useful proof that configuration, content, and the pinned theme are connected correctly.

Customize from shallow to deep

  • Use OINK Starter — choose languages first, then identity, home page, content, navigation, brand, integrations, and deployment.
  • Starter repository tour — which file owns each part of the site, what to replace, and what can be removed.
  • Writing pages — front matter, headings, links, images, drafts, and the page-end controls.
  • Components — add expression only after the content tree is stable.
  • Brand and appearance — logo, accent, typography, width, and CSS extension points.
  • Deploy — use the supplied GitHub Pages or Cloudflare Pages workflow, then verify the real public routes.

This order is deliberate. A site that first proves its build and content tree is easier to debug than one that changes languages, navigation, CSS, analytics, and hosting at the same time.

Publication gate

Before the first push, run the same warning-strict production build the Starter workflows use:

hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings --panicOnWarning

Success means the command ends with Total in …, prints no warning or error, and public/ contains the language roots and representative Docs, Blog, and Book routes. It does not yet prove deployment: a local build, a commit, a push, a green workflow, and correct public rendering are separate gates.

Next

Start with the complete Starter tutorial. If the template deliberately carries more structure than your project needs, use the repository tour to remove it safely. Use From scratch only when adding OINK to an existing site or when you explicitly want to assemble every file yourself.

1 - Use OINK Starter

Turn the official starter into your project site, one controlled layer at a time — languages, identity, home page, content, navigation, brand, integrations, and deployment.

pgsty/oink-starter is the supported starting point for a new OINK site. It is deliberately smaller than oink.pgsty.com: no theme documentation, analytics account, comment repository, browser regression suite, or PGSTY-specific brand is copied into your project.

As of 2026-09-20, the template pins OINK v1.0.0, Go 1.27, and Hugo Extended 0.165.0. Its default three-language, English-only, and English–Chinese profiles have all been built warning-strictly against that release.

What the template contains

Surface Included baseline First decision
Languages English, Simplified Chinese, French Keep all three, or select a supplied single/bilingual profile
Content Docs, Blog, and a short Book tutorial Rewrite the examples; delete a whole surface only when you do not need it
Home One compact data/home/<lang>.yaml per language Replace the project promise and destinations
Brand Neutral logo and favicon Keep them until real project artwork exists
Integrations Repository, Giscus, analytics, share, and feedback examples are commented Enable only complete configurations you intend to operate
Deployment GitHub Pages and Cloudflare Pages Direct Upload workflows Choose one production path and verify its real URL

The Starter’s own Book at /book/ is a four-chapter tour from preview to deployment. This page is the maintainer-grade version: it explains the order of changes, the boundaries between them, and the checks after each layer.

Create your repository

GitHub template, recommended

Open the Starter repository, select Use this template → Create a new repository, then clone the repository created under your account or organization:

git clone https://github.com/OWNER/PROJECT-DOCS.git
cd PROJECT-DOCS
hugo server

This gives your site its own Git history and keeps the original Starter as an upstream reference rather than as a remote you might accidentally push to.

Clone the original to evaluate it

For a disposable local evaluation:

git clone https://github.com/pgsty/oink-starter.git
cd oink-starter
hugo server

Do not start a real project by deleting this clone’s .git directory. GitHub’s template operation already creates the clean project boundary and preserves an auditable first commit.

Preview before changing anything

Open these routes:

  • /, /zh/, /fr/ — the three home pages;
  • /docs/, /blog/, /book/ — the three content surfaces;
  • one translated page, then the language switcher;
  • search and the light/dark control at a narrow viewport.

Also record the resolved module:

hugo mod graph | grep github.com/pgsty/oink

The template snapshot 137843b documented here pins github.com/pgsty/[email protected]; if the template has since changed, use the version in your clone’s go.mod. This unchanged preview is your baseline. After that first success, upgrade a site still using 1.0 as a separate step with the 1.0-to-1.1 checklist, before customizing content.

Customize in layers

Layer 1: language profile

The root configuration enables English, Chinese, and French. Before making other configuration edits, choose one of the supplied profiles when that is not your intended language set. Choose one of these commands:

cp examples/hugo.single.yaml hugo.yaml     # English only
cp examples/hugo.bilingual.yaml hugo.yaml  # English + Chinese

These are complete minimal configurations, not fragments: copying one replaces the commented integration examples in the root file. Do it at the beginning; if hugo.yaml already contains project changes, merge the languages and disableLanguages sections instead of overwriting it.

If you already changed the title and URL in Get started, keep that file. To change the default three-language setup to English and Chinese, add only disableLanguages: [fr] at the top level; use [zh, fr] for English only. This preserves the identity and integration settings you have already changed.

Disabled languages stay declared so Hugo recognizes .zh.md and .fr.md as translations and safely ignores them. If you remove a language permanently, remove its content and home data only after the selected profile builds.

Layer 2: identity

Change the two marked values at the top of hugo.yaml:

hugo.yaml
title: &siteTitle Project Name
baseURL: https://example.org/

The YAML anchor carries the title to all enabled languages. Then change the copyright holder and, after the new repository exists, uncomment its links:

hugo.yaml
params:
  copyright:
    authors: '[Project contributors](https://example.org/community/)'
    from_year: 2026
  github_repo: https://github.com/OWNER/PROJECT-DOCS
  github_branch: main

Run hugo server again and check the browser title, footer, edit/history links, and canonical URL. Do not change the logo yet unless the project has final artwork; text identity is easier to review first.

Layer 3: home page

The home page is data rather than an opaque layout override:

data/home/en.yaml
data/home/zh.yaml
data/home/fr.yaml

Edit one language first. In each file, sections fixes the order; hero, cards, and cta provide the content. Replace the promise, destination URLs, and sample card copy while keeping the structure. After the first language is right, translate the same information into the enabled peers.

For another composition, use the full registry in Home and landing pages; do not copy the Starter home partial, because there is no site-specific template to copy.

Layer 4: content and navigation

Rewrite or remove sample leaf pages under content/. Keep section roots until you decide whether that whole surface belongs in your project:

content/docs/  reference and task documentation
content/blog/  posts, design records, and release announcements
content/book/  a sequential long-form guide

The content tree becomes the sidebar. Top navigation lives in menus.main on the translated _index roots, so renaming Docs, Blog, or Book happens beside the content it names rather than in a second global menu tree. Keep translated files side by side and give corresponding headings the same explicit IDs:

page.md
page.zh.md
page.fr.md

Follow Organizing content before creating a custom navigation data file; the generated tree is enough for most sites.

Layer 5: brand and reader features

Replace assets/icons/logo.svg and static/favicon.svg when real assets are ready. Then enable the smallest useful configuration changes, one at a time:

hugo.yaml
params:
  ui:
    theme_color: '#245f94'
    typography: system
    image_zoom: true
    share: [mastodon, linkedin, email, copy]

For custom local fonts, use params.ui.fonts for family names or declare font files in site CSS. For layout, sidebar, search, and component settings, consult the Configuration reference rather than copying the much larger configuration of oink.pgsty.com.

Layer 6: integrations

The Starter leaves repository actions, Giscus, Google Analytics, feedback, and sharing off or commented. Enable an integration only after all of its required facts are known:

  • repository links need the real owner, repository, and branch;
  • Giscus needs its repository/category names and immutable IDs;
  • Google Analytics needs a project-owned measurement ID;
  • feedback records structured gtag events only when analytics is present;
  • assistant links send the current URL to a third party and therefore require an explicit policy choice.

An incomplete optional block should remain commented. See Comments, Analytics and SEO, and Repository links for the operating boundary of each integration.

Build and deploy

Strict local build

Before enabling a hosting workflow:

hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings --panicOnWarning

Commit hugo.yaml, go.mod, and go.sum; never commit generated public/, resources/, module caches, or a local module replacement.

GitHub Pages

The Starter already contains .github/workflows/github-pages.yaml. In Settings → Pages, select GitHub Actions as the source. A push to main builds with the pinned toolchain, asks GitHub for the correct project subpath, and publishes public/ through the Pages deployment API.

Cloudflare Pages

The supplied .github/workflows/cloudflare-pages.yaml uses Direct Upload. Create a Pages Direct Upload project, add CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, then run the workflow manually once. Set the repository variable CLOUDFLARE_PAGES_ENABLED=true for automatic deploys, and CLOUDFLARE_SITE_URL when the canonical address is not the default pages.dev domain.

Use either Direct Upload or Cloudflare Git integration for one project, not both. The complete host comparison and baseURL rules are in Deploy.

Verify and remove samples

Before calling the site ready:

  1. Search for placeholders such as Project Name, example.org, OWNER, and PROJECT, then decide whether each remaining occurrence is intentional.
  2. Open every enabled language root and representative Docs, Blog, and Book pages on desktop and mobile.
  3. Confirm language switching lands on peers, not the home page.
  4. Test search, dark mode, one component, Markdown output, print, 404, canonical URLs, and repository actions.
  5. Check the deployed workflow and the public URL separately from the local build.

Delete the sample Book or Blog only after removing its top-menu root and any home-page card that links to it. A warning-strict rebuild after each whole surface is removed keeps failures attributable to one change.

Next

Use the Starter repository tour as a file-level map, then continue with Writing pages and Configuration. For an existing site that should not inherit the Starter’s content model, use From scratch.

2 - Starter repository tour

A file-level map of oink-starter — what owns identity, languages, home, content, navigation, brand, deployment, and the pinned theme.

This page describes the repository created from pgsty/oink-starter. It is not a tour of the much larger oink.pgsty.com documentation and regression repository. The theme source is not copied into either site: go.mod pins it as a Hugo Module, and Hugo stores the resolved source in the Go module cache.

Top-level map

oink-starter/

  • oink-starter/
    • hugo.yamlidentity, languages, outputs, parameters, module import
    • go.modsite module and exact OINK release
    • go.summodule checksums
    • examples/
      • hugo.single.yamlEnglish-only complete profile
      • hugo.bilingual.yamlEnglish + Chinese complete profile
    • data/
      • home/
        • en.yamlone compact landing page per language
        • zh.yaml
        • fr.yaml
    • content/
      • _index.mdlanguage home roots
      • _index.zh.md
      • _index.fr.md
      • docs/Introduction, Get Started, Tutorial, Reference
      • blog/posts, design records, release announcements
      • book/sequential tutorial about the Starter
    • assets/
      • icons/logo.svgprocessed project logo
    • static/
      • favicon.svgcopied unchanged to the site root
    • i18n/
      • fr.yamlStarter-specific French interface overrides
    • .github/workflows/
      • github-pages.yamlstrict build and GitHub Pages deployment
      • cloudflare-pages.yamlstrict build and Cloudflare Direct Upload
    • README.mdoperating summary for repository maintainers
    • LICENSEtemplate source license

Generated public/, resources/, .hugo_build.lock, and module caches are ignored build state, not source.

What to change first

Path Responsibility Initial action
hugo.yaml Identity, canonical URL, languages, outputs, theme features, optional integrations Change the two marked values; choose a language profile before other edits
data/home/ Home-page promise, cards, calls to action Rewrite every enabled language after one language is approved
content/ All reader-facing material Replace example leaves; keep a section root until deciding to remove that whole surface
assets/icons/logo.svg Processed logo Replace only with final artwork
static/favicon.svg Browser icon Replace together with the logo review
params.github_* in hugo.yaml Edit/history/new-page/issue links Uncomment only after the destination repository exists

What to keep

  • go.mod and go.sum: together they pin and verify the template’s selected OINK release. Commit both; keep that baseline distinct from a later upgrade.
  • The three Goldmark settings in hugo.yaml: native Steps, Cards, Fields, image attributes, and Book targets depend on them.
  • outputs: removing markdown, LLMS, or print intentionally removes the corresponding Markdown, agent-index, or print surfaces.
  • fetch-depth: 0 in workflows when enableGitInfo stays on: last-modified and contributor facts need repository history.
  • GOWORK: off and HUGO_MODULE_WORKSPACE: off in CI: a developer’s local workspace must not replace the published release being verified.

Optional surfaces

Docs, Blog, and Book are independent top-level surfaces. To remove one safely:

  1. delete its content/<surface>/ tree;
  2. remove any home-page card or link that targets it;
  3. confirm no other page links to it;
  4. run a warning-strict build and inspect the remaining top navigation.

Do not delete only translated section roots: that creates language-specific navigation and fallback behaviour that is difficult to distinguish from a mistake. Remove a surface in all enabled languages or document the asymmetry.

The two configuration profiles under examples/ are optional after the language decision. They are useful references, but the root hugo.yaml is the only active site configuration.

Content and navigation

Under Docs and Book, directory structure and weight form the sidebar and pager sequence. Top navigation comes from menus.main on section roots. A translated root repeats the same identifier, parent, and weight while translating visible labels.

The Starter intentionally demonstrates the Documentation System model:

  • Introduction explains what and why;
  • Get Started gets a new user to a result;
  • Tutorial teaches an end-to-end task;
  • Reference records exact supported behaviour.

Rename or reshape those sections for the project, but preserve the separation between learning paths rather than mixing every kind of answer into one tree.

Language model

English source files end in .md; Chinese and French peers end in .zh.md and .fr.md. Home data uses language keys under data/home/. The root profile declares the languages, their locale, order, and site description.

The single and bilingual profiles keep disabled languages declared. This is intentional: Hugo then recognizes the unused suffixes as translations instead of rendering several files onto one English URL. Copy a profile only before project-specific configuration begins; afterwards merge changes by hand.

Where OINK lives

Two files establish the module boundary:

hugo.yaml
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: '0.160.1'
go.mod
module github.com/OWNER/PROJECT-DOCS

go 1.27.0

require github.com/pgsty/oink v1.0.0

This is the 137843b Starter snapshot used by the tutorial, not the latest OINK release. For a newer template, read its own go.mod.

hugo mod graph shows the resolved version. Production follows the exact tag in go.mod; a local HUGO_MODULE_REPLACEMENTS value is a development override and must never be committed or treated as release proof.

Deployment files

The GitHub Pages workflow runs automatically on pushes to main; repository settings must select GitHub Actions as the Pages source. The Cloudflare workflow runs manually, or automatically only after the repository variable CLOUDFLARE_PAGES_ENABLED=true is set. Its required account ID and API token remain repository secrets.

Keep only the workflows for deployment paths you operate. Cloudflare Direct Upload and Cloudflare Git integration are alternative ownership models for the same project, not two gates to run together.

Safe customization order

  1. Prove the untouched preview.
  2. Select languages, then change identity.
  3. Replace one home page and then its translations.
  4. Replace content and verify navigation.
  5. Change brand and reader features one group at a time.
  6. Enable complete external integrations.
  7. Run the strict production build.
  8. Deploy, then verify production independently.

Commit between layers when the repository is already yours. Small boundaries make a later regression or rollback attributable to one decision.

Verify

hugo mod graph | grep github.com/pgsty/oink
hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings --panicOnWarning
git status --short

The module graph names the pinned release, the build emits no warning or error, and Git status contains source edits but no public/ or cache files. Then open the enabled language roots and one Docs, Blog, and Book route before moving to deployment.

3 - From scratch and other install methods

Build a minimal OINK site in an empty directory, and weigh the four install methods — Module, submodule, offline archive, pinned source copy.

This is the manual alternative to the recommended OINK Starter. It builds a minimal site in an empty directory: a small hugo.yml plus one hugo mod get gives a single-language site you can preview. The cost is that the home page, example content, deployment workflow, and every component usage are yours to assemble.

For an existing Hugo site, use the short integration path below. For an existing Docsy site, see Upgrade.

The second half weighs four install methods: Hugo Module, Git submodule, offline archive, and pinned source copy. OINK 1.1.0 uses Go 1.27 and Hugo Extended 0.165.0 for release validation. The theme’s lower declared compatibility floor is for existing sites that deliberately retain an older toolchain.

Add OINK to an existing site

Work on a branch with the site’s current configuration and content preserved. Skip hugo new site and keep the existing configuration filename.

  1. If the site has no go.mod, run hugo mod init with your repository’s module path. Otherwise keep the existing module declaration.
  2. Run hugo mod get github.com/pgsty/[email protected].
  3. Replace the old theme selection with the OINK module.imports entry shown below; preserve unrelated imports and configuration. Merge the three markup.goldmark settings and markup.highlight.noClasses: false from the example. Do not replace your whole configuration with it.
  4. Review site-owned layouts/ and assets, old theme shortcodes, and page type/layout values: those overrides and conventions may still select the previous theme’s behavior. Keep content and make only the adaptations needed.
  5. Run hugo --panicOnWarning, then open an existing representative page with hugo server. Check its navigation, images, and code blocks before applying optional OINK features. Continue with verification.

From an empty directory to the first page

  1. Create the skeleton and fetch the theme

    hugo new site --format yaml my-docs
    cd my-docs
    git init
    hugo mod init github.com/example/my-docs
    hugo mod get github.com/pgsty/[email protected]

    What follows hugo mod init is your own site’s module path, usually the repository address. hugo mod get writes go.mod and go.sum, and both are committed.

    Before building, create .gitignore so generated files stay out of Git. Leave enableGitInfo off until you have made the first commit:

    .gitignore
    /public/
    /resources/
    /.hugo_build.lock
    /.hugo_cache/

    The newest version number is on GitHub Releases; the v1.2.0 on this page is what this site currently pins. A production site pins a release tag rather than following main: @latest is a one-off resolution, not a version policy.

  2. Writing hugo.yml

    For this new site only, rename the generated hugo.yaml to hugo.yml (Hugo accepts both) and replace its contents with the following. Existing sites should merge the relevant settings instead:

    hugo.yml
    title: Product Docs
    baseURL: https://docs.example.com/
    defaultContentLanguage: en
    # enableGitInfo: true        # the "last modified" time comes from git; make the first Git commit before enabling
    
    languages:
      en:
        label: English
        locale: en-US
        weight: 1
        title: Product Docs
        params:
          description: Everything about running Product in production
        menus:
          main:
            - { name: Docs, pageRef: /docs, weight: 20 }
            - { name: Blog, pageRef: /blog, weight: 50 }
    
    # The three Goldmark prerequisites: OINK's native Markdown components depend on them
    markup:
      goldmark:
        renderer:
          unsafe: true # allow inline HTML in content
        parser:
          attribute:
            block: true # attribute lines such as {.steps} {.cards} {caption=}
          wrapStandAloneImageWithinParagraph: false # only a block-level image can carry an attribute line
      highlight:
        noClasses: false # code colours follow light and dark mode
    
    params:
      offline_search: true
      github_repo: https://github.com/example/product-docs
      copyright:
        authors: '[Example Inc.](https://example.com/)'
        from_year: 2026
      ui:
        dark_mode: true
        sidebar_menu_foldable: true
        section_index: cards
    
    outputs:
      home: [HTML, markdown, LLMS]
      page: [HTML, markdown]
      section: [HTML, RSS, print, markdown]
    
    module:
      imports:
        - path: github.com/pgsty/oink
      hugoVersion:
        extended: true
        min: '0.160.1'

    What each of the five blocks governs:

    Block Governs Consequence of omitting it
    Top level + languages Site name, domain, languages and navbar menu A wrong baseURL sends every absolute link astray in production
    markup.goldmark The three component prerequisites An attribute line becomes a literal {.steps} in the prose
    params Search, repository links, shell switches Interactive features stay off; the theme does not decide for the site
    outputs The per-page .md, llms.txt and print pages No “Copy as Markdown” in the page menu, and no print view
    module References the theme and declares the Hugo floor The build cannot find the theme

    Mathematics additionally needs Goldmark’s passthrough extension; see Math. Every key’s full meaning and default is in Configuration.

  3. Write the first page

    Every top-level directory under content/ is a section, and the directory structure is the sidebar structure. A documentation section needs at least an _index.md:

    content/docs/_index.md
    ---
    title: Docs
    linkTitle: Docs
    description: Everything about running Product in production.
    weight: 20
    ---
    
    Start with [Install](/docs/install/).
    content/docs/install.md
    ---
    title: Install
    description: Install Product on a fresh machine.
    weight: 10
    ---
    
    ## Prerequisites {#prerequisites}
    
    > [!IMPORTANT]
    > Product needs PostgreSQL 18 or newer.
    
    ## Install {#install}
    
    ```bash
    curl -fsSL https://get.example.com | bash
    ```

    Write explicit {#id} anchors on headings: when a translation is added later, the two languages’ anchors have to correspond. How to write a page is in Writing pages.

  4. Preview

    hugo server

    Open http://localhost:1313/docs/; the Docs section lists Install. The home page is still empty until you add home content. Edit the Install page and confirm that the preview updates.

Other install methods

The steps above use a Hugo Module. The other three address particular constraints: network isolation, a platform that requires the build input to contain the whole theme tree, or an organization that reviews its own copy of the theme. Apart from hugo mod vendor, none of them creates a Go module, and the site references the theme with theme: oink rather than module.imports. The shared cost is that version resolution and integrity checking become your responsibility.

Hugo Module (recommended)

hugo mod init github.com/example/product-docs
hugo mod get github.com/pgsty/[email protected]
hugo.yml
module:
  imports:
    - path: github.com/pgsty/oink

The only method where Hugo resolves the version itself, verifies the checksum, and leaves an audit record in go.sum. hugo mod graph shows what actually resolved and hugo mod get -u upgrades. It needs Go on the machine.

Git submodule

Record an exact theme commit in the site repository:

git submodule add https://github.com/pgsty/oink.git themes/oink
git -C themes/oink fetch --tags
git -C themes/oink checkout v1.2.0
git add .gitmodules themes/oink
hugo.yml
theme: oink

CI must initialize the submodule before running Hugo, or themes/oink is an empty directory:

git submodule update --init --recursive

Offline archive

For network-isolated environments. Two paths, both prepared on a connected machine and carried in whole.

With hugo mod vendor, the resolved theme source is frozen into the site directory, and later builds need neither the network nor Go.

hugo mod vendor          # writes _vendor/, holding the theme's full source tree
tar czf ../my-docs.tgz . # put the archive outside the directory being archived

When _vendor/ exists Hugo prefers it (hugo mod graph prints +vendor), and module.imports in hugo.yml stays as it is. This step needs Go; the builds after it do not. Upgrading the theme means returning to a connected environment and running hugo mod get and hugo mod vendor again.

_vendor/ collects only the directories the theme mounts (assets, data, i18n, layouts, static) plus hugo.yaml and theme.toml. It does not include LICENSE, NOTICE or VENDOR.json. To redistribute that archive, take those three files from the theme repository as well.

With a tag source archive, no Go module is created; a version of the theme is simply unpacked into themes/oink/.

curl -L -o oink.tar.gz \
  https://github.com/pgsty/oink/archive/refs/tags/v1.2.0.tar.gz
mkdir -p themes/oink
tar xzf oink.tar.gz -C themes/oink --strip-components=1
hugo.yml
theme: oink

The theme repository’s root is the module root, so unpacking lands directly on layouts/, assets/, i18n/ and static/ with no further level to descend into. Redistribution must keep LICENSE, NOTICE and VENDOR.json; the last records each third-party runtime’s version, source, licence path and SHA-256, and is what an offline audit rests on.

When moving between machines, generate the archive and its checksum from an immutable tag on the connected side:

git clone --branch v1.2.0 --depth 1 \
  https://github.com/pgsty/oink.git oink
git -C oink archive --format=tar.gz --prefix=oink/ \
  --output=../oink-v1.2.0.tar.gz v1.2.0
shasum -a 256 oink-v1.2.0.tar.gz \
  > oink-v1.2.0.tar.gz.sha256

Carry the archive and its .sha256 into the isolated environment, verify, then unpack:

shasum -a 256 -c oink-v1.2.0.tar.gz.sha256
mkdir -p themes
tar -xzf oink-v1.2.0.tar.gz -C themes

An archive produced this way is your own artifact, not a project release. Whether a given tag’s release page carries an archive and a checksum file varies by release; verify the checksum independently when using a public attachment.

Before building offline, confirm the archive is complete. All eleven of these must be present:

themes/oink/

  • oink/
    • go.modmodule path declaration, used when resolving as a Hugo Module
    • hugo.yamltheme default parameters and the Hugo version floor
    • theme.tomltheme metadata, required by the theme: oink method
    • LICENSEApache-2.0
    • NOTICEupstream attribution; must be kept on redistribution
    • VENDOR.jsonthird-party runtime manifest: version, source, licence path, SHA-256
    • assets/SCSS, JS and the third-party runtimes shipped with the theme
    • layouts/templates, partials, shortcodes, render hooks
    • static/font files, published as is
    • i18n/32 interface language files
    • data/the SPDX licence table behind the page-end attribution line

Pinned source copy

When a hosting platform needs the theme files in the site repository, use the tag archive procedure above and unpack it into themes/oink/. Set theme: oink and commit the extracted files together with the tag and checksum you verified.

A plain git clone ... themes/oink leaves a nested .git directory. Adding it to the parent repository records a Git link, not the theme files; it therefore does not provide this self-contained source copy. Use a submodule if you want Git to track the theme by reference.

The four methods compared

Method Needs Go Version auditable Theme source in your repository Use when
Hugo Module Yes go.sum verifies automatically No The default
Git submodule No The repository records the commit By reference The theme source has to be in the repository
Offline archive No Checksums verified by hand Yes Network isolation
Pinned source copy No Record the tag and checksum Yes The platform requires a complete tree
A consuming site needs no front-end toolchain

Bootstrap, Font Awesome, the fonts, and the search and diagram runtimes all ship with the theme. A site needs no node_modules, no PostCSS, no RTLCSS and no CDN. Tutorials that install npm dependencies for a Docsy site describe upstream Docsy’s process and do not apply to OINK.

Developing against a local theme checkout

This section applies only when changing the theme and the site together. Clone the two repositories as siblings:

sibling directory layout
~/pgsty/
├── oink/            # the theme
└── product-docs/    # your site

Use the HUGO_MODULE_REPLACEMENTS environment variable to substitute the local checkout temporarily, leaving go.mod untouched:

cd ~/pgsty/product-docs
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> ../oink' hugo server

The documentation site’s Makefile is an alias for exactly these commands, and make dev and make check expect the theme checkout at the sibling ../oink:

Makefile: as the documentation site writes it
build:
	hugo --cleanDestinationDir --minify

check:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' npm test

dev:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' hugo server --renderToMemory

A Go workspace (go work init plus HUGO_MODULE_WORKSPACE=go.work) is an equivalent alternative. Both apply to the local machine only: CI and production builds use the version in go.mod, and go.work is never committed.

Verify

hugo mod graph                                       # which theme version actually resolved
hugo --gc --minify --printPathWarnings --panicOnWarning

It passes when the build ends with Total in … and no WARN or ERROR. Then confirm:

  • /docs/ opens and the sidebar holds the page you wrote
  • The navbar has a search box that finds the heading you just wrote
  • The light/dark toggle is present, and code block colours follow it (which shows markup.highlight.noClasses: false took effect)
  • git status --short lists only source changes; generated output is ignored. For the Module path, commit both go.mod and go.sum; other install methods keep their own theme source or submodule record.

4 - OINK CLI capabilities and next steps

The 2026-09-30 six-command CLI snapshot, its safety and automation features, and the development directions proposed at that time.
Historical first-stage snapshot

This page retains the 2026-09-30 command inventory and evidence. Its six-command, platform and suggested-CI statements describe that date. Current maintenance behavior belongs to the CLI contract and usage guide; the maintenance record preserves earlier acceptance for its identified source and binaries. Local acceptance does not imply a public CLI release or deployment.

oink is a command-line tool for OINK site maintainers. It brings site creation, environment diagnosis, output validation, local preview, and theme upgrades into one interface. Its six commands already form a usable local workflow. The next useful investment is to help more users install it, understand failures, and repeat routine maintenance. Migration, documentation version management, and API reference generation can follow one capability at a time.

This page explains current capabilities and possible next steps. For complete installation instructions, see Using OINK CLI. Executed tests are recorded in the first-stage acceptance report.

Current status

As of 2026-09-30, this page describes local 0.1.0-dev, commit e623d93. Code, tests, installation, and reproducible archives have been prepared and validated. Public CLI publication and deployment have not been completed. Future capabilities below are suggestions or existing proposals, not available commands or committed delivery dates.

Purpose and audience

The OINK theme owns presentation, navigation, search, content components, and output formats. Hugo loads configuration and renders the site. The CLI connects those inputs and results into repeatable maintenance: configuration mistakes, the actual theme source, broken references, and proposed upgrade changes should all have inspectable evidence.

The CLI is a standalone Go executable that calls external Hugo. It requires no Python, Node.js, account, or background service. Initialized sites retain normal Hugo configuration and content. Once dependencies are available, ordinary Hugo can build them without the CLI. Theme and CLI version numbers serve different purposes.

It serves three audiences: new maintainers establishing a working baseline, existing maintainers diagnosing and upgrading sites, and CI or automation programs consuming stable JSON results and exit codes.

The six implemented commands

Command Problem it addresses Current behavior and boundary
oink doctor Is the environment ready, and what does this site actually use? Inspects Hugo Extended/version, required Go/Git, effective configuration, the declared theme version and resolved source, workspaces, replacements, vendor, languages, and outputs. Read-only diagnosis; no site build.
oink check Does the rendered site contain detectable problems? Copies inputs, isolates output and caches, builds with --panicOnWarning, then checks actual local links, anchors, resources, and supported machine outputs.
oink init <directory> How can I get a usable, reproducible starting point? Creates a site from an embedded, fixed Starter snapshot with its license and provenance. Supports en, en,zh, and all (EN/ZH/FR); validates before creating files and refuses nonempty targets.
oink upgrade --to <tag> Will an upgrade work, and what will it change? Handles one selected site, validates a candidate, and reports a plan. Preview is the default; only explicit --write applies selected module-file changes.
oink dev How do I start everyday local preview? Transparently runs hugo server, forwards arguments after --, and forwards process signals.
oink build How do I run a strict production build? Transparently runs Hugo, defaults to production, and adds --panicOnWarning. It does not run the additional reference checks provided by check.

doctor inspects readiness; check also builds and validates artifacts. build produces the site’s normal publishing output, while check validates in an isolated copy. dev and build can write normal Hugo output and caches; read-only diagnosis and upgrade preview preserve site sources.

Checks follow rendered output

check asks Hugo to enumerate the output formats and URLs declared by each page in each language, then verifies the generated files. It handles multiple languages, root URLs and subpaths, URL encoding, and external-link boundaries. It does not derive a second routing system from Markdown filenames. Page-level output overrides, statically mounted unlisted pages, and intentional link-only pages follow Hugo’s actual semantics.

Supported machine outputs include NAVJSON v1 navigation trees, BookManifest v1 book manifests, offline search indexes, LLMS indexes, and LLMSFULL content bundles. Disabled outputs are optional. A valid artifact in one language cannot hide a missing required artifact in another. An unknown required contract is reported as incomplete coverage.

check --release verifies a build intended to use a public theme version. It disables both Go and Hugo workspaces and Hugo replacements in the isolated copy. A conflicting theme go.mod replace is reported, never silently removed. Ordinary check can validate a selected vendor build; --release explicitly reports that verification of its public-source byte identity is incomplete.

Upgrades validate the candidate first

An upgrade plan identifies the target version, selected files, and before/after state. --expect-plan can bind a write to a reviewed plan; the command also checks whether target files changed after planning. Dirty target module files are protected, unrelated dependencies and edits survive, and write failures provide backup and recovery evidence. Recovery must preserve concurrent user edits instead of overwriting them to undo the CLI’s own work.

Current upgrades handle go.mod and go.sum. They do not refresh _vendor or rewrite arbitrary content or configuration. Vendor refresh requires a separate, explicit, reviewable workflow. The CLI does not commit, push, or deploy.

Automation and offline use

Every command is non-interactive. With --json, stdout contains exactly one oink.result/v1 result; tool logs go to stderr. Results include rule IDs, severity, known locations, explanations, actions, coverage, and raw Hugo evidence. A source line number is never invented when the available evidence cannot establish it.

Exit Meaning Interpretation for automation
0 Required work completed without blocking findings. This request passed; still inspect coverage that was not checked.
1 Completed checks found policy violations. Fix the reported inputs and check again.
2 Required work did not complete. Investigate tools, builds, I/O, caches, or unsupported inputs; this is not a passing check.

The default process policy is offline. Only explicit --network allows network use for that invocation. The CLI does not download Go toolchains, install system packages, change global configuration, or add telemetry. Supported workflows can run offline after module preparation; a missing cache entry fails explicitly.

Isolated commands use disposable caches. A successful init --network does not establish a persistent cache for subsequent commands. Only prepared module download artifacts are reused; Hugo’s global remote-resource cache is not. Required remote content must be materialized locally or fetched during an invocation that explicitly permits network access.

A complete working path

After installing locally and installing the Go, Git, and Hugo Extended versions required by Starter, use the following sequence. The first command is an explicit dependency-preparation step that may use the network:

GOWORK=off GOTOOLCHAIN=local go mod download github.com/pgsty/[email protected]
export GOMODCACHE="$(go env GOMODCACHE)"

oink init my-docs --languages en,zh
oink doctor --site my-docs
oink dev --site my-docs -- --bind 127.0.0.1 --port 1313

Edit the site title, baseURL, and content during preview. After stopping the preview process, run:

oink check --site my-docs --release --json > check.json 2> check.log
oink build --site my-docs -- --minify

For an existing site’s upgrade, preview first, then review and explicitly apply the plan using the upgrade guide. OINK v1.1.0 here is the tested initialization baseline, not a claim that it is always the latest theme version.

Verified scope and current limits

First-stage acceptance covers Go tests, vet, race checks, ordinary Hugo builds for all three Starter profiles at root URLs and subpaths, and three real consumer repositories: the OINK documentation site, the PIG site, and repository documentation. It also includes initialization and checking with OS-level network denial, real upgrade write protection, thin-wrapper process checks, and archive reproduction.

The exercised runtime platform is macOS arm64, with Go 1.27.1 and Hugo Extended 0.166.0. Darwin amd64 and Linux amd64/arm64 were cross-compiled but have not completed runtime qualification on those systems. Windows is outside the first-stage support scope. Source installation is available; public downloads, tag-based installation, and Homebrew distribution are not delivered yet.

Isolated checks currently support materialized, single-host sites. Linked Git worktree metadata, mounted symlinks, mounts outside the isolated inputs, custom configuration directories, dynamic content adapters, multihost language output, and render segments that suppress verification probes have explicit boundaries in the input scope table. Unsupported required inputs cannot receive a complete passing result.

Static checking does not certify browser interaction, accessibility, external URL availability, hosting redirects, translation completeness, or content semantics. Browser and deployment acceptance remain separate workflows.

Improvements to prioritize next

First reduce friction in the six existing commands. These are suggested capabilities based on current limitations, not implemented features. New flags and contracts still belong in the formal proposal process.

Priority area Possible additions Evidence of completion
Installation and platform support Exercise complete workflows on target macOS/Linux systems, publish checksummed archives, and provide reproducible tag-based or Homebrew installation. New users can install, initialize, preview, and check using public instructions; supported platforms have execution evidence.
More actionable diagnosis Group findings by tools, dependencies, configuration, and artifacts; add rule explanations and repair examples; map to source only when reliable; evaluate CI annotations or SARIF export. Users can identify the input to change, CI retains raw evidence, and false positives can be reviewed against real samples.
Explicit dependency preparation Offer deliberate cache preparation and missing-input reports, distinguish modules from remote resources, and record exact versions, sources, and network requirements. One preparation step supports repeated offline runs, with precise explanations when an input is missing.
More complete upgrade maintenance Evaluate separate vendor candidate refresh and byte comparison, persistable review plans, and clearer recovery instructions. Users can review the complete diff while vendor content, unrelated dependencies, and concurrent edits remain protected.
Easier initialization and authoring Accept declarative site title, URL, and supported language-profile inputs; add a small set of official document, article, and Book page templates. Fewer manual placeholder edits; output remains ordinary Markdown, data, and Hugo configuration.
Broader real-project coverage Qualify common structures such as linked worktrees first; extend to multihost, external mounts, and dynamic content when needed; optimize large-site checks from measurements. Every added input profile has regression evidence for source preservation and explainable failures.

These directions should not all start at once. Use independent installation and maintenance sessions to identify recurring obstacles, then choose one measurable improvement at a time. Future ignore rules or lint baselines must not hide Hugo build failures or missing required coverage.

Later product capabilities

The following directions are discussed in the CLI roadmap and remain proposals. They preserve the boundary that generated site sources are ordinary files and rendering does not depend on the CLI.

Capability The CLI’s possible responsibility Prerequisites and limits
Bounded Docsy migration Produce an assessment classifying inputs as compatible, convertible, requiring review, or unsupported; later convert into a new directory and compare old/new routes. Start with one documented profile from real sites, not arbitrary Docsy, MDX, or React conversion; preserve original files and literal code examples.
Documentation version lifecycle Prepare version snapshots, maintain a small version manifest, and validate page correspondence and archive status. The theme owns reader presentation; the CLI generates reviewable configuration. Missing pages must not be presented as equivalents, and versions remain independently buildable.
Static OpenAPI reference Generate operation, parameter, request/response, and schema Markdown/data from local specifications for the existing Hugo output pipeline. Define the specification subset, generate deterministically, and protect manual edits. Prepare remote references explicitly; request execution, credentials, and SDK platforms are separate work.
Agent and editor integration Evaluate editor entry points or MCP over stable JSON results so other tools can reuse the same diagnostics and upgrade plans. Establish repeated use of the core commands first. MCP, Studio, graphs, and hosted services require independent demand and maintenance capacity.

The recommended sequence is a publicly usable maintenance tool, followed by assessment for one migration path. For the next content capability, prefer documentation version lifecycle unless real API users demonstrate stronger repeated demand for static OpenAPI. Choose one foundation per stage to avoid maintaining several models before users have validated them.

Further reading

5 - Use the OINK CLI

Build the optional Go executable locally, initialize a pinned Starter, inspect existing sites, and validate a single-site upgrade before writing.

oink is an optional Go CLI for Hugo sites. The local 0.1.0-dev candidate focuses on diagnosis, real output checks, initialization, builds, theme upgrades, and guarded maintenance plans. Hugo remains the renderer. Sites continue to work with ordinary Hugo.

Local implementation

This guide describes the reduced command surface on 2026-10-04. The CLI has no established public release or distribution. Earlier R1–R8 acceptance belongs to its historical source and binaries. The CLI contract defines current behavior. Studio, general editing, context, snippets, editor setup, and CI generation are retired.

The current cached-module move integration failed once and passed a targeted rerun; the intermittent failure remains open. For the recorded scope, see the verification limits.

Build and install locally

From an available oink-cli source checkout, use Go 1.26 or newer and Make:

make deps                    # explicit network preparation of Go dependencies
make build                   # local toolchain, offline build into bin/oink
./bin/oink --version
./bin/oink --help
make install                 # defaults to $HOME/.local/bin
export PATH="$HOME/.local/bin:$PATH"

The export affects this shell only. The CLI does not install system tools or change a shell profile. make install PREFIX=/your/prefix chooses another prefix; BINDIR=/your/bin selects the exact directory. A source build needs the dependencies in go.sum; make deps explicitly prepares them when network access is available. The build and install targets then use the local toolchain without downloading dependencies or another Go compiler.

The dated runtime acceptance record exercised its identified historical candidate on macOS arm64, native Linux arm64 and Linux amd64 emulated through QEMU TCG, with Go 1.27.1, Hugo Extended 0.166.0 and the public OINK v1.1.0 module. The Hugo version gate accepts Extended 0.160.1 or newer; this is not a claim that every accepted version has been tested. The embedded Starter documents Hugo Extended 0.165.0 or newer and requires Go 1.27. Those Linux tests ran as nonroot users on ext4 with provisioned offline dependencies; required filesystem/signal and selected actual-Hugo cases execute. Optional tools absent from a guest stay explicit skips; they have separate host protocol evidence. Two fresh builds reproduced all five archives, and the three declared runtime archives were extracted and executed outside a checkout without Node. Darwin amd64 is an experimental archive and remains unverified after actual Bad CPU type; cross-compilation does not prove runtime support. Windows is outside the declared scope. These results remain bound to the recorded source and archives; they do not qualify the later reduced CLI or a newly built executable automatically.

make release VERSION=0.1.0-dev DIST=dist prepares four binary archives, a source archive, and SHA256SUMS in a new or empty directory. It does not publish them. See the archive acceptance and reproduction steps for the exercised platform and reproducibility limits.

Create a site from the fixed Starter

Prepare the public theme once if it is not already cached. This explicit provisioning command may use the network:

GOWORK=off GOTOOLCHAIN=local go mod download github.com/pgsty/[email protected]
export GOMODCACHE="$(go env GOMODCACHE)"
oink init my-docs --profile docs --languages en,zh

init accepts a new directory or an existing empty directory. Its parent must exist. It refuses existing files, including dotfiles, and refuses a symbolic link as the target. It validates a temporary candidate before creating any target file and detects target changes during that operation.

Choose --profile project (the default), docs, blog, or book. project keeps the complete previous Starter projection. The other choices retain their archived content section and adapt the existing localized site title, home cards/actions and navigation to that section. Selected content and shared assets/examples/workflows/license retain their archived bytes; generated configuration and homepage YAML are the only serialized profile projections. The archived workflow examples remain unchanged and are not the checksum-bound CI plans from ci init.

Language choices remain en (default), en,zh, and all (English, Chinese and French), independently of the selected profile. Every composition uses the same MIT-licensed Starter commit 137843b25bacd76ddd1f7ce71330bf2e3155b954 embedded in the executable, pins OINK v1.1.0 with recorded Go checksums and disables enableGitInfo before the first Git commit. No runtime template fetch, Git initialization or commit occurs. Unknown profiles fail before writing; unavailable or failed required Hugo validation leaves the new/empty target unchanged.

Edit the title and baseURL in my-docs/hugo.yaml, then edit the home data and sample content using the Starter tutorial. After creating Git history yourself, you can enable enableGitInfo if wanted. Ordinary Hugo can build the generated site without the CLI:

cd my-docs
GOWORK=off HUGO_MODULE_WORKSPACE=off GOPROXY=off HUGO_MODULE_PROXY=off \
  GOTOOLCHAIN=local hugo --environment production --panicOnWarning
cd ..

This uses the GOMODCACHE exported above and provisioned dependencies. All 12 profile/language combinations passed ordinary warning-strict Hugo at both root and /manual/ URLs (24 builds). Rendered local references were checked, and complete source bytes/modes/file inventories compared equal before/after. Public init/check tests separately cover all four profiles in English and bilingual configurations, default-project byte/mode parity and failure paths.

Create ordinary content

Start from the initialized bilingual site above. Preview a new bundle and its Chinese draft, inspect the diff and candidate result, then apply the saved plan:

oink new content/docs/guide --site ./my-docs --title "Getting started" --translations zh --kind docs --plan new-guide.json
oink plans apply new-guide.json --site ./my-docs

--language defaults to the effective default language; --kind defaults to page and also accepts docs, blog, or book. The site-relative bundle path must map unambiguously through actual content mounts and their language-site matrix. Language directories retain distinct physical indexes; shared filenames use the actual language relationships. Existing bundles or same-page sibling files are preserved. The primary file is an ordinary page; selected translation files are drafts with the supplied title as placeholder. They remain unreviewed until explicit human review. Each proposed file must be recognized as one actual site-owned Hugo page with actual rendered outputs; link-only/no-output, ignored or build-never new files cannot pass merely because existing content builds cleanly. Saving a plan does not write those site files. Apply rechecks the bound fresh directory and source state and preserves later editor attachments on failure.

Configure editor hints and snippets with the ordinary site editor. The CLI no longer generates these settings.

Inspect pages and committed impact

Use an actual page ID, Hugo Path, permalink or captured source path. The language:path ID removes multilingual selector ambiguity:

oink inspect 'en:/docs/old' --site my-docs --offline --json
oink impact --since HEAD --site my-docs --offline --json
oink check links --since HEAD --site my-docs --offline --json

Choose IDs from the actual captured page facts; these example pages must exist in your site. inspect exposes observed references, actual outputs, translation peers and physical bundle inputs. impact renders the selected committed Git tree and the current site, including old deleted identities and unchanged inbound pages. Changed global configuration/templates/data and uncertain ownership expand scope. An observed alias output with unproven page ownership also causes full scope, without guessing an owner from front matter.

check --since currently runs the full current check. Read data.check_scope: full separately from causal data.impact.full_scope. Completed inspect/impact queries return 0 while quality findings remain in data.current_check; full checking retains policy findings 1. Missing or unrenderable history is required incomplete 2: known current facts remain visible, while old identities and changes stay unknown. Current external local dependencies are never borrowed as historical bytes. A committed site-owned theme is supported; symlink/submodule and required unsupported or incomplete history states are explicit limits.

Preview and apply a content move

oink move content/docs/old content/docs/new --site my-docs --offline \
  --plan /tmp/oink-move-plan.json --json
oink plans apply /tmp/oink-move-plan.json --site my-docs --offline --json

Use clean physical site-relative file/bundle paths and save the new plan outside the selected site. The preview shows original checks, a provisional route probe, final validation, translation/attachment mappings, byte/full-mode diffs, actual old/new routes, alias advice and manual references. A provisional probe may report stale-link findings 1; only the final candidate can validate the plan. Raw HTML, shortcode output, transformed or ambiguous destinations are not rewritten. Repeated ordinary Markdown destinations can also stay manual when exact source/output occurrence ownership is unproven, including aggregate/print output. Their final broken targets return 1 and no plan is saved. Review manual source locations and actual output pointers in your editor, then create a fresh preview. A moved attachment needs a proven new published URL, not merely a new physical path. Paired equal-byte processed image outputs can be proven while absolute original-resource URLs remain manual; those unproven URLs are not constructed automatically. Aliases are advice for review, not automatically serialized front matter.

Explicit saved apply captures and regenerates the actual proof before selected writes. Complete source hashes/modes/inventory, external inputs and fresh target directories remain guarded. Existing targets, later source/attachment/ configuration edits or mode changes return 2 without overwriting them. The move preserves original modes and binary bytes, unrelated files and the Git index; it does not commit. The resulting ordinary Hugo inputs remain buildable with Hugo independently of the CLI. Inspect the named recovery directory if an apply reports a failure during writes.

Diagnose and validate an existing site

Run these commands from any directory and select exactly one site:

oink doctor --site ./my-docs
oink check --site ./my-docs
oink check links --site ./my-docs --format json
oink check --site ./my-docs --base-url https://example.org/manual/
oink check --site ./my-docs --release --keep-work

doctor reports the real Hugo executable and version, required tools, the declared theme pin, effective Hugo configuration, module graph and mounts, workspaces, replacements, vendor presence, languages, and enabled outputs. It does not build the site. Keep the raw subprocess evidence alongside the structured findings when investigating a Hugo error.

check copies the inputs into a disposable directory, isolates build outputs and caches, invokes Hugo with --panicOnWarning, and checks supported local links, anchors, resources, and machine-output references against the rendered files. Hugo itself enumerates each page’s enabled output formats and URLs for each language, including front matter overrides and statically authored pages excluded from ordinary page lists. A temporary verification output is added only in the disposable copy and removed before artifact checks. The CLI does not infer routes from Markdown filenames. Disabled machine outputs are not errors. Coverage entries identify checks that were completed, omitted, unsupported, or incomplete. Browser interactions, accessibility, external URL availability, server redirects, and deployment are outside this static check.

JSON data.pages supplies Hugo’s page identities, actual routes, aliases, languages, translations, publication settings, known source provenance and outputs. data.references supplies observed rendered references and checked anchor state. Generated pages with no proven file retain an explicit unknown source. These are production-view facts; their presence does not certify undeclared translation coverage or invent a Markdown source line. Human output summarizes page/reference counts; use JSON for the full arrays.

Both commands preserve source files. --keep-work retains the disposable directory and reports its path for inspection; otherwise it is removed. Use --config FILE, --environment NAME, and --hugo PATH when the site needs a specific configuration, environment, or Hugo executable. Configuration files must be inside the selected site. For inspection, --environment takes precedence over HUGO_ENVIRONMENT; otherwise the environment is production. If Hugo adds or changes go.mod or go.sum in the temporary copy, the CLI reports that required dependency preparation remains unreviewed; it does not apply those changes to the source or silently call the original inputs ready.

--release disables both Go and Hugo workspaces and disables environment and Hugo-configuration replacements in the disposable copy. It preserves go.mod replacements. A local OINK replacement must be reviewed explicitly before claiming a public-pin check; the CLI does not silently delete it. Vendor evidence is also separate: a public requirement in go.mod does not establish which bytes are in _vendor.

The initial isolated-check scope has these limits:

Input shape Current behavior
Normal checkout with its own .git directory, or materialized files without Git Supported within the other documented boundaries
Linked Git worktree with a .git file Rejected; use a separate materialized copy with its own Git metadata if Git history is needed
Mounted symlinks or mounts still pointing outside the isolated snapshot Rejected; materialize those inputs inside the selected site or supported local dependency
Unmounted auxiliary symlinks Omitted from the snapshot; this does not validate their contents
Mounts using excluded public, resources, node_modules, or tmp trees Rejected when they are required source inputs; keep authored/generated source in a dedicated source directory
Custom HUGO_CONFIGDIR outside the supported config location Rejected; use the site’s config tree or an explicit in-site --config file
Hugo content adapters (_content.gotmpl) Complete enabled-output enumeration is unsupported; check and candidate validation return incomplete work
Multihost language configuration Unsupported for full output validation; returns incomplete work rather than treating each host as one output tree

Likewise, disabling page rendering or selecting render segments that omit an enabled language’s verification output cannot produce a successful full check. These are coverage limits, not instructions to delete a worktree, replacement, symlink, or authored content. doctor can still inspect supported configuration without claiming a completed output build.

Select checks and record project policy

Create a regular oink.yaml at the site’s root when the project needs explicit checking policy. It uses schema_version: oink.policy/v1 in one YAML document. Keep languages, menus, URLs and theme versions in existing Hugo/module inputs. Unknown policy keys/groups, invalid reviews and required disabled groups return 2.

This example retains required links and demonstrates a reviewed finding and a separately deployed URL scope. Replace illustrative paths and review metadata with actual project decisions:

schema_version: oink.policy/v1
checks:
  links: {enabled: true, required: true}
rules:
  ANCHOR_MISSING: warning
exclusions:
  - rule_id: REFERENCE_MISSING
    file: docs/legacy/index.html
    reason: Reviewed legacy reference awaiting removal
    reviewed_by: site-maintainer
    reviewed_at: "2026-10-03T00:00:00Z"
external_scopes:
  - url: https://example.org/status/
    reason: Separately deployed status application
    reviewed_by: site-maintainer
    reviewed_at: "2026-10-03T00:00:00Z"

Rules use exact diagnostic IDs and error, warning or info. Exclusion globs use clean relative paths; recursive ** and escape paths are unsupported. Excluded findings stay visible with disposition: "excluded" and review metadata. Checks never create review records as a side effect. Required build/input/tool failures and unsupported coverage remain 2 despite policy.

For a site at https://example.org/manual/, same-origin HTML /status/ references normally fail as outside the published base path. The reviewed scope declares that separate application; availability remains untested. Complete path-segment matching excludes /status-other/. A scope cannot hide missing targets inside /manual/ or required local machine-output references.

Without a policy, check enables required links, translations and style. check links, check translations and check style each select one required engine; unselected groups report optional not_checked. Explicit policy groups can disable optional checks. Every check retains its strict Hugo prerequisites.

Declare translation coverage

Start with Hugo’s actual page identities in check --json (data.pages), then declare the source Page.Path scope and required enabled languages. Paths are Hugo source identities, independent of slug, URL, aliases or language prefixes. Extend the same single oink.yaml object; this complete example also declares protected prose and the baseline path:

schema_version: oink.policy/v1
checks:
  links: {enabled: true, required: true}
  translations: {enabled: true, required: true}
  style: {enabled: true, required: true}
translations:
  scopes:
    - path: /docs/handbook
      source_language: en
      required_languages: [zh]
      mode: localized
      drafts: include
      constraints:
        explicit_ids: true
        ids: [setup]
        placeholders: ["${SERVICE_NAME}"]
        code_labels: [bash]
        required_fields: [title]
        equal_fields: [weight]
style:
  protected:
    - file: content/docs/handbook.md
      literal: "${SERVICE_NAME}"
      count: 1
baseline: .oink/baseline.json

Use real page paths, filenames, IDs and protected literals from your project. mode defaults to localized, drafts to include. Strict mode with explicit_ids: true requires complete recognized explicit-ID correspondence; localized mode protects the selected ids. Placeholder counts, named fenced code, required dotted fields and equal dotted values are separate opt-in constraints. Other prose, heading counts and code may differ.

drafts: ignore skips draft sources and treats draft targets as unavailable; require-published requires the source and required targets in the production view. Hugo-disabled known languages are optional not_applicable; unknown languages fail policy loading. Without scopes, existing default-language pairs and duplicate relationships are inspected, but universal localization is not required. JSON data.translations distinguishes missing, draft and hash-review states. An explicit nonpublishable analysis includes draft/future/expired pages without replacing production output or publishing them.

Inspect source rules and provenance

oink check style --site ./my-docs --json

Generic rules inspect recognized explicit IDs and declared protected prose. The parser follows effective Hugo markup.goldmark.parser.attribute.title and .block, plus markup.goldmark.extensions.passthrough.enable and its configured .delimiters. These settings remain in Hugo configuration. It keeps original UTF-8/CRLF/BOM offsets and accepts unknown valid YAML/TOML/JSON front matter. Code, shortcode bodies, raw HTML and math contents are excluded from prose evidence; an attribute after a fence is not treated as a supported code attribute. Unsupported required source syntax or absent declared protected inputs returns 2.

The small native OINK v1.1.0 catalog reports advisory code/table conflicts, deprecated attribution fields and attributes the published theme drops. data.native_rule_provenance records immutable source/license hashes. The catalog runs only for a SHA-verified actual public module-cache mount. Other versions, replacements, vendor copies and unknown identities report optional native-theme-rules: not_checked; generic rules still run. Review this coverage before treating the check as complete for a particular component.

Review translations and apply metadata plans

Use the exact Hugo IDs or unambiguous captured source filenames from the report:

oink translations status --site ./my-docs --json
oink translations diff 'en:/docs/handbook' --site ./my-docs
oink translations review 'en:/docs/handbook' 'zh:/docs/handbook' \
  --site ./my-docs --reviewed-by site-maintainer \
  --reason 'Reviewed source and translation together' --plan /tmp/oink-review.json
oink plans apply /tmp/oink-review.json --site ./my-docs

Review previews .oink/translations.json (oink.translations/v1) after candidate validation. It never writes the site during preview. The record binds full source/translation byte SHA-256 values and explicit reviewer/reason/time. --reviewed-at RFC3339 is optional and defaults to current UTC. No record means unknown; current, source_changed, translation_changed and both_changed describe hashes since review, without judging translation accuracy. Modification time is not review evidence. diff shows captured source text for comparison.

To acknowledge completed, reviewed existing findings while keeping them visible:

oink baseline capture --site ./my-docs --reviewed-by site-maintainer \
  --reason 'Reviewed existing findings for this maintenance baseline' \
  --plan /tmp/oink-baseline.json
oink plans apply /tmp/oink-baseline.json --site ./my-docs

The default baseline is .oink/baseline.json (oink.baseline/v1); baseline in policy can select another clean relative file. Acknowledged exact rule, normalized location/pointer and condition remain visible with disposition: "baseline" and review metadata. Severity changes do not invalidate that fingerprint; new conditions still block. Required incomplete work cannot be captured or hidden by a baseline.

Both preview commands require reviewer and reason. --plan FILE creates a new oink.plan/v1 file without overwriting; omitting it prints the validated plan only. Review the readable diff, site, file list and base byte/mode guards before plans apply. That command validates a fresh isolated candidate and refuses stale guards, escapes, .git, symlinks and nonregular files. It writes only the plan’s selected files; these commands do not use --write. Failed partial writes restore owned unchanged files and preserve later editor bytes, modes or deletions. The reported recovery directory retains original/concurrent evidence.

Preview and apply one theme upgrade

Choose an explicit release tag. The following command validates a candidate and prints the proposed module-file changes without applying them:

oink upgrade --site ./my-docs --to v1.1.0 --json > upgrade-plan.json

The normal human output shows a unified module diff, mode changes and bounded route/alias/capability changes. In JSON, review data.plan_id, data.changes, data.comparison, baseline/candidate check summaries and raw evidence. A missing old URL/output blocks the update unless an observed redirect at its old output file proves preservation; unknown custom alias identity stays incomplete. This is not universal theme or browser compatibility. To apply the freshly revalidated reviewed plan, use its recorded ID:

oink upgrade --site ./my-docs --to v1.1.0 --write \
  --expect-plan 'COPY_PLAN_ID_FROM_PREVIEW'

The CLI changes only the selected go.mod and go.sum bytes after candidate validation. It preserves unrelated requirements, replacement directives, comments, and unrelated uncommitted work. --write refuses uncommitted changes to either selected file, and detects changes after planning. Recovery evidence identifies backups and any rollback that could not safely restore a concurrently changed file.

The ID binds current copied source bytes/modes/inventory and actual comparison, not just module-file text. A later source/workspace/dependency edit requires a new preview; only selected module files are applied. Unknown resolved pins or changed/unknown renderer/environment cannot pass. Comparison supports a single HTTP(S) base origin/path; multihost inputs stay incomplete. Emitted byte hashes also bind the ID, so nondeterministic templates can require a refreshed preview. No configuration migration is performed automatically; review unsupported changes manually.

An OINK go.mod replace is a blocking condition for this public-pin workflow. A site containing _vendor is also refused: this release does not refresh vendor content. Prepare a separate reviewed copy, update the intended pin and run hugo mod vendor explicitly, then review and validate the entire vendor change. A go.mod bump alone is never reported as a vendor upgrade.

Preview and build through Hugo

oink dev --site ./my-docs -- --port 1315 --bind 127.0.0.1
oink build --site ./my-docs -- --minify

Arguments after -- go directly to Hugo. The CLI shows the effective command and forwards process cancellation. dev runs hugo server; build selects the production environment by default and adds --panicOnWarning. These are direct Hugo operations by default and may create the site’s normal output and cache files. They do not run the reference checks performed by oink check.

Check and export one build

Choose a real publication URL in OINK_PUBLIC_BASE_URL, prepare the site’s exact dependencies, then use a fresh output directory and separate new manifest:

OINK_ARTIFACT_DIR=$(mktemp -d)
oink build --check --site ./my-docs --release \
  --base-url "$OINK_PUBLIC_BASE_URL" \
  --destination "$OINK_ARTIFACT_DIR/public" \
  --manifest "$OINK_ARTIFACT_DIR/build-manifest.json" --marker

Add --network to this operation only if its dependencies or required remote resources need downloading. An example/local publication address is a release error; ordinary diagnosis reports a warning. --release also checks actual public theme resolution independently of local Git history or declared pins.

Hugo renders one isolated production output. The CLI checks, seals and exports that same tree without rebuilding or changing site sources. Required incomplete coverage returns 2; blocking findings return 1. Neither result creates a verified export. An explicit scoped translation policy needing publication-excluded Hugo identities returns 2; run standalone check/translations for the full nonpublishable maintenance view, or deliberately select a production policy. The command does not infer identities from filenames.

The destination must be new or empty and its parent must exist. The manifest must be a new file outside the public tree. Existing entries are preserved; failed partial export remains explicitly unverified. Optional --marker adds only the artifact identity at .well-known/oink-build.json; without the flag, no marker is added. The local oink.artifact/v1 manifest records original input identity, known Git state, effective settings/theme/tools, required coverage, Hugo routes and exact file digests/modes. It excludes absolute local paths and logs and is saved with mode 0600. Keep it outside the upload.

Managed builds allow only --minify, --gc, --ignoreCache and --noTimes after --, with optional boolean =true/=false. The ordinary build example above still accepts transparent Hugo arguments.

Verify artifacts and a deployed site

Immediately before uploading, check the exported directory offline:

oink artifacts verify --artifact "$OINK_ARTIFACT_DIR/public" \
  --manifest "$OINK_ARTIFACT_DIR/build-manifest.json"

This compares the exact files, bytes and full modes. Missing, additional or modified files invalidate the previous identity. Upload this directory without rebuilding; preserve its hidden .well-known marker when enabled.

After a separately authorized deployment, check its public URL explicitly:

oink verify --site "$OINK_PUBLIC_BASE_URL" \
  --manifest "$OINK_ARTIFACT_DIR/build-manifest.json" --network

Verification reads every declared file and distinct actual Hugo route, including language/subpath URLs, and compares bounded decoded response digests plus recorded HTML canonical/language identities and the enabled marker. HTTP cannot inspect local file modes. Wrong content, a soft-404 or a different captured identity returns 1. Timeouts, authentication/rate-limit failures, server unavailability and a missing required marker return 2; redirects outside the selected origin/path are blocked. No credentials are discovered or sent. A build’s --network permission does not authorize this later request or any upload.

Retired CI generation

ci init is removed. Keep CI configuration in the site or Starter. Local CLI validation does not execute hosted CI or deploy a site. Previously saved CI plans are rejected by plans apply.

Check explicitly registered sites

R6 supported local scope accepted

Workspace and adapter examples passed owning/runtime, actual protocol, four-consumer parity/preservation and canonical source/render gates. A07/A15 supported scope is accepted locally in the R6 record. They do not describe a published CLI release or a completed platform refresh.

Create a separate registry such as oink.workspace.yaml beside your selected projects. Its only site fields are name and directory; keep Hugo settings in each site and checking policy in that site’s oink.yaml.

schema_version: oink.workspace/v1
sites:
  - name: docs
    directory: ../docs-site
  - name: blog
    directory: ../blog-site
oink workspace list --workspace ./oink.workspace.yaml
oink workspace check --workspace ./oink.workspace.yaml --offline --json
oink workspace check links --workspace ./oink.workspace.yaml \
  --sites docs,blog --offline --json
oink check style --workspace ./oink.workspace.yaml --site docs --offline --json
Command Selection
workspace list --workspace FILE List explicit entries without running Hugo
workspace check [GROUP] --workspace FILE [--sites NAME,NAME] Check all or an exact subset, in registry order
check ... --workspace FILE --site NAME Run the normal single-site check for one registered name
plans apply FILE --workspace FILE --site NAME Revalidate and apply only a plan bound to that named canonical site

Names are case sensitive ASCII identifiers matching [A-Za-z][A-Za-z0-9_-]{0,63}. A regular nonsymlink registry has one strict YAML document, 1–64 nonoverlapping sites and a 256 KiB limit. Directories are literal relative paths from its actual parent, or absolute paths; no environment/glob expansion or sibling discovery occurs. Canonical aliases identify the same site and cannot register it twice. Missing directories remain listed; their check returns 2, while the remaining explicit sites are still checked. The aggregate returns 2 before 1 before 0, preserving full per-site findings and coverage. Omitting --sites means all registered sites; an explicit list rejects blanks, duplicates and unknown names and keeps registry order.

A direct command needs --site NAME; there is no default registry site. init, artifacts and verify do not accept registry selection. Save a reviewed plan outside its site, then explicitly apply it to the same name:

oink translations review en:/docs/manual zh:/docs/manual \
  --workspace ./oink.workspace.yaml --site docs \
  --reviewed-by 'Maintainer' --reason 'Reviewed terminology and examples' \
  --reviewed-at 2026-10-03T00:00:00Z --plan ./review.plan.json --offline
oink plans apply ./review.plan.json \
  --workspace ./oink.workspace.yaml --site docs --offline

Use actual page identities returned by your site’s checks for the review selectors. Selecting blog for a plan bound to docs returns 2 before a source write. Preview, validation, freshness and byte/mode safeguards are the same as direct single-site use; no other registered or neighboring site is updated automatically.

Configure already provisioned optional tools

The CLI does not install markdownlint, Vale or lychee. After provisioning tools separately, add explicit entries to the selected site’s oink.yaml. The current protocols are markdownlint-cli 0.49.1, Vale 3.24.0 and lychee 0.24.2; other reported versions remain unsupported until qualified.

schema_version: oink.policy/v1
tools:
  markdownlint:
    required: false
    config: .markdownlint.yaml
    timeout_seconds: 60
  vale:
    required: false
    config: .vale.ini
    timeout_seconds: 60
  lychee:
    required: false
    config: lychee.toml
    timeout_seconds: 60

enabled defaults to true, required to false, and command to the kind’s name. You can select one provisioned executable by name or absolute path; commands are not shell snippets. Configuration paths must be clean relative paths inside the captured site. Process time defaults to 60 seconds, with bounded nondefault values 1–300. Unavailable optional tools show omissions; required unavailability or unsupported protocol returns 2. A problem baseline or lower rule severity cannot turn required incompletion into success.

Markdownlint and Vale belong to style; lychee belongs to links. Select the group whose tools you intend to run:

oink check style --workspace ./oink.workspace.yaml --site docs --offline --json
oink check links --workspace ./oink.workspace.yaml --site docs --network --json

The second command explicitly permits actual external HTTP requests. Without --network, lychee is not invoked: optional coverage is not_checked, required coverage returns 2. A successful native local-links check does not attest external availability. HTTP 401, 403, 408, 425, 429, 5xx, DNS/TLS failures and timeouts are inconclusive, not definite broken links. Other failed 4xx responses are typed findings. External locations remain actual output files and DOM pointers; the CLI does not guess their Markdown line.

For markdownlint, use declarative JSON, YAML or TOML, for example:

default: true
MD013: false

JS/JSONC configs, custom rules and extends are unsupported. The CLI stages a private rule object behind an unpredictable JSON pointer so upstream rc data does not change its effective rules. Vale requires an explicit INI and captured styles. A supported minimal configuration is:

StylesPath = styles
MinAlertLevel = warning

[*.md]
BasedOnStyles = Project

Provide declarative rule files under styles/Project/. Supported rule kinds are existence, substitution, repetition, occurrence, consistency, capitalization and sequence. Actions, scripts, packages, sync, conversion assets and style pipelines require manual review and are not executed by this adapter. Lychee accepts these bounded request settings:

timeout = 10
max_retries = 0
max_concurrency = 8

Allowed ranges are 1–300 seconds, 0–3 retries and 1–32 concurrent requests. The literal cache = false is also accepted; cache = true is refused. Cache and preprocessors are disabled; arbitrary extra tool arguments are not accepted. No adapter fixes or formats source files. Code prose is outside source attribution; Markdown structure and fence/inline code boundaries remain available to markdownlint, while Vale receives the prose-only mask. Private masks preserve proven UTF-8/BOM/CRLF boundaries around front matter, shortcodes, raw HTML, configured math and attributes; findings from excluded/synthetic text remain omissions. Only already captured site-owned Markdown is read for prose tools.

Inspect data.adapters, adapter.KIND coverage and raw evidence before interpreting an exit. Each adapter retains version/executable/configuration hashes and protocol provenance. Caller proxy URLs/credentials and Node preload settings are not forwarded; literal NO_PROXY/no_proxy host-list data may be retained for the qualified runtime. This does not disable every operating-system proxy route or create a network sandbox. Network checks do not verify external fragments, browser behavior or remote content identity.

Retired local Studio

studio is removed from the CLI. Use an ordinary editor and oink dev for a site preview. Read maintenance facts through inspect and structured reports. The dated R7 acceptance remains historical evidence for its identified inputs.

Retired Studio views

Read page and quality facts with inspect, check, and structured reports.

Retired browser targets

The CLI Studio browser test targets are removed with the implementation. Current CLI checks remain Go and actual Hugo tests.

Retired general editing

edit and Studio editing are removed. Edit source with an ordinary editor, then run check. new, move, review records, and baseline plans retain candidate validation and byte/mode guards. Previously saved editing plans are rejected. The dated R8 record remains historical evidence.

Retired edit commands

The edit text|field|snippet|attachment command family is removed. Run new --help or move --help for the retained bounded file workflows.

Retired Studio editing

The CLI does not serve an editor or accept browser Apply requests.

Retained plan review

Retained previews display the complete proposed diff. Save a new plan, then explicitly run plans apply FILE --site DIR. Candidate validation, source and external input guards, and concurrent-edit recovery remain required.

Network and offline operation

Network access is disabled by default; --offline makes that choice explicit. Missing dependencies produce an incomplete result. The CLI does not install Hugo, download a Go toolchain, change global configuration, or enable telemetry. Allow the current operation to use the network only when intended:

oink check --site ./my-docs --network

--network and --offline cannot be combined. Diagnostic and validation operations use disposable caches; a download there does not establish a persistent cache for the next offline run. For repeatable offline work, provision the exact module versions in a normal Go module cache and set GOMODCACHE explicitly as shown above. Include all transitive dependencies needed by your site. Isolated validation reuses provisioned module download artifacts, not Hugo’s global remote-resource (GetRemote) cache. A prewarmed remote-resource cache alone does not make this check work offline. Materialize required remote content as local site resources, or explicitly use --network for that build. Theme assets that already ship as local files do not require such a download.

Text, JSON, YAML, and automation

oink check --site ./my-docs
oink check --site ./my-docs --verbose
oink check --site ./my-docs -J > check.json 2> check.log
oink check --site ./my-docs -Y > check.yaml 2> check.log
oink translations review --help
Option Output
Default Concise colored English text
--json, -J One JSON oink.result/v1 object
--yaml, -Y One YAML document with the same result fields and types
--verbose, -v All findings, coverage details, and tool logs
--no-color Plain English text

Choose one structured format. Nonempty NO_COLOR or TERM=dumb also disables text colors. Structured output adds no terminal colors. Tool logs go to stderr. --format json|yaml and --non-interactive remain hidden compatibility options. Every command is non-interactive.

Default text shows status, counts, up to eight active findings, and explicit coverage omissions. Detailed facts and reviewed findings remain available in structured results. Plan and upgrade previews retain their complete diffs. Cobra owns command dispatch and focused help. CLI messages use short, active English sentences inspired by ASD-STE100; this does not assert certification. User content and external tool evidence retain their original language.

Exit code Meaning
0 Requested work completed without blocking findings
1 Completed checks found a policy problem
2 Required work could not complete, including tool, build, or I/O failure

Always inspect coverage alongside the exit code. An exit code of zero from doctor does not prove a build, and a successful static check does not prove browser behavior or a public deployment. These commands do not commit, push, publish a theme, or deploy a site.