Skip to content
internal/page
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

Go go
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.

#DocumentTitleLimit

Go go
const DocumentTitleLimit = 60

DocumentTitleLimit is the longest a document title should be: a search engine displays about 50 to 60 characters of one and cuts the rest, and the check that measures a page's title holds every page to that number.

#PagefindFacetKeys

Go go
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

Go go
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

Go go
type PageDates struct

PageDates 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

Go go
type Options struct

Options 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

Go go
type NotFoundOptions struct

NotFoundOptions 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

Go go
type SourceFile struct

SourceFile 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).

Go go
type NavItem struct

NavItem 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

Go go
type VersionEntry struct

VersionEntry 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

Go go
type LocaleEntry struct

LocaleEntry is one locale the project publishes, as the config declares it.

#PagefindFacets

Go go
type PagefindFacets struct

PagefindFacets are the facet values one page emits for the search index.

#SEOOptions

Go go
type SEOOptions struct

SEOOptions 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

Go go
type WrapOptions struct

WrapOptions 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.

#DocumentTitleParts

Go go
type DocumentTitleParts struct

DocumentTitleParts are the written values a page's is composed of.</p> <p>Every one of them is text somebody wrote -- a page's <a href="../glossary-terms/#term-frontmatter" class="term-link" data-tooltip="Defined in: Glossary Terms">frontmatter</a> title, a project's declared name, a site's declared name. Nothing here is cut out of another field.</p> <h3 id="newoptions"><a class="heading-link" href="#newoptions" aria-label="Link to section: NewOptions">#</a><code>NewOptions</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">NewOptions</span><span class="p">(</span><span class="p">)</span><span class="w"> </span><span class="nx">Options</span></code></pre></figure> <p>NewOptions returns the options a build starts from: every setting whose absence means something other than the Go zero value, spelled out.</p> <h3 id="generatehtml"><a class="heading-link" href="#generatehtml" aria-label="Link to section: GenerateHTML">#</a><code>GenerateHTML</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">GenerateHTML</span><span class="p">(</span><span class="nx">opts</span><span class="w"> </span><span class="nx">Options</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>GenerateHTML converts one mount's Markdown pages into full HTML documents.</p> <p>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.</p> <p>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.</p> <p>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.</p> <h3 id="generate404page"><a class="heading-link" href="#generate404page" aria-label="Link to section: Generate404Page">#</a><code>Generate404Page</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">Generate404Page</span><span class="p">(</span><span class="nx">opts</span><span class="w"> </span><span class="nx">NotFoundOptions</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>Generate404Page generates the custom 404 page from the standard page template.</p> <p>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.</p> <p>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.</p> <h3 id="buildnav"><a class="heading-link" href="#buildnav" aria-label="Link to section: BuildNav">#</a><code>BuildNav</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">BuildNav</span><span class="p">(</span></code></pre></figure> <p>BuildNav builds the navigation tree from the Markdown file list.</p> <p>Top-level pages sort by the "nav_order" frontmatter value (lower first) and then alphabetically by source path; a page declaring none 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.</p> <p>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.</p> <p>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.</p> <h3 id="flattennav"><a class="heading-link" href="#flattennav" aria-label="Link to section: FlattenNav">#</a><code>FlattenNav</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">FlattenNav</span><span class="p">(</span><span class="nx">navItems</span><span class="w"> </span><span class="p">[</span><span class="p">]</span><span class="nx">NavItem</span><span class="p">)</span><span class="w"> </span><span class="p">[</span><span class="p">]</span><span class="nx">NavItem</span></code></pre></figure> <p>FlattenNav flattens grouped navigation items into a simple page list.</p> <p>Groups are expanded in place, so the previous/next links a page carries cross group boundaries in sidebar order.</p> <h3 id="rendernav"><a class="heading-link" href="#rendernav" aria-label="Link to section: RenderNav">#</a><code>RenderNav</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">RenderNav</span><span class="p">(</span></code></pre></figure> <p>RenderNav renders the sidebar navigation HTML.</p> <p>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").</p> <p>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.</p> <h3 id="pagefindheadtags"><a class="heading-link" href="#pagefindheadtags" aria-label="Link to section: PagefindHeadTags">#</a><code>PagefindHeadTags</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PagefindHeadTags</span><span class="p">(</span><span class="nx">assetPrefix</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>PagefindHeadTags returns the head tags that load the Pagefind UI bundle.</p> <p>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.</p> <p>assetPrefix is the hop from this page back to the output root, as the addressing authority computed it.</p> <h3 id="modulespecifier"><a class="heading-link" href="#modulespecifier" aria-label="Link to section: ModuleSpecifier">#</a><code>ModuleSpecifier</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">ModuleSpecifier</span><span class="p">(</span><span class="nx">path</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>ModuleSpecifier returns path as a specifier a browser will resolve against the page.</p> <p>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 "./".</p> <h3 id="thememodulesprefix"><a class="heading-link" href="#thememodulesprefix" aria-label="Link to section: ThemeModulesPrefix">#</a><code>ThemeModulesPrefix</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">ThemeModulesPrefix</span><span class="p">(</span><span class="nx">cssHref</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>ThemeModulesPrefix returns the hop from a page to the framework payload's module directory.</p> <p>Both places a framework theme's stylesheet is written -- a standalone build's "css/style.css" and the assembly's "_chrome/<theme>-<digest>/css/style.css" -- end in the same relative tail, because both are themes.FrameworkCSSRel. Stripping that tail off the address the page already carries yields the payload root, and the modules sit beside the stylesheet's directory inside it. Deriving it beats threading a second address through every page renderer: the two can then never disagree about where the payload is.</p> <p>A stylesheet address that is not a framework theme's is an error, because there is no payload to address from it.</p> <h3 id="palettesearchscript"><a class="heading-link" href="#palettesearchscript" aria-label="Link to section: PaletteSearchScript">#</a><code>PaletteSearchScript</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PaletteSearchScript</span><span class="p">(</span><span class="nx">assetPrefix</span><span class="p">,</span><span class="w"> </span><span class="nx">cssHref</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>PaletteSearchScript returns the module script that gives a framework theme its search surface.</p> <p>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.</p> <p>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.</p> <p>assetPrefix reaches the index; cssHref locates the framework payload the modules are served from.</p> <h3 id="pagefindfacetshtml"><a class="heading-link" href="#pagefindfacetshtml" aria-label="Link to section: PagefindFacetsHTML">#</a><code>PagefindFacetsHTML</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PagefindFacetsHTML</span><span class="p">(</span><span class="nx">facets</span><span class="w"> </span><span class="nx">PagefindFacets</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>PagefindFacetsHTML returns the hidden facet elements Pagefind reads its filters from.</p> <p>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.</p> <p>Empty values are omitted: an empty filter value is a filter group the UI offers and nothing matches.</p> <p>The elements must sit inside the data-pagefind-body region, which is the article -- a filter outside the indexed body is not read.</p> <h3 id="derivepagetype"><a class="heading-link" href="#derivepagetype" aria-label="Link to section: DerivePageType">#</a><code>DerivePageType</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">DerivePageType</span><span class="p">(</span><span class="nx">mdPath</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">pageMeta</span><span class="w"> </span><span class="nx">util</span><span class="p">.</span><span class="nx">Frontmatter</span><span class="p">,</span><span class="w"> </span><span class="nx">navGroup</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>DerivePageType returns the "type" facet for a page.</p> <p>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.</p> <h3 id="pagefindmetahtml"><a class="heading-link" href="#pagefindmetahtml" aria-label="Link to section: PagefindMetaHTML">#</a><code>PagefindMetaHTML</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PagefindMetaHTML</span><span class="p">(</span><span class="nx">project</span><span class="p">,</span><span class="w"> </span><span class="nx">pageType</span><span class="p">,</span><span class="w"> </span><span class="nx">date</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>PagefindMetaHTML returns the hidden elements carrying Pagefind result metadata.</p> <p>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.</p> <h3 id="pagefindinitscript"><a class="heading-link" href="#pagefindinitscript" aria-label="Link to section: PagefindInitScript">#</a><code>PagefindInitScript</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PagefindInitScript</span><span class="p">(</span><span class="nx">assetPrefix</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>PagefindInitScript returns the inline script that initializes the Pagefind UI and wires the Cmd+K shortcut.</p> <p>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 -- "<assetPrefix>pagefind/" resolved against the page. That is the correct answer at every depth and under every mount, and it is the one thing a build-time string cannot be.</p> <p>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 -- "<assetPrefix>pagefind/" -- and not against the page. So a page at the output root sent "pagefind/", a bare specifier a browser refuses outright, and every site's front page and every project's landing page returned no search results at all. A page more than one level inside its mount sent one "../" too many and fetched another project's index, or a</p> <ol><li>The only depths that worked were the ones where the two mistakes</li></ol> <p>cancelled.</p> <p>assetPrefix stays in the signature because the caller has it and the head tags beside this one still need it.</p> <h3 id="pagefinddialoghtml"><a class="heading-link" href="#pagefinddialoghtml" aria-label="Link to section: PagefindDialogHTML">#</a><code>PagefindDialogHTML</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">PagefindDialogHTML</span><span class="p">(</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>PagefindDialogHTML returns the search dialog the Pagefind UI mounts into.</p> <p>The dialog itself is chrome: the input, the results list and the filter controls are all rendered by the Pagefind UI inside "#pagefind-container".</p> <h3 id="renderseotags"><a class="heading-link" href="#renderseotags" aria-label="Link to section: RenderSEOTags">#</a><code>RenderSEOTags</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">RenderSEOTags</span><span class="p">(</span><span class="nx">opts</span><span class="w"> </span><span class="nx">SEOOptions</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">seoTags</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">securityMeta</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>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.</p> <p>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.</p> <h3 id="extracttitle"><a class="heading-link" href="#extracttitle" aria-label="Link to section: ExtractTitle">#</a><code>ExtractTitle</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">ExtractTitle</span><span class="p">(</span><span class="nx">mdContent</span><span class="p">,</span><span class="w"> </span><span class="nx">fallback</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>ExtractTitle returns the text of the first H1 heading in Markdown content, or fallback when the content has none.</p> <p>It reads the block token list rather than scanning lines, so a "#"-prefixed line inside a fenced <a href="../glossary-terms/#term-code-block" class="term-link" data-tooltip="Defined in: Glossary Terms">code block</a> is code and never a title.</p> <h3 id="documenttitle"><a class="heading-link" href="#documenttitle" aria-label="Link to section: DocumentTitle">#</a><code>DocumentTitle</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">DocumentTitle</span><span class="p">(</span><span class="nx">parts</span><span class="w"> </span><span class="nx">DocumentTitleParts</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span></code></pre></figure> <p>DocumentTitle is what a page's <title> element carries: the written values in [DocumentTitleParts], joined with " - ".</p> <p>The page's own title comes first, then the project that publishes it, then the site that publishes the project. An empty name is left out, and a name equal to the one before it is written once, so no title ever renders "X - X".</p> <p>An index page names no project. It IS the project's front page, and its written title already says which project a reader arrived at, so only the site name follows it -- and on a standalone build, where there is no site name, the written title stands alone.</p> <h3 id="wrappage"><a class="heading-link" href="#wrappage" aria-label="Link to section: WrapPage">#</a><code>WrapPage</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="nf">WrapPage</span><span class="p">(</span><span class="nx">opts</span><span class="w"> </span><span class="nx">WrapOptions</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span></code></pre></figure> <p>WrapPage wraps a converted body in the full HTML document.</p> <p>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.</p> <h3 id="navitemisgroup"><a class="heading-link" href="#navitemisgroup" aria-label="Link to section: NavItem.IsGroup">#</a><code>NavItem.IsGroup</code></h3> <figure class="tm-code"><figcaption class="tm-code-bar"><span class="tm-code-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="2" fill="#00ADD8"/><text x="8" y="12.5" text-anchor="middle" font-family="monospace" font-size="9" font-weight="700" fill="#fff">Go</text></svg> go</span><span class="tm-code-actions"></span></figcaption><pre tabindex="0" aria-label="Code: go"><code class="language-go"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">n</span><span class="w"> </span><span class="nx">NavItem</span><span class="p">)</span><span class="w"> </span><span class="nf">IsGroup</span><span class="p">(</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">Group</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">""</span><span class="w"> </span><span class="p">}</span></code></pre></figure> <p>IsGroup reports whether this entry is a group rather than a page.</p> <div class="share-address"><span class="share-address-label">Share this page</span><button type="button" class="share-address-copy" data-share-url="https://smmh.dev/selfdoc/internal-page/">Evergreen link (always current)</button></div> <footer class="page-footer"><div class="page-meta"><span>Last updated <time datetime="2026-09-15">September 15, 2026</time></span></div><nav class="page-nav"><a class="page-nav-prev" href="../internal-ownership/"><span class="page-nav-label">Previous</span>← internal/ownership</a><span class="page-progress">Page 92 of 115</span><a class="page-nav-next" href="../internal-payloadschemas/"><span class="page-nav-label">Next</span>internal/payloadschemas →</a></nav></footer> </article> </div> <nav class="docs-toc" aria-label="On this page"><div class="docs-toc-head">Contents</div><a class="docs-toc-item" href="#internalpage">internal/page</a> <a class="docs-toc-item sub" href="#ordered-inputs">Ordered inputs</a> <a class="docs-toc-item sub" href="#json-ld-is-emitted-in-pythons-spelling">JSON-LD is emitted in Python's spelling</a> <a class="docs-toc-item sub" href="#pagefindwidgetbundle">PagefindWidgetBundle</a> <a class="docs-toc-item sub" href="#documenttitlelimit">DocumentTitleLimit</a> <a class="docs-toc-item sub" href="#pagefindfacetkeys">PagefindFacetKeys</a> <a class="docs-toc-item sub" href="#pagefinduiassets">PagefindUIAssets</a> <a class="docs-toc-item sub" href="#pagedates">PageDates</a> <a class="docs-toc-item sub" href="#options">Options</a> <a class="docs-toc-item sub" href="#notfoundoptions">NotFoundOptions</a> <a class="docs-toc-item sub" href="#sourcefile">SourceFile</a> <a class="docs-toc-item sub" href="#navitem">NavItem</a> <a class="docs-toc-item sub" href="#versionentry">VersionEntry</a> <a class="docs-toc-item sub" href="#localeentry">LocaleEntry</a> <a class="docs-toc-item sub" href="#pagefindfacets">PagefindFacets</a> <a class="docs-toc-item sub" href="#seooptions">SEOOptions</a> <a class="docs-toc-item sub" href="#wrapoptions">WrapOptions</a> <a class="docs-toc-item sub" href="#documenttitleparts">DocumentTitleParts</a> <a class="docs-toc-item sub" href="#newoptions">NewOptions</a> <a class="docs-toc-item sub" href="#generatehtml">GenerateHTML</a> <a class="docs-toc-item sub" href="#generate404page">Generate404Page</a> <a class="docs-toc-item sub" href="#buildnav">BuildNav</a> <a class="docs-toc-item sub" href="#flattennav">FlattenNav</a> <a class="docs-toc-item sub" href="#rendernav">RenderNav</a> <a class="docs-toc-item sub" href="#pagefindheadtags">PagefindHeadTags</a> <a class="docs-toc-item sub" href="#modulespecifier">ModuleSpecifier</a> <a class="docs-toc-item sub" href="#thememodulesprefix">ThemeModulesPrefix</a> <a class="docs-toc-item sub" href="#palettesearchscript">PaletteSearchScript</a> <a class="docs-toc-item sub" href="#pagefindfacetshtml">PagefindFacetsHTML</a> <a class="docs-toc-item sub" href="#derivepagetype">DerivePageType</a> <a class="docs-toc-item sub" href="#pagefindmetahtml">PagefindMetaHTML</a> <a class="docs-toc-item sub" href="#pagefindinitscript">PagefindInitScript</a> <a class="docs-toc-item sub" href="#pagefinddialoghtml">PagefindDialogHTML</a> <a class="docs-toc-item sub" href="#renderseotags">RenderSEOTags</a> <a class="docs-toc-item sub" href="#extracttitle">ExtractTitle</a> <a class="docs-toc-item sub" href="#documenttitle">DocumentTitle</a> <a class="docs-toc-item sub" href="#wrappage">WrapPage</a> <a class="docs-toc-item sub" href="#navitemisgroup">NavItem.IsGroup</a></nav> </div> <section class="sibling-projects" data-pagefind-ignore aria-labelledby="sibling-projects-heading"> <h2 id="sibling-projects-heading">More tools from this site</h2> <ul> <li><a href="../../claudestream/">claudestream</a> <span>Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python</span></li> <li><a href="../../claudewheel/">claudewheel</a> <span>A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.</span></li> <li><a href="../../dirstat/">dirstat</a> <span>Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON</span></li> <li><a href="../../fastware/">fastware</a> <span>A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client</span></li> <li><a href="../../go-toml-edit/">go-toml-edit</a> <span>Zero-dep TOML editing library for Go with comment preservation</span></li> <li><a href="../../howmuchleft/">howmuchleft</a> <span>The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms</span></li> <li><a href="../../orxtra/">orxtra</a></li> <li><a href="../../pgdesign/">pgdesign</a></li> <li><a href="../../predraw/">predraw</a> <span>Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines</span></li> <li><a href="../../reposummary/">reposummary</a> <span>Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM</span></li> <li><a href="../../rlsbl/">rlsbl</a> <span>Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more</span></li> <li><a href="../../safegit/">safegit</a> <span>git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository</span></li> <li><a href="../../saferm/">saferm</a> <span>Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored</span></li> <li><a href="../../strictcli/">strictcli</a></li> <li><a href="../../stricttest/">stricttest</a> <span>An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.</span></li> <li><a href="../../wesktop/">wesktop</a> <span>A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview</span></li> </ul> </section> <footer class="site-footer"> <p>Built with <a href="https://github.com/smm-h/selfdoc">selfdoc</a></p> <p><a class="feed-link" href="../feed.xml"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>Subscribe via RSS</a></p> </footer> </main> </div> <div class="tm-sr-only" aria-live="polite" aria-atomic="true"></div> </div> <script>(function(){var btn=document.querySelector('.theme-toggle');var states=['system','light','dark'];function getState(){var s=localStorage.getItem('selfdoc-theme');return(s==='light' || s==='dark')? s : 'system';}function apply(state){if(state==='light' || state==='dark'){document.documentElement.setAttribute('data-theme',state);localStorage.setItem('selfdoc-theme',state);}else{document.documentElement.removeAttribute('data-theme');localStorage.removeItem('selfdoc-theme');}btn.setAttribute('data-state',state);var labels={system: 'Theme: system. Click for light mode',light: 'Theme: light. Click for dark mode',dark: 'Theme: dark. Click for system theme'};btn.setAttribute('aria-label',labels[state]);}apply(getState());btn.addEventListener('click',function(){var cur=getState();var next=states[(states.indexOf(cur)+ 1)% states.length];apply(next);});})();(function(){var toggle=document.querySelector('.tm-hamburger');var sidebar=document.getElementById('tm-sidebar');var app=document.getElementById('tm-app');if(!toggle || !sidebar || !app)return;function openSidebar(){app.classList.add('sidebar-open');toggle.setAttribute('aria-expanded','true');var focusable=sidebar.querySelectorAll('a,button,input,[tabindex]');if(focusable.length){focusable[0].focus();sidebar.addEventListener('keydown',trapFocus);}}function closeSidebar(){app.classList.remove('sidebar-open');toggle.setAttribute('aria-expanded','false');sidebar.removeEventListener('keydown',trapFocus);toggle.focus();}function trapFocus(e){if(e.key !=='Tab')return;var focusable=sidebar.querySelectorAll('a,button,input,[tabindex]');if(!focusable.length)return;var first=focusable[0];var last=focusable[focusable.length - 1];if(e.shiftKey && document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();}}toggle.addEventListener('click',function(){if(app.classList.contains('sidebar-open')){closeSidebar();}else{openSidebar();}});document.addEventListener('keydown',function(e){if(e.key==='Escape' && app.classList.contains('sidebar-open')){closeSidebar();}});document.addEventListener('click',function(e){if(app.classList.contains('sidebar-open')&& !sidebar.contains(e.target)&& !toggle.contains(e.target)){closeSidebar();}});sidebar.querySelectorAll('a').forEach(function(link){link.addEventListener('click',function(){closeSidebar();});});})();(function(){var groups=document.querySelectorAll('#tm-nav details.tm-tree-details');if(!groups.length)return;groups.forEach(function(d){var slug=d.querySelector('.tm-tree-label');if(!slug)return;var key='selfdoc-nav-' + slug.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g,'-');var saved=localStorage.getItem(key);if(saved==='closed'){d.removeAttribute('open');}d.addEventListener('toggle',function(){localStorage.setItem(key,d.open ? 'open' : 'closed');});});})();(function(){function setup(container,scroller){if(scroller.scrollWidth <=scroller.clientWidth)return;container.classList.add('has-overflow');function check(){var max=scroller.scrollWidth - scroller.clientWidth;container.classList.toggle('scrolled-start',scroller.scrollLeft <=2);container.classList.toggle('scrolled-end',scroller.scrollLeft >=max - 2);if(container===scroller){container.style.setProperty('--scroll-x',scroller.scrollLeft + 'px');}}scroller.addEventListener('scroll',check,{passive: true});check();}function init(){document.querySelectorAll('.tm-code').forEach(function(el){var pre=el.querySelector('pre');if(pre)setup(el,pre);});document.querySelectorAll('.table-wrap').forEach(function(el){setup(el,el);});}init();if(window.ResizeObserver){new ResizeObserver(function(){document.querySelectorAll('.has-overflow').forEach(function(el){el.classList.remove('has-overflow','scrolled-start','scrolled-end');});init();}).observe(document.documentElement);}})();(function(){var bar=document.getElementById('reading-progress');var scroller=document.getElementById('tm-content');if(!bar || !scroller)return;function update(){var height=scroller.scrollHeight - scroller.clientHeight;if(height <=0){bar.style.width='100%';return;}var progress=Math.min(scroller.scrollTop / height * 100,100);bar.style.width=progress + '%';}scroller.addEventListener('scroll',update,{passive: true});update();})();(function(){var COPY='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="8" width="12" height="12"/><path d="M4 16V4h12"/></svg>';var DONE='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M4 12.5l5 5L20 6"/></svg>';document.querySelectorAll('.tm-code').forEach(function(figure){var code=figure.querySelector('pre code');var slot=figure.querySelector('.tm-code-actions');if(!code || !slot)return;var btn=document.createElement('button');btn.className='copy-btn';btn.type='button';btn.setAttribute('aria-label','Copy code');btn.innerHTML=COPY;btn.addEventListener('click',function(){navigator.clipboard.writeText(code.textContent).then(function(){btn.classList.add('copied');btn.innerHTML=DONE;setTimeout(function(){btn.classList.remove('copied');btn.innerHTML=COPY;},2000);});});slot.appendChild(btn);});})();(function(){var tocLinks=document.querySelectorAll('.docs-toc-item');var scroller=document.getElementById('tm-content');if(!tocLinks.length || !scroller)return;var headings=[];tocLinks.forEach(function(link){var id=link.getAttribute('href').substring(1);var el=document.getElementById(id);if(el)headings.push({el: el,link: link});});var ticking=false;function update(){var top=scroller.getBoundingClientRect().top;var active=null;for(var i=0;i < headings.length;i++){if(headings[i].el.getBoundingClientRect().top - top <=100){active=headings[i];}}tocLinks.forEach(function(a){a.removeAttribute('aria-current');});if(active){active.link.setAttribute('aria-current','page');active.link.scrollIntoView({block: 'nearest',behavior: 'smooth'});}ticking=false;}scroller.addEventListener('scroll',function(){if(!ticking){requestAnimationFrame(update);ticking=true;}},{passive: true});update();})();(function(){document.querySelectorAll('.heading-link').forEach(function(link){link.addEventListener('click',function(e){e.preventDefault();var url=window.location.origin + window.location.pathname + link.getAttribute('href');history.replaceState(null,'',link.getAttribute('href'));navigator.clipboard.writeText(url).then(function(){var toast=document.createElement('span');toast.className='copy-toast';toast.textContent='Link copied!';link.parentElement.style.position='relative';link.parentElement.appendChild(toast);setTimeout(function(){toast.remove();},2000);});});});})();(function(){var buttons=document.querySelectorAll('.share-address-copy');if(!buttons.length)return;Array.prototype.forEach.call(buttons,function(button){button.addEventListener('click',function(){var url=button.getAttribute('data-share-url');if(!url)return;var label=button.textContent;function confirmCopy(){button.textContent='Copied';setTimeout(function(){button.textContent=label;},1500);}navigator.clipboard.writeText(url).then(confirmCopy);});});})();requestAnimationFrame(function(){requestAnimationFrame(function(){document.documentElement.style.scrollBehavior='';});});</script> <dialog class="search-dialog" id="search-dialog" aria-label="Search documentation"> <div class="search-inner"> <div class="search-header"> <span class="search-header-title">Search</span> <button class="search-close" aria-label="Close search" type="button">X</button> </div> <div id="pagefind-container"></div> </div> </dialog> <script> document.addEventListener("DOMContentLoaded", function() { new PagefindUI({ element: "#pagefind-container", showSubResults: true, showImages: false }); var dialog = document.getElementById("search-dialog"); function openSearch() { if (dialog.open) return; dialog.showModal(); var input = dialog.querySelector(".pagefind-ui__search-input"); if (input) input.focus(); } var triggers = document.querySelectorAll(".search-trigger, .search-bar-trigger"); for (var i = 0; i < triggers.length; i++) { triggers[i].addEventListener("click", openSearch); } document.addEventListener("keydown", function(e) { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); if (dialog.open) { dialog.close(); } else { dialog.showModal(); var input = dialog.querySelector(".pagefind-ui__search-input"); if (input) input.focus(); } } if (e.key === "Escape" && dialog.open) { dialog.close(); } }); var closeBtn = dialog.querySelector(".search-close"); if (closeBtn) closeBtn.addEventListener("click", function() { dialog.close(); }); dialog.addEventListener("click", function(e) { if (e.target === dialog) dialog.close(); }); }); </script> </body> </html>