ZeroSite
Source

Add a custom theme

Start with an additional stylesheet when you only need a brand color or a small layout adjustment. Replace the full HTML template only when the page structure needs to change; a custom template takes responsibility for the site controls and responsive behavior it replaces.

Add a CSS override

Put this file at site/public/assets/custom.css:

:root {
  --accent: #6d3ec1;
  --accent-soft: #efe8fb;
}

:root[data-theme="dark"] {
  --accent: #c1a4ff;
  --accent-soft: #302344;
}

/* Follow the user's system preference unless they selected a theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --accent: #c1a4ff;
    --accent-soft: #302344;
  }
}

Configure the public directory and stylesheet in site/config.json:

{
  "staticDir": "site/public",
  "styles": ["assets/custom.css"]
}

staticDir copies the contents of site/public to the output root. Stylesheet paths are relative to that output root and load after the built-in stylesheet. Using the built-in CSS variables and keeping explicit and system dark-mode values aligned preserves the theme switcher and contrast across color modes. Give custom assets distinct names so they do not collide with bundled assets.

Replace the HTML layout

For a complete layout change, create site/layout.html:

<!doctype html>
<html lang="{{.Config.Lang}}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="color-scheme" content="light dark">
  <title>{{.Page.Title}} · {{.Config.Title}}</title>
  {{if .Page.Description}}<meta name="description" content="{{.Page.Description}}">{{end}}
  {{if .Canonical}}<link rel="canonical" href="{{.Canonical}}">{{end}}
  {{if .NoIndex}}<meta name="robots" content="noindex">{{end}}
  <script src="{{.Theme}}"></script>
  <link rel="stylesheet" href="{{.CSS}}">
  {{range .Styles}}<link rel="stylesheet" href="{{.}}">{{end}}
</head>
<body class="layout-{{.Page.Layout}}">
  <header>
    <a href="{{.Home}}">{{.Config.Title}}</a>
    <button id="theme" type="button">Theme: system</button>
    {{if .Config.Repository}}<a href="{{.Config.Repository}}">Source</a>{{end}}
  </header>
  <details id="navigation" open>
    <summary>Navigation</summary>
    <nav aria-label="Documentation">
      {{range .Groups}}
        <section><h2>{{.Title}}</h2><ul>
          {{range .Pages}}<li><a href="{{call $.URL .Output}}">{{.NavTitle}}</a></li>{{end}}
        </ul></section>
      {{end}}
    </nav>
  </details>
  {{if .Search}}
    <label for="search">Search</label>
    <input id="search" type="search" autocomplete="off" aria-controls="search-results">
    <div id="search-results" role="region" aria-live="polite" hidden></div>
  {{end}}
  <main id="main">
    <article>{{.Body}}</article>
    {{if .TOC}}<nav aria-label="On this page"><ul>{{range .TOC}}
      <li class="level-{{.Level}}"><a href="#{{.ID}}">{{.Title}}</a></li>
    {{end}}</ul></nav>{{end}}
    <nav aria-label="Adjacent pages">
      {{if .Previous}}<a href="{{call .URL .Previous.Output}}">← {{.Previous.NavTitle}}</a>{{end}}
      {{if .Next}}<a href="{{call .URL .Next.Output}}">{{.Next.NavTitle}} →</a>{{end}}
    </nav>
  </main>
  {{if .Search}}<script src="{{.Search}}"></script>{{end}}
  <script src="{{.Script}}" defer></script>
  {{range .Scripts}}<script src="{{.}}" defer></script>{{end}}
</body>
</html>

Set "template": "site/layout.html" in the configuration. The template uses Go's html/template; .Body contains trusted rendered Markdown, while metadata is escaped normally. Use {{call .URL "index.html"}} to build a path relative to the current generated page. The .Theme, .CSS, .Search, .Script, and .Styles/.Scripts values are escaped paths relative to the current output page, so use those values instead of hard-coding root-relative asset paths.

A custom layout replaces the default navigation and other markup. Keep the #navigation details element, theme script, stylesheet, theme button, search elements, and client script hooks shown above if those features are enabled. The built-in script expects #navigation and #theme; search also expects #search-results. The sample navigation lists top-level groups; extend it to render nested .Children if the configuration uses child sections. The page body stays in .Body; do not render Markdown a second time. Verify the finished site at desktop and mobile widths and in light, dark, and system color modes.