Usage and customizations: where this module differs from Hugo and Hextra

July 23, 2026 · View on GitHub

This file is a reference for content authors and maintainers. It has four parts:

  • Authoring — the shortcodes and render behavior that docs-theme-extras adds on top of, or changes from, stock Hugo and Hextra. For anything not listed, the Hugo and Hextra defaults apply unchanged.
  • Section tab navigation — the opt-in docTabs feature that groups a version's top-level sections into tabs, and how it behaves on desktop versus in the mobile drawer.
  • Logo placement — the navbar / sidebar / footer logo slots, the common arrangements, and the mobile slide-out drawer logo.
  • Maintaining — the Hextra layout files this module shadows, the # ours convention, how to debug override precedence, and the Hextra upgrade workflow.

If you just want the full parameter docs for one shortcode, the source file is the source of truth: each file under layouts/_shortcodes/ opens with a comment block describing its parameters and behavior. This file is the map; the source comments are the detail.


Authoring: shortcodes and render behavior

Upstream references

Bookmark these. When something behaves the way you expect, it is probably stock Hugo or Hextra, and their docs are the right place to look first.

A note on call form: {{% %}} vs {{< >}}

This trips up more people than any single shortcode, so it comes first.

In stock Hugo the two call forms are not interchangeable: percent form ({{% name %}}) sends the inner content back through the markdown renderer, angle-bracket form ({{< name >}}) does not. See the Hugo shortcodes guide for the upstream rule.

Several shortcodes in this module (for example table, github-table, and the reuse/rebase family) deliberately render their inner content through .Page.RenderString so that both call forms produce identical HTML. This is not cosmetic: the rebase pipeline rewrites percent-form shortcodes to angle-bracket form before rendering, so any shortcode that relied on percent-form "splice back into the source" behavior would break on a rebased page. If you are writing a new shortcode here, prefer the RenderString approach for the same reason.

The safest author-facing guidance is: use whichever form the existing content around you uses, and assume the two are equivalent for this module's shortcodes unless a shortcode's own comment says otherwise.

Shortcodes that override a Hextra shortcode

These share a name with a Hextra shortcode, so you may already know them, but their behavior here differs. The "Difference" column is what changed; follow the Hextra link for the baseline behavior.

ShortcodeDifference from HextraHextra reference
calloutDerives the icon from type/context using the Material Icons font instead of Hextra's SVG icon lookup. Adds role="note" for accessibility (not role="alert"). Emits the box on one logical line so a callout inside a list item does not break the list. Has a translation-export mode.callout
detailsDefaults to closed. Hextra defaults to open and uses closed="true" to collapse; here you pass open="true" to expand, and Hextra's closed="true" is a harmless no-op.details
cardEmits the same .section-card markup as the auto-generated section-index cards, so manual cards match generated ones. Title and subtitle are <span> (display:block), not <p>, so the card survives being wrapped by markdownify. Adds a path= parameter that resolves a section-relative path.cards
cardsEmits the .section-cards container that the auto-card grid uses; cols defaults to 3.cards

Rendering behavior this module overrides (markup render hooks)

These are not shortcodes. They change how ordinary markdown renders across every page, by overriding Hugo/Hextra render hooks. You do not call them; they just change the default output.

HookWhat changesReference
Table render hook (render-table.html)Caps per-cell column width only on wide reference tables (a 3+ column header is the signal, for example Key | Type | Default | Description). Two-column narrative tables render uncapped and fill the content width. See .table-capped in docs-theme-extras.css. Use the table shortcode below to override this per table.Hugo table hooks
Link render hook (render-link.html)Simplified so it works inside RenderString context (nested reuse shortcodes), where .PageInner/.RelPermalink would fail. Uses relURL for internal links instead of full page resolution.Hugo link hooks
Blockquote alert hook (render-blockquote-alert.html)Same as Hextra's GitHub-style alert hook (> [!NOTE]), except it also ships two custom alert types that need no consumer config — [!SOLO] (branded, solo icon) and [!SUCCESS] (green check, the GitHub-syntax counterpart to callout type="success") — and additionally accepts any custom types a consumer declares under site.Params.themeExtras.alertTypes. Unknown types still warn and fall back to the default style.Hugo blockquote hooks

Shortcodes unique to this module (no Hugo or Hextra equivalent)

These have no upstream counterpart, so there is nothing to compare against. They are grouped by purpose. See each source file for full parameters.

Content reuse and versioning

ShortcodeWhat it does
reuseIncludes a snippet from the assets tree, applying version- and product-aware shortcode rewrites so the same snippet renders correctly across versions and products. Takes the snippet path as the first positional parameter.
rebaseRenders another version's asset tree into the current page, applying version-aware shortcode rewrites through a multi-stage pipeline.
readfileInlines a local file. Renders it as markdown when markdown="true", otherwise passes it through as raw HTML. Skips SECURITY_SCAN files when Site.Params.noSecurityScan is set. Wraps Hugo's readFile.
githubFetches a remote file by URL and inlines it. .md URLs render through the page's markdown renderer; other file types pass through as-is (wrap them in a code fence).
github-tableFetches a remote markdown file and inlines a single section, selected by heading name, with an optional exclude regex.
versionEmits version-conditional text. Dispatches on the shape of the inner content to stay list-safe and code-fence-safe (see the source comment for the trailing-step, no-markdown, and inline-markdown paths).
version-cardsRenders a card grid mirroring the navbar version dropdown, for a section landing page.
conditional-textIncludes or excludes its inner content based on the page's build condition (for example gme vs gmg), resolved through utils/page-context.
upstream / downstreamContent gating for the oss-vs-enterprise build split. upstream shows content only in the source build; downstream shows it only in the downstream build.
linkAn internal link resolved from a section-relative path= to the current section and version. Has a translation-export mode that preserves the source shortcode form.
link-hextraLike link, but infers the product and version from the current page's permalink when they are not passed in (typical inside a reused snippet).

UI components

ShortcodeWhat it does
tableWraps a markdown table so the author chooses how columns size, overriding the render-hook heuristic above. mode="wrap" (default) fills the body width and wraps; mode="nowrap" sizes to content and scrolls horizontally (good for commands and code); mode="equal" gives equal-width columns. The markdown table inside it must start at column 0.
alertAn alias for callout (it maps context onto type and calls callout), so the two render identically.
checklistRenders an interactive checkbox list from - [ ] markdown lines.
glossRenders an inline glossary term with a hover/focus tooltip, looked up from the consumer's data/glossary.yaml. Optional inner content overrides the displayed text.
details(listed above as a Hextra override)
redirectEmits a client-side redirect stub (script + <noscript> meta-refresh + a visible link) for a page that has moved.
reuse-imageA theme-aware image that renders in both light and dark mode, with a translation-export guard against path doubling.
reuse-image-light / reuse-image-darkSingle-mode image variants, shown only in light or only in dark mode via the .toggle-dark CSS class.
openapiEmbeds a Swagger UI viewer for an OpenAPI spec, and also emits a plain-text summary so the "Copy as Markdown" feature captures the API content that Swagger renders client-side.
renderRenders the changelog widget (release/chronological/compare-versions selector) for a changelog page.

Deprecated

ShortcodeStatus
prismDeprecated legacy stub from docs-theme-lotus. Use a fenced code block ( ```lang {hl_lines=[...]} ) instead. prism emits pre-rendered <pre> HTML, which breaks list continuation when used inside a reused list item. Slated for removal once no consumer references it.

Section tab navigation (docTabs)

Groups a version's top-level sections into named tabs — for example Documentation, API Reference, Changelog — so a large version presents one group of sections at a time instead of one long left nav. It is configured per site, opt-in, and only affects versions that actually populate two or more tabs.

Source: docs-tabs.html (the desktop band plus the shared state the sidebar reads), sidebar.html (the mobile chip row, the per-tab tree panels, and tree scoping), head-end.html (the mobile interaction JS), and the docs-tabs* / sidebar-mobile-tab* rules in docs-theme-extras.css.

Enabling

Declare the tabs, in display order, under params.docTabs. Mark one as the default; if none is marked, the first entry is the default.

[[params.docTabs]]
  name = "Documentation"
  default = true
[[params.docTabs]]
  name = "API Reference"
[[params.docTabs]]
  name = "Changelog"

Tabs render only when a version has two or more non-empty tabs. An empty tab is dropped, and a version that ends up with fewer than two populated tabs shows its full, unscoped left nav (the feature is effectively off for that version). So tabs roll out per version as you tag pages — no per-version config change is needed.

Assigning pages to tabs

Tabs are populated by the version's top-level sections and pages. Assign one to a tab with tab: front matter; anything untagged falls into the default tab.

---
title: API Reference
tab: API Reference   # this top-level section and its descendants live in that tab
---
  • The active tab is the tab of the current page's top-level ancestor (or the page itself, when it sits at the top level).
  • The left-nav tree is scoped to the active tab at depth 0 — only that tab's top-level sections show; levels below the top are unfiltered.
  • Each tab links to its first top-level item's own page — the first entry in that tab's left nav — which is that section's landing page (or the page itself when the first item is a leaf), not a deeper first-leaf page.

Desktop: the tab band

At and above the sidebar breakpoint (xl, 1280px) the tabs render as a band across the top of the content area (.docs-tabs-band). Clicking a tab navigates to that tab's first page.

Mobile: tabs in the slide-out drawer

Below xl the band is hidden and the tabs move into the slide-out sidebar drawer as a chip row, under a Contents heading, directly above the page tree they scope:

  • Underline style — the active tab is a brand-colored underline sitting on the row's hairline (matching the desktop .docs-tab band), so it reads as a different kind of control from the outlined version chips above it.
  • Horizontal scroll — the row scrolls sideways instead of wrapping; / arrows appear only when a tab is off-screen, and tapping a chip centers it.
  • Structure-swap, not navigation — tapping a tab shows that tab's link structure in place (client-side), so you can explore a section's topics before committing to a page; you then tap a page in the tree to navigate. The desktop band still navigates on click — the swap only applies below xl.

Cost: the structure-swap works by pre-rendering every tab's tree into the page as hidden panels (.sidebar-mobile-tree-panel), so each page in a tabbed version carries all of that version's tab trees in its HTML. On a version with large per-tab trees this adds page weight — a deliberate trade for no-navigation tab switching on mobile.


Logo placement

Three optional logo slots, each set independently in a consumer's config. The theme renders whatever each points at — the placements below are convention, not enforced by the theme.

ParamSlotRendered by
params.navbar.logoTop navbar (path, dark, width, height, link)navbar-title.html
params.sidebar.logoDesktop sidebar header, and the mobile slide-out drawersidebar.html
params.footer.logoFooter (path, dark)footer.html

Common arrangements

  • Product mark in the navbar (OSS shape): set navbar.logo to the product lockup and leave sidebar.logo unset. The navbar mark then shows at every breakpoint.
  • Product in navbar, corporate mark in footer (enterprise target): set navbar.logo to the product lockup and footer.logo to the corporate (Solo) mark, with no sidebar.logo.
  • Product in sidebar, corporate mark in navbar (older enterprise): set navbar.logo to the corporate mark and sidebar.logo to the product lockup.

Below the sidebar breakpoint (xl, 1280px) the persistent sidebar becomes the slide-out drawer, and the theme pins a logo to the top of it — where the navbar logo would sit, since the open drawer covers the navbar's left region. It uses sidebar.logo if set, otherwise falls back to navbar.logo, so a site that only configures a navbar logo still gets a drawer mark.

Details worth knowing:

  • The drawer logo is centered and sized by an explicit height (28px), not max-height — the logo SVGs carry only a viewBox (no intrinsic width/height), which would otherwise collapse a max-height image to 0×0.
  • The desktop sidebar header logo (.sidebar-product-logo) is hidden below xl (the drawer logo replaces it), so on a site with sidebar.logo set you never see both at once. It sizes the navbar logo from the width/height params directly, so adjust those to resize it.
  • When sidebar.logo is set, the navbar logo is desktop-only (the sidebar/drawer logo covers mobile); when it is not, the navbar logo shows at all breakpoints.

Nothing here is tied to docTabs — logos and tabs are independent features.


Maintaining the shadows

The section above is about shortcodes. This section is about the partial and layout files this module overrides.

Every file under layouts/ in this module that has a same-named counterpart in imfing/hextra is a shadow: Hugo's project-over-imports precedence makes the module's copy win.

When you bump the Hextra pin in go.mod, diff each shadow against the new upstream version and forward-port any non-bootstrap changes. The pattern: read upstream, find this module's local additions (usually one or two inserted blocks), reapply them on top of the new upstream text, then run make self-test.

A # ours comment at the top of every shadow file documents what was inserted vs. upstream. If you find a shadow without that header, treat it as a maintenance gap: either add the header or unshadow the file.

Top-level partials

FileWhy it's shadowedDiff target on Hextra upgrade
layouts/partials/footer.htmlReplace "Powered by Hextra" with Solo footer; honor params.footer.copyright AND call custom/footer.html.hextra/layouts/_partials/footer.html
layouts/partials/navbar.htmlWire up version dropdown + Solo brand chrome.hextra/layouts/_partials/navbar.html
layouts/partials/navbar-title.htmlLight/dark product logo with sidebar variant.hextra/layouts/_partials/navbar-title.html
layouts/partials/sidebar.htmlMaterial icons in nav, product-logo block, sidebar badges.hextra/layouts/_partials/sidebar.html
layouts/partials/toc.htmlSticky "Scroll to top" footer; scroll-spy hook.hextra/layouts/_partials/toc.html
layouts/partials/breadcrumb.htmlHome icon + chevron separators matching old theme.hextra/layouts/_partials/breadcrumb.html
layouts/partials/copy-markdown.htmlCopy-as-Markdown button with dropdown + dialog.hextra/layouts/_partials/copy-markdown.html (if present upstream) or NEW partial unique to this module
layouts/partials/version-banner.htmlPer-page "you're viewing vN, latest is vM" banner.NEW partial unique to this module

Subdirectory shadows

DirectoryNotes
layouts/partials/components/Per-component overrides — re-diff each file individually on upgrade.
layouts/partials/docs/Docs-layout-specific partials.
layouts/partials/scripts/Search, mermaid, analytics, math (KaTeX/MathJax).
layouts/partials/utils/Page description, page-width override.
layouts/partials/themeExtras/Not a shadow. Module-internal namespace. Bootstrap content invoked by consumers from their own custom/head-end.html.

Shortcode shadows

The authoring section above lists the shortcodes that shadow Hextra (callout, details, card, cards) and the ones unique to this module. Each shortcode file starts with a # ours comment block explaining what was changed vs. the Hextra original.

Debugging shadow resolution

When a partial isn't behaving as expected, the first question is "which template did Hugo actually load?" Hugo's templates.Current action (available since v0.146.0) reports the resolved template's filename inline, which is useful when chasing override precedence between this module, upstream Hextra, and the consumer's layouts/.

Drop a warnf into the partial you're debugging:

{{ warnf "TEMPLATE %s resolved from %s" templates.Current.Name templates.Current.Filename }}

Run a build; the resolved path lands in stderr (for example .build-oss.log). Compare against what you expected:

  • A path under /go/pkg/mod/github.com/imfing/hextra@.../ means Hugo found the upstream Hextra version.
  • A path inside this module's working tree means the shadow won.
  • A path inside the consumer's layouts/ means the consumer's override won (highest precedence).

Remove the warnf once you have your answer. It's a debug aid, not a logging hook to ship: every warning during a build counts against the hugo-warnings allowlist.

Hextra upgrade workflow

  1. Bump go.mod: hugo mod get github.com/imfing/hextra@vX.Y.Z.
  2. For each entry in the tables above, fetch the new upstream file and diff against the module's shadow. Reapply the local insertions.
  3. Run make build-fixture && make self-test.
  4. Visual smoke: open /v2/everything/ and /v2/rebased/ in light + dark.
  5. Tag a new module release; update CHANGELOG.md.
  6. See RELEASE.md for the full release checklist.

Keeping this current

When you add or change a shortcode, render hook, or shadowed layout file, update the matching row here. If a row here and the source comment (or the # ours header) disagree, trust the source and fix the row.