Skip to content

Updating the Website

captf.io is one site, built from one source by Zensical: the landing page at /, the documentation at /docs/ and the blog at /blog/. This page covers the parts that are not documentation pages: the landing page, blog posts, the navigation, the header and footer, the generated files, and the theme. For the change workflow and the checks, see Contributing to the Docs.

The site has one look, dark only: there is no light theme or theme switch.

The landing page

The landing page is docs/index.md, rendered by the overrides/home.html template with overrides/assets/stylesheets/landing.css and overrides/assets/javascripts/landing.js. The Markdown file has no body:

  • the page’s copy (hero, sections, questions) is in home.html. It is mirrored word for word from the organization profile in captf-io/.github, so change the two together;
  • the feature grid is data, in the features: list of docs/index.md’s front matter, in display order. Each entry has a title, a body (which may use <code>), an href (a page of this site, or an external URL), the link text more, and an icon, one of the line icons home.html defines: globe, module, cluster, lock, pulse, shield, checks, chart, layers, link, inbox, scale, home, job, book, bolt, github, terraform, opentofu or kubernetes;
  • the anchors: list in the same front matter is what the navigation drawer shows on this page: one entry per landing section, with the id of the section’s heading in home.html, a title, a subtitle and a Lucide icon. Add an entry when you add a section to home.html.

Every claim on the landing page must be one the documentation backs up. The diagram and social card are docs/assets/landing/how-it-works.svg and og.jpg, also shared with the organization profile.

Blog posts

A post is a Markdown file in docs/blog/posts/, named <YYYY-MM-DD>-<slug>.md:

docs/blog/posts/2026-10-02-reference-cloud-modules.md
---
date: 2026-10-02
slug: reference-cloud-modules
title: Reference modules for five clouds
description: "Reference module sets for AWS, Google Cloud, Azure, OCI and OpenStack: what each creates, and their pre-release status."
authors:
  - maintainers
categories:
  - Modules
---

# Reference modules for five clouds

The opening paragraph, shown on the blog's index page.

<!-- more -->

The rest of the post.
  • The post’s URL is /blog/<YYYY>/<MM>/<DD>/<slug>/, from date and slug, so both are required, and neither changes once published.
  • Everything above <!-- more --> is the excerpt on the index and in the feeds. The marker is required: the build fails without it.
  • authors names entries in docs/blog/.authors.yml.
  • Use one category, an existing one where it fits: Documentation or Modules. Each category gets its own index page.
  • State only what the docs or the project’s history back up, and link to the docs for detail rather than repeating it.

After adding or retitling a post, run make gen: the navigation drawer lists the newest posts on blog pages from data it writes into zensical.toml. The RSS and JSON feeds, the archive and the post’s social card are built automatically.

The nav in zensical.toml is the whole site’s navigation. Its top-level entries are the documentation’s tabs, one per reader (see Where things go), plus the blog. A group without a page of its own shows as a heading; a group whose first entry is a README.md opens on that page.

Each top-level section has an entry in [project.extra.sections], keyed by its title: a Lucide icon, a subtitle of at most 34 characters and, when the title is long, a short label. The tab row and the drawer both use it, so a section looks the same in each.

The tab row is full

Nine tabs with icons fill the tab row at the narrowest desktop width, and on smaller windows the row steps down to titles, then short labels, then icons. A tenth tab, or a longer section title, needs the row checked at every size: run tools/mobile_audit.py.

A page’s own sidebar icon and subtitle come from tools/nav_meta.json; see Add a page.

The navigation drawer, on windows narrower than the sidebar layout, follows where the reader is: the docs sections on docs pages, the blog and its recent posts on blog pages, and the landing page’s sections on the home page. Its top row switches between Home, Docs and Blog.

Part Where to change it
Site name site_name in zensical.toml: the full name, for the browser title, link previews and feeds
Brand name extra.brand: the short name in the header, the drawer and the footer
Home, Docs and Blog links overrides/partials/header.html and, for the drawer, overrides/partials/nav.html
Footer columns, tagline, status [project.extra.footer]; a link href without :// is a page of this site
Social links [[project.extra.social]]
Pre-release banner The announce block in overrides/main.html; readers can dismiss it
Contract chip at the end of the tab row [project.extra.tabs]
Hover tooltips for acronyms includes/abbreviations.md, one *[ABBR]: Expansion line each
Logo and favicon docs/assets/brand/

Generated files

make gen rewrites three things. Never edit them by hand: the next run overwrites the change.

Generator Writes Reads
tools/gen_redirects.py A redirect page at each URL of the first edition of the docs (/docs/<page>.html), pointing at the page’s current URL Every page under docs/docs/ not listed in its SKIP
tools/gen_llms.py The llms.txt sections, between markers in zensical.toml The nav and each page’s description:
tools/gen_blog_nav.py The recent posts for the drawer, between markers in zensical.toml The posts’ front matter

Search, the RSS and JSON feeds, the sitemap, the social cards, llms.txt itself and a Markdown copy of every page are built with the site.

The theme

The site uses Zensical’s own theme, with templates under overrides/ replacing some of its parts:

Override What it changes
main.html Page metadata (Open Graph, structured data, feeds), the browser title, the banner
home.html The landing page
partials/header.html The header: brand, the Home, Docs and Blog links, search
partials/tabs.html, partials/tabs-item.html The docs tab row, its icons and the contract chip
partials/nav.html, partials/nav-item.html The sidebar and drawer: icons, subtitles, status badges, the drawer’s switch and lists
partials/footer.html The footer and the previous and next page links
partials/source-file.html The page’s dates and authors
partials/comments.html The share links under blog posts

The theme’s look is changed in overrides/assets/stylesheets/extra.css, in numbered sections with a comment on each saying what it is for. Colors are variables at its top.

Overrides are copies

Each overridden template except home.html starts as a copy of the theme’s own and names the Zensical version it was copied from. When the pinned Zensical version in pyproject.toml changes, compare each override with the new theme file and carry the theme’s changes across.

Two rules keep the theme working across page changes:

  • Clicking a link inside the site swaps the page’s content without reloading, and the header stays. So anything in the header that depends on the page (which site link is active, for example) is set from a script on each page change (overrides/assets/javascripts/sitenav.js), and its links are absolute URLs. A part of the page is only swapped in if the page being left had it too, so such parts are rendered on every page, empty where unused.
  • Do not put a title attribute on an element: the theme turns it into a tooltip that can stay on screen after a page change. Use aria-label.

Check a theme change with make build, then tools/mobile_audit.py, clicking through from the home page as well as loading pages directly.