Theme

Netdocs ships the Material theme — a Scriban port of the Material for MkDocs DOM, driven by the vendored Material CSS/JS bundle. The theme is configured under theme in appsettings.json.

Theme keys

Key Type Default Description
name string "material" Theme name.
language string "en" Content language (<html lang>).
logo string — Path to a logo image.
favicon string — Path to a favicon.
customDir string — Override directory for templates/partials (Scriban).
highlight string "highlightjs" Client-side syntax-highlighting renderer for code blocks (see Code highlighting).
palette array — Color schemes (see below).
features array — Enabled Material feature flags.
font object — text and code font families.
icon object — Icon overrides.

Palette

"palette": [
  {
    "media": "(prefers-color-scheme: light)",
    "scheme": "default",
    "primary": "indigo",
    "accent": "indigo"
  }
]
  • scheme — default (light) or slate (dark).
  • primary / accent — Material color names (e.g. indigo, teal, grey, orange).
  • media — optional media query used for automatic light/dark switching.
  • toggle — optional { "icon": "...", "name": "..." }. When present, the theme renders a header switcher button so visitors can flip between schemes (persisted in the browser).

Light/dark toggle

List two palettes, each with a toggle, to render a header switcher. The first palette is the default applied on initial load, so put slate first for a dark-by-default site:

"palette": [
  {
    "scheme": "slate",
    "primary": "indigo",
    "accent": "indigo",
    "toggle": { "icon": "material/brightness-4", "name": "Switch to light mode" }
  },
  {
    "scheme": "default",
    "primary": "indigo",
    "accent": "indigo",
    "toggle": { "icon": "material/brightness-7", "name": "Switch to dark mode" }
  }
]

Recognized toggle icons are material/brightness-7 (sun), material/brightness-4 (moon), and material/brightness-auto. The selected scheme is stored in the browser and restored on the visitor's next visit. Add media queries instead if you prefer the scheme to follow the operating-system setting automatically.

Fonts

"font": { "text": "Roboto", "code": "Roboto Mono" }

Fonts are loaded from Google Fonts. Set to your preferred families.

Features

Feature flags mirror Material for MkDocs. Commonly used flags:

Feature Effect
navigation.tabs Top-level sections become header tabs.
navigation.sections Render top-level sections as groups in the sidebar.
navigation.indexes Section landing pages (first index.md/README.md child links the section title).
navigation.path Breadcrumb trail above page content.
navigation.top Back-to-top button.
navigation.footer Prev/next footer navigation (also sequences blog posts by date).
search.suggest Search-as-you-type suggestions.
search.highlight Highlight matches on the target page.
search.share Shareable search-result deep links.
content.code.copy Copy-to-clipboard on code blocks.
content.tabs.link Link content tabs with the same label across a page.
toc.follow Table of contents follows the scroll position.

Code highlighting

Code blocks are produced in two independent stages:

  1. Parsing (core, always on). The Markdig fence parser understands the fence syntax — language, title="…", linenums, hl_lines, the brace/attr-list form, #!lang inline shebangs, and ```mermaid — and emits neutral, renderer-agnostic HTML: <pre><code class="language-x" …>. This never changes.
  2. Highlighting (theme, swappable). A client-side renderer colourises that HTML in the browser. This is what the highlight key selects.
Value Behaviour
highlightjs (default) highlight.js via CDN, with line numbers, hl_lines row highlighting, and an extended language set (PowerShell, Dockerfile, nginx, HTTP, DNS zones, …). Zero configuration.
none No highlighter is injected. Blocks render as plain, monospaced text. Titles (title="…") still render — that styling is part of the core theme, not the highlighter. Line numbers and hl_lines are not shown (they are produced by the highlight.js renderer).
(any other value) Treated as custom: Netdocs injects no highlighter and gets out of your way. Wire up your own (Prism, Shiki, a self-hosted highlight.js build, …) through extra_css / extra_javascript. Your script sees the same <pre><code class="language-x"> markup.
appsettings.json
{
  "Netdocs": {
    "theme": {
      "name": "material",
      "highlight": "highlightjs"
    }
  }
}

Bringing your own highlighter

Set highlight to any value other than highlightjs/none and load your renderer as an extra asset:

appsettings.json
{
  "Netdocs": {
    "theme": { "highlight": "prism" },
    "extra_css": ["https://cdn.jsdelivr.net/npm/prismjs/themes/prism-tomorrow.css"],
    "extra_javascript": ["https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"]
  }
}

Because the core parser already stamps each block with class="language-x", most highlighters work with no further wiring. If your theme uses instant navigation, re-run your highlighter on Material's document$ observable so blocks are re-processed after each page swap.

Overrides (customDir)

Point customDir at a directory of Scriban templates/partials to override the built-in theme. Files there take precedence over the bundled theme, letting you replace partials such as partials/footer.html or the base main.html.

Overridable templates & partials

The Material theme is deliberately split into small pieces so you can override just the part you care about — for example, replace partials/social.html to change how social links render without copying the whole header. Create a file with the same path under your customDir and it wins over the bundled version.

File Purpose
main.html Base page layout: <html>, includes head, header, nav, content, footer, scripts. Defines the render_nav / render_toc helpers.
export.html Standalone layout used by netdocs export: page content and theme styles only, no header, nav, search or footer.
partials/head.html Everything inside <head>: meta, Open Graph / Twitter cards, stylesheets, fonts, and the inline __md_* helper script. Override to add analytics or verification tags.
partials/header.html The single-row header: logo, top-level nav (left), social + palette toggle + search (right).
partials/social.html Social links at the right of the header (driven by extra.social).
partials/search.html Search box + results overlay markup.
partials/nav.html Sidebar navigation drawer.
partials/tabs.html Header tabs bar (only when navigation.tabs is off and you want a second row).
partials/content-actions.html Floating Edit / View-source buttons at the top-right of the article (content.action.edit / content.action.view).
partials/breadcrumbs.html Breadcrumb trail above the page title (navigation.path, driven by breadcrumbs).
partials/source-file.html "Last updated / Created" line under the article body (show_source_meta, non-post pages).
partials/toc.html Right-hand table of contents.
partials/footer.html Site footer: prev/next nav, copyright, footer links, social links.
partials/post-meta.html Blog-post byline: author avatar/name, date, reading time, categories.
partials/blog-nav.html Blog sidebar: recent posts / categories / archive listing.
partials/scripts.html End-of-body scripts: Material __config blob, the vendored bundle, hashed site scripts, inline scripts, and the highlight/mermaid loaders.
partials/highlight.html highlight.js CDN loader + init (only injected when highlight: highlightjs).
partials/mermaid.html Mermaid CDN loader + init for ```mermaid diagrams.
404.html Not-found page layout.

Blog & tag pages don't have their own template

The blog index and tag pages are produced by the blog/tags plugins as ordinary content pages — their body HTML is generated, then rendered through main.html like any other page. To restyle them, override main.html (or the blog partials post-meta.html / blog-nav.html), or target their generated markup via extra_css. There is no blog.html / tags.html to copy.

Every bundled partial starts with a {{ # … }} comment block documenting the globals it receives and how to override it. Those Scriban comments produce no output, so they never appear in your HTML (minified or not) — read them straight from the theme source.

Note

Netdocs templates use Scriban, not Jinja2. If any .html file under customDir still contains Jinja constructs ({% %}, lang.t, super()), the whole directory is ignored with a warning — port those files to Scriban to re-enable customDir.

Porting a Material override to Scriban

Material's Jinja partials map fairly directly onto Scriban. The common substitutions:

Jinja Scriban
{% if x %}…{% endif %} {{ if x }}…{{ end }}
{% for x in xs %}…{% endfor %} {{ for x in xs }}…{{ end }}
{% include "partials/p.html" %} {{ include "partials/p.html" }}
{% set c = "a" %} {{ c = "a" }}
"feature" in features features | array.contains "feature"
{{ config.site_name }} {{ config.site_name }} (unchanged)

Use the trim markers {{~ / ~}} to strip surrounding whitespace, and the theme helpers (social_icon, social_label, strip_slash, base_url) instead of Material's Jinja filters. For example, an override that renders the configured social links in the header:

{{~ if extra.social ~}}
<div class="header-social">
  {{~ for link in extra.social ~}}
  {{~ label = link.name ~}}
  {{~ if !label || (label | string.strip) == "" ~}}{{~ label = social_label link.icon ~}}{{~ end ~}}
  <a href="{{ link.link }}" class="md-social__link md-header__button md-icon"
     title="{{ label }}" aria-label="{{ label }}" target="_blank" rel="noopener">{{ social_icon link.icon }}</a>
  {{~ end ~}}
</div>
{{~ end ~}}

Configure the links shown in the header and footer under extra.social. Each entry has an icon name (Material/FontAwesome-style, e.g. fontawesome/brands/github) and a link. You can optionally add a name to control the hover tooltip / aria-label; when omitted, a friendly label is derived automatically from the icon (see Tooltips & labels):

{
  "Netdocs": {
    "extra": {
      "social": [
        { "icon": "fontawesome/brands/github", "link": "https://github.com/you/repo" },
        { "icon": "fontawesome/brands/mastodon", "link": "https://fosstodon.org/@you" },
        { "icon": "material/rss", "link": "feed_rss_created.xml", "name": "Subscribe via RSS" }
      ]
    }
  }
}

The social_icon helper maps an icon name to an inline SVG. Built-in glyphs are matched by substring, so both the Material (material/github) and FontAwesome (fontawesome/brands/github) naming styles work:

Matches Renders
github GitHub
discord Discord
reddit Reddit
rss RSS
x-twitter / twitter X / Twitter
mastodon Mastodon
linkedin LinkedIn
youtube YouTube
mail / email / envelope Email
(anything else) Globe (generic link)

Tooltips & labels

Each social link needs readable hover text for usability and accessibility. The theme resolves the label in this order:

  1. An explicit name on the entry (e.g. "name": "Subscribe via RSS") — always wins.
  2. Otherwise the social_label helper derives a friendly name from the icon: it takes the last path segment and maps well-known ones to proper casing — fontawesome/solid/rss → RSS Feed, fontawesome/brands/x-twitter → X (Twitter), fontawesome/brands/linkedin → LinkedIn, fontawesome/solid/globe → Website, and so on.
  3. Anything unrecognised is title-cased from its last segment, so simple/my-service still reads as My Service rather than the raw icon path.

The resolved label is used for both the title (tooltip) and aria-label. Set an explicit name whenever the icon alone is ambiguous — for example a generic globe that actually points to your main website.

Custom icons

Add or override glyphs without editing the theme by supplying extra.social_icons — a map of icon-name substring to an SVG path (using a 0 0 24 24 viewBox). Custom entries take precedence over the built-ins:

{
  "Netdocs": {
    "extra": {
      "social_icons": {
        "bluesky": "M12 10.8C10.7 8.3 7.2 3.6 3.9 ... Z"
      },
      "social": [
        { "icon": "fontawesome/brands/bluesky", "link": "https://bsky.app/profile/you" }
      ]
    }
  }
}

Icons

Nav items can show a small glyph before their label — the same "reference" look as mkdocs-material. Set an icon in either place:

  • Front matter on a page (mkdocs-material compatible):

    ---
    title: Code blocks
    icon: code-braces
    ---
    
  • Nav config (icon on any nav entry, including sections):

    "nav": [
      { "title": "Setup", "icon": "rocket-launch", "children": [ /* … */ ] },
      { "path": "reference/code-blocks.md", "icon": "code-braces" }
    ]
    

An icon name is resolved in this order: a custom extra.social_icons override → a curated Material glyph → a brand glyph (social_icon) → a literal emoji/short string rendered as text (so icon: "🚀" works too). Fully-qualified names like material/rocket-launch also match — only the last path segment is used.

Curated icon names

Category Names
Callouts alert, alert-circle, information, plus-circle
Content code-braces, tab, table, format-size, grid, image, format-list-bulleted, sigma, math-integral, tooltip-text
Diagrams sitemap, graph
Sections book, book-open-variant, puzzle, cog, cog-outline, rocket, rocket-launch, home, flask, heart
Files & actions file-document, cloud-upload, download, content-save, magnify, tag, shield-check
Fun emoticon-happy, cursor-default-click

Need one that isn't listed? Add it via extra.social_icons — the override map feeds nav icons and badges too.

Add a row of text links to the footer meta area (below the copyright/attribution line) with extra.footer_links. Each entry has a name and a link. Use this for pages such as Disclaimers, Privacy, or Terms:

{
  "Netdocs": {
    "extra": {
      "footer_links": [
        { "name": "Disclaimers", "link": "/pages/disclaimers/" },
        { "name": "Privacy", "link": "/pages/privacy/" },
        { "name": "Source", "link": "https://github.com/you/repo" }
      ]
    }
  }
}

Links are resolved relative to the current page automatically: an internal path (e.g. /pages/disclaimers/) is prefixed with the correct base_url so it works from any depth, while absolute URLs (starting with http or //) are left as-is and open in a new tab.