On this page
Building the chrome a converted body is wrapped in: navigation, table of contents, breadcrumbs, pickers, notices, the search surface and every SEO tag.
#internal/page
#internal/page
Package page builds the chrome a converted Markdown body is wrapped in.
The body HTML itself comes from the html package; everything a reader sees around it is built here: the sidebar and its navigation tree, the topbar, the table of contents, the breadcrumbs, the version and locale pickers, the superseded-version notice, the share control, the page footer, the search surface, the Pagefind filter and metadata elements, and every SEO tag in the head including the JSON-LD documents.
GenerateHTML is the entry point the build calls once per locale-and-version pass. It converts every page of one mount, collects the terms the pages declared, synthesizes the glossary page from them, and wraps each page in the full document -- returning a map keyed by each page's output key, so no caller has to staple the mount prefix on afterwards.
#Ordered inputs
The Python surface this replaces passed insertion-ordered dicts of Markdown sources, and three decisions read that order: which nav group title wins when two pages in one directory declare different ones, how two groups whose titles sort equal are ordered, and the order pages are converted in. A Go map has no order, so the sources arrive as a slice of SourceFile and the order is the caller's to state.
#JSON-LD is emitted in Python's spelling
The structured data is written with the separators and the key order Python's json.dumps produces -- ", " between items, ": " between a key and its value, and properties in the order the emitter built them. That is why identity.Entity is an ordered property list and why util.PythonJSON, which sorts keys and writes no spaces, is not what emits these documents.
#PagefindWidgetBundle
const PagefindWidgetBundle = "pagefind/pagefind-ui.js"PagefindWidgetBundle is the widget bundle a page names when it loads Pagefind's own search UI. A tree in which no page names it is a tree the widget can be pruned from.
#PagefindFacetKeys
var PagefindFacetKeys = []string{PagefindFacetKeys are the facets the corpus carries, in the order they are emitted. Every one is a Pagefind filter, so every one is selectable in the search UI; "tags" is last because it is the only multi-valued key.
The slice is package state a caller must not write to.
#PagefindUIAssets
var PagefindUIAssets = []string{PagefindUIAssets is every file the Pagefind indexer writes that belongs to its own search WIDGET rather than to the index or the query API.
A framework theme draws its own search surface, so these are neither loaded nor kept: they would be a payload every deploy carries and no page references, and their stylesheets paint rounded corners the framework does not allow.
The slice is package state a caller must not write to.
#PageDates
type PageDates structPageDates are the dates one page states: when it was first published and when it was last changed, each in ISO form and each "" when unstated.
#Options
type Options structOptions is everything one locale-and-version pass of the build hands the page renderer.
It mirrors the keyword arguments of the Python function it replaces, with two omissions: that function also took the current version and an is-latest flag, and read neither.
Start from NewOptions rather than from the zero value. Four settings default to true or to a non-empty string in the surface this replaces -- the previous/next links, the reading-progress line, the glossary page and the code-icon style -- so a zero-valued Options turns three features off and asks for a code-icon style that does not exist.
#NotFoundOptions
type NotFoundOptions structNotFoundOptions is what the 404 page is built from.
It mirrors the keyword arguments of the Python function it replaces, with one omission: that function also took the repository URL and never passed it on, because the 404 has no source file to offer an edit link for.
#SourceFile
type SourceFile structSourceFile is one Markdown source and the docs-relative path it was read from.
It stands in for one entry of the insertion-ordered dict the Python surface passed around: the order of the slice is the order the pages were walked, and three navigation decisions read it (see the package documentation).
#NavItem
type NavItem structNavItem is one entry of the sidebar navigation tree.
An entry is either a page or a group, and Group is what tells them apart: a page carries Label, Path and MdPath, while a group carries Group, Slug and Items. Nothing carries both.
#VersionEntry
type VersionEntry structVersionEntry is one version the project has published, as the config declares it. The order of the configured list is oldest to newest: the version picker reads the last entry as the current one.
#LocaleEntry
type LocaleEntry structLocaleEntry is one locale the project publishes, as the config declares it.
#PagefindFacets
type PagefindFacets structPagefindFacets are the facet values one page emits for the search index.
#SEOOptions
type SEOOptions structSEOOptions is everything the head's SEO block is built from.
It mirrors the keyword arguments of the Python function it replaces, one field per argument, with one omission: that function also took the current locale and never read it.
#WrapOptions
type WrapOptions structWrapOptions is everything one page's document is built from.
It mirrors the keyword arguments of the Python function it replaces, with two omissions: that function also took the current version and an is-latest flag, and read neither.
Three fields are pointers because the empty string is a legal value for them and "not stated" is a different answer: a page at its mount root really does have an empty hop. Leaving one nil takes the default the field documents.
#NewOptions
func NewOptions() OptionsNewOptions returns the options a build starts from: every setting whose absence means something other than the Go zero value, spelled out.
#GenerateHTML
func GenerateHTML(opts Options) (map[string]string, error)GenerateHTML converts one mount's Markdown pages into full HTML documents.
The result is keyed by each page's output key -- the mount-prefixed path the file is written at -- so no caller has to staple the mount on afterwards.
Two passes, because a page's body depends on what the other pages declared. The first converts each page, applies the post-processing, and collects every author-declared term into the site-wide table. The glossary page is then synthesized from that table, unless the project ships a glossary page of its own or has turned the feature off. The second pass links each definition site to its glossary entry and wraps every page in the full document.
It refuses a page that declares more than one H1 heading, and one that declares neither an H1 nor a frontmatter title: a page's title is what every address, anchor and metadata field is built from, so there is nothing to guess from.
#Generate404Page
func Generate404Page(opts NotFoundOptions) (string, error)Generate404Page generates the custom 404 page from the standard page template.
The 404 sits at the output root, so its assets need no relative hop -- but the pages it links to live under a mount, and the mount coordinates say which one. Every page it links to is a current one, so the hop is always to the stable mount: the sidebar never points a lost reader into an archived version.
It is emitted only by a project that serves its own output root. A 404 is a hosting-provider convention answered at the root of what is served, and a mounted project's output root is a subdirectory of somebody else's site, so the caller is the one that decides whether to ask for one.
#BuildNav
func BuildNav(BuildNav builds the navigation tree from the Markdown file list.
Top-level pages sort by the "order" frontmatter value (lower first) and then alphabetically by source path; a page declaring no order sorts after every page that does. index.md is always first, and its label is always "Home" -- a frontmatter title does not reach the sidebar for the home page.
A page in a subdirectory joins a collapsible group named after the first path component: the directory name with hyphens and underscores replaced by spaces and then title-cased, unless a page in it declares "nav_group", which overrides the title (the last such page in the file list wins). Within a group, pages sort by "nav_order" (default 0) and then by source path. Groups sort by their title, lower-cased, with two equal titles keeping the order their first page appeared in.
unversionedPages are the persistent pages a versioned build shows in every version's sidebar. They are appended at the end as up to two groups: the pages declaring "type: post" as "Posts", newest first by their "date" value, and the rest as "General", sorted like any group. Every item in both carries Unversioned. Passing none appends neither group.
#FlattenNav
func FlattenNav(navItems []NavItem) []NavItemFlattenNav flattens grouped navigation items into a simple page list.
Groups are expanded in place, so the previous/next links a page carries cross group boundaries in sidebar order.
#RenderNav
func RenderNav(RenderNav renders the sidebar navigation HTML.
Page entries render as flat anchors. Group entries render inside a native details/summary disclosure reusing the framework's tree-row classes, so the scripted and unscripted spellings are painted identically; the group holding the active page carries "open" so it auto-expands. Hrefs use clean directory URLs ("guide/" rather than "guide/index.html").
Three hops, because the sidebar spans three roots: prefix reaches the rendering page's own mount, unversionedPrefix reaches the version-free mount where every item carrying the unversioned marker was built, and sitePrefix reaches the site level, where the posts are. Inside a version the first two differ by one level, and addressing an unversioned page with the versioned hop names a file no build ever writes.
#PagefindHeadTags
func PagefindHeadTags(assetPrefix string) stringPagefindHeadTags returns the head tags that load the Pagefind UI bundle.
The bundle is what the indexer itself wrote into "pagefind/" at the output root, never a CDN copy: a built site answers its own searches with no network at all.
assetPrefix is the hop from this page back to the output root, as the addressing authority computed it.
#ModuleSpecifier
func ModuleSpecifier(path string) stringModuleSpecifier returns path as a specifier a browser will resolve against the page.
A module specifier that begins with neither "." nor "/" is a BARE specifier, which a browser refuses outright unless an import map defines it -- import "js/palette.js" raises "Failed to resolve module specifier". A page at the output root is where the hop is empty and the path becomes bare, which is to say the front page and every project's landing page. The Pagefind bundle path made the same mistake in the same place, which is why this is a function rather than a remembered "./".
#ThemeModulesPrefix
func ThemeModulesPrefix(cssHref string) (string, error)ThemeModulesPrefix returns the hop from a page to the framework payload's module directory.
Both places a framework theme's stylesheet is written -- a standalone build's "css/style.css" and the assembly's "_chrome/
A stylesheet address that is not a framework theme's is an error, because there is no payload to address from it.
#PaletteSearchScript
func PaletteSearchScript(assetPrefix, cssHref string) (string, error)PaletteSearchScript returns the module script that gives a framework theme its search surface.
The framework's command palette replaces Pagefind's shipped widget: the widget's stylesheet and bundle are not loaded at all, and the palette queries the index through Pagefind's own search() API instead. What the reader gets is the framework's own overlay -- keyboard-driven, painted by the sheets already on the page -- rather than a second design language bolted onto the corner of the site.
The palette ranks what a source returns by subsequence-matching the query against each item's label, so a source that pre-filters (as a full-text index does) has to return labels the query still matches. That is why the label carries the matched excerpt after the page title rather than the title alone.
assetPrefix reaches the index; cssHref locates the framework payload the modules are served from.
#PagefindFacetsHTML
func PagefindFacetsHTML(facets PagefindFacets) stringPagefindFacetsHTML returns the hidden facet elements Pagefind reads its filters from.
Pagefind takes one data-pagefind-filter per element, so each facet value is its own empty element. That is also the shape multi-valued tags need, and it means no value is ever escaped into a comma-separated list where a comma inside a tag or a nav group name would split it in two.
Empty values are omitted: an empty filter value is a filter group the UI offers and nothing matches.
The elements must sit inside the data-pagefind-body region, which is the article -- a filter outside the indexed body is not read.
#DerivePageType
func DerivePageType(mdPath string, pageMeta util.Frontmatter, navGroup string) stringDerivePageType returns the "type" facet for a page.
Explicit frontmatter wins; otherwise the type is read off what the page IS -- a generated reference page in an API or CLI nav group, a changelog, a glossary, or an ordinary guide. Distinct from the frontmatter-only page type the layout and the structured data use: every page has a facet type, while only a page that declares one gets special layout.
#PagefindMetaHTML
func PagefindMetaHTML(project, pageType, date string) stringPagefindMetaHTML returns the hidden elements carrying Pagefind result metadata.
Metadata is what a result SHOWS, as opposed to what it filters by. One element per key for the same reason the facets get one each: an element carries a single data-pagefind-meta attribute, and the comma-separated form would split a value that contains a comma.
#PagefindInitScript
func PagefindInitScript(assetPrefix string) stringPagefindInitScript returns the inline script that initializes the Pagefind UI and wires the Cmd+K shortcut.
NO bundlePath is passed, on purpose. The UI derives its own from document.currentScript.src at load time, which yields the ROOT-ABSOLUTE path of the directory the bundle was loaded from -- "
A build-time value was passed here for a long time, computed as assetPrefix + "pagefind/", and it was wrong twice over. The UI loads the index with a dynamic import(), whose relative specifiers resolve against the MODULE's URL -- "
- The only depths that worked were the ones where the two mistakes
cancelled.
assetPrefix stays in the signature because the caller has it and the head tags beside this one still need it.
#PagefindDialogHTML
func PagefindDialogHTML() stringPagefindDialogHTML returns the search dialog the Pagefind UI mounts into.
The dialog itself is chrome: the input, the results list and the filter controls are all rendered by the Pagefind UI inside "#pagefind-container".
#RenderSEOTags
func RenderSEOTags(opts SEOOptions) (seoTags string, securityMeta string, err error)RenderSEOTags builds the head's SEO block: the JSON-LD documents, the Open Graph and Twitter Card meta, the canonical link, the hreflang alternates, and -- separately -- the security meta a GitHub Pages deploy needs.
The two results are returned apart because the page template writes them in that order with nothing between them, and the security block is the one part that depends on where the site is hosted rather than on the page.
#ExtractTitle
func ExtractTitle(mdContent, fallback string) stringExtractTitle returns the text of the first H1 heading in Markdown content, or fallback when the content has none.
It reads the block token list rather than scanning lines, so a "#"-prefixed line inside a fenced code block is code and never a title.
#WrapPage
func WrapPage(opts WrapOptions) (string, error)WrapPage wraps a converted body in the full HTML document.
Prefix reaches this page's own mount root (its sibling pages), AssetPrefix reaches the output root (the shared assets), and UnversionedPrefix reaches the version-free mount (the pages marked "versioned: false"). All three come from the addressing authority. HomeHref addresses the page the site calls home, which is not always this mount's index.
#NavItem.IsGroup
func (n NavItem) IsGroup() bool { return n.Group != "" }IsGroup reports whether this entry is a group rather than a page.