Changelog

Neko is under active development and follows a calendar-versioning (vYY.M) scheme. The most recent release is listed first.

v26.7

Fixes

Fixed

Tesserae live samples that embed a <script> tag

A tesserae sample whose code contains the literal </script> — for example a Sandbox sample whose srcdoc HTML embeds its own <script> block — no longer breaks the live preview. The compiled sample is inlined into the preview page inside a <script> element, and that inner </script> was closing the tag early, dumping the rest of the compiled app into the page as visible text so the sample never ran. The closing sequence is now backslash-escaped (<\/script) when inlined, which is identical to the browser's JavaScript parser but invisible to the HTML parser.

Improvements

Changed

Dependencies updated; CLI migrated to System.CommandLine 2.0

Neko's package dependencies were refreshed (Markdig, YamlDotNet, Mosaik.Core, the Roslyn code-analysis packages, h5.Compiler.Service, and more). As part of the update the CLI was migrated from the old System.CommandLine beta API to the 2.0 GA release: options are now constructed with a primary name plus aliases and a DefaultValueFactory, handlers use SetAction over the ParseResult, and the root command is invoked via rootCommand.Parse(args).InvokeAsync(). Every command and flag behaves exactly as before. Two transitive dependencies are pinned deliberately: ZLogger stays on 1.x because h5.Compiler.Service calls its 1.x logging API when compiling Tesserae samples, and SixLabors.ImageSharp (test-only) stays on 3.1.x because 4.0 requires a paid license enforced at build time.

v26.6

New components

New

API endpoint reference

A ```endpoint fenced block renders a REST endpoint in the Microsoft Learn / Swagger style: a colour-coded HTTP method badge, the monospace path, a summary, and an aligned details grid for Auth, Body, Returns, and any other Label: value rows. The first body line carries the method and path (POST /api/login/create) — so paths with {placeholders} stay intact — and the rest holds the summary and detail rows, all rendered as inline Markdown so `code`, bold, and links work. Each block gets a stable anchor (e.g. #post-api-login-create) and a permalink icon, and ships its own styling so it matches in light and dark mode. Documented at components/endpoint.md, with an endpoint skill and a neko-endpoint snippet.

New

Version badge

A compact, theme-agnostic package pill — a muted label, a bold monospace version, an optional link and a one-click copy button: Curiosity.FrontEndv26.6.1753. Place several next to each other and they flow inline, wrapping onto the same line: Tesseraev2026.6.67285 Curiosity.CLIv26.6.1718. Documented at components/version-badge.md, with a version-badge skill and a neko-version-badge snippet.

New

Link card

A ```links fenced block renders a titled card of links — each row is a labelled link on the left and a version pill on the right, one row per line as text | url | version:

Documented at components/link-card.md, with a link-card skill and a neko-link-card snippet.

New

Folder-based changelogs

Changelogs are now built from a folder instead of a single hand-maintained file. Drop a folder anywhere in your project, mark it with changelog: true in its index.yml (plus a title/description), and add one Markdown file per release named after its version (v26.6.md, 1.2.0.md, …). Neko parses each file name as a version, sorts the entries newest-first, and renders a single timeline page at the folder URL. The folder collapses to one sidebar entry, the version files are not emitted as standalone pages, and the aggregated page is what gets indexed for search and listed in the sitemap. The Neko changelog you are reading now uses this model.

New

Quiz component

Added a ```quiz fenced block whose YAML body defines a self-scoring multiple-choice comprehension check. Questions render radio buttons for a single correct answer or checkboxes for multiple answers: [..], with an optional per-question explain. Check answers grades client-side, highlights correct/incorrect options, and reveals explanations; score and answered-state persist per-browser in localStorage (no backend). Documented at components/quiz.md, with a quiz skill and a neko-quiz snippet.

New

Contextual pivot navigation

Any top-navigation link group that has items now surfaces those items as a row of tabs in a secondary bar directly below the header. The bar appears only while the reader is inside that section — a page is "inside" when its URL matches one of the group's item links (or sits below it) — and the tab for the current page is highlighted. This is the default for every dropdown group; no extra configuration is required. When several groups define items, the one whose item most specifically matches the current page wins, so sibling sections sharing a path prefix do not collide. The flyout dropdown still works on top of it, so a group acts as both a header dropdown for jumping between sections and an in-section pivot. Documented at configuration/core/project.md#items.

Improvements

New

Blog-mode posts now end with two new pieces of marketing chrome, both rendered by the engine so they appear on every post without per-page markup. A Read next section shows a grid of related-post cards (the same card as the index) under a configurable heading; each post picks its own related articles with a new readNext: frontmatter key (a list of post links — file path, URL, or slug), and any unfilled slots auto-fill with the most recent other posts, so a post needs no setup to get a sensible section. Below it, a CTA band — a centred headline, an optional line, and a row of buttons — renders above the footer on posts and the index. Both are configured under the blog: block: blog.readNext (title, description, count, enabled) and blog.cta (title, description, actions); cta.actions defaults to the top-level actions: so the demo/sales pills carry over automatically. Documented at configuration/core/project.md#blog, configuration/page.md#readnext, and the neko-yml / frontmatter skills.

New

Configurable blog index hero (pill + title)

The blog index page can now show a hero above the post grid — a small rounded pill, the large page title, and an optional lead paragraph — configured with a new blog: block in neko.yml (pill, title, description). It renders only on the blog index in mode: blog, and takes precedence over the page-title heading below — falling back to it when no blog: fields are set. The fields are inherited per-field by multi-repo child sites. Matching curiosity.ai/resources/blog, the pill renders in a plain system sans-serif at 12px while the title inherits the site body font (theme.font) at 30.4px / weight 500, both picking up blog mode's antialiased smoothing. The post-card grid also gets a roomier 44px gutter between cards. Documented at configuration/core/project.md#blog and the neko-yml skill.

Improved

Blog-mode header has a mobile menu and never squeezes the logo

On narrow viewports the blog-mode marketing header now collapses into a hamburger menu instead of cramming the nav links and CTA pills next to the logo (where the links would overflow under the CTA pills around ~900px). Below the lg breakpoint (1024px) the desktop nav is hidden and a hamburger button appears on the right; tapping it opens a full-width drop-down holding the nav links (dropdown groups flatten to a labelled section with their items) and the actions pills stacked full-width, with body scroll locked while it's open. The logo is now shrink-0, so a crowded header can never compress the wordmark the way it did before. The desktop layout (≥1024px) is unchanged — the links, dropdowns, and CTA pills render exactly as before. docs mode keeps its own sidebar-driven mobile menu.

Improved

Blog index & filters view polish

A round of refinements to the mode: blog index and its tag-filter view: the index now shows its page title above the search + filter row; the active tag chip fills with the blog ink (#1f1f1f) over the page background instead of the docs-mode blue, tracking the palette in dark mode; and the full-bleed footer is pinned to the bottom of short pages, so filtering down to a few cards no longer leaves a large band of page background below the footer.

Improved

Wider blog index for the card grid + search

The blog index page now uses a roomier max-w-6xl (~1152px) content column instead of the max-w-4xl reading width, giving the post-card grid and the search box more breathing room — matching the wider curiosity.ai/resources/blog layout. Individual blog post pages keep the comfortable max-w-4xl reading column; the Read next / CTA outro below a post matches that same body width.

Improved

Animated arrow on blog post cards

Each blog post card carries a small right-pointing arrow that rotates 45° counter-clockwise on hover (a 300ms ease-out transform), swinging from → to ↗ — the curiosity.ai/resources/blog interaction. It pairs with the existing cover-image zoom for the same lift-on-hover feel.

Improved

Blog covers always reserve a consistent slot

Blog post cards and inline-search result rows now render a fixed cover slot that always takes the same space, so rows and the card grid stay visually consistent whether or not a post has a cover:. A real cover fades in over a neutral picture placeholder; a missing or broken cover hides itself and leaves the placeholder in its place rather than collapsing the row or showing a torn-image glyph.

Improved

Blog-mode search is scoped to posts, with tag filtering

The in-content search on a mode: blog index now surfaces only blog posts — other pages (about, contact, …) and the blog landing page itself stay out of the results — and drops the breadcrumb trail, which was always just "blog". A row of tag chips sits under the search bar: click one to filter the post grid in place by that tag. With an empty query the posts stay as the usual cards rather than highlighted search rows, and the All chip clears the filter.

Improved

Blog-mode footer matches the curiosity.ai marketing footer

The full-width blog-mode footer (rendered when footer.columns / social / badges are set) now lines up pixel-for-pixel with the curiosity.ai footer. The brand block and link groups share the row as equal-width columns in a 32px flex row instead of an uneven grid, the top padding and the gap before the copyright bar match the marketing site's vertical rhythm, column titles render in the #f1f1f1 ink with capitalize, link text uses the #f1f1f1 resting / white-on-hover pair, and the tagline, badge sub-labels, and copyright line pick up the #b5bdc5 ink at the right sizes. Light-only blog mode is unchanged otherwise.

New

`footer.copyrightIcon` — configurable footer copyright icon

The blog-mode mega-footer copyright line takes an optional footer.copyrightIcon in neko.yml — a UIcon name (e.g. cookie) or an image/SVG path (/assets/img/cookie.svg) served from the site's own assets. It defaults to the built-in cookie glyph, so no site-specific artwork is baked into the engine.

Improved

Blog-mode header + footer match curiosity.ai to the pixel

A pass to bring blog mode pixel-for-pixel against curiosity.ai, verified region by region against the live site. Footer trust badges are now circular (rounded-full, object-cover) instead of square, with the live vertical rhythm (~78px between badges), top-aligned icon/title (items-start), #f1f1f1 titles, and the live title→sub-label and icon→text gaps. Footer link columns adopt the live 37px row pitch and the #f1f1f1 link colour. The outlined CTA pill in the header (e.g. Talk to Sales) now draws its border with an inset box-shadow rather than a border, so it keeps the exact same box size as the filled pill — matching curiosity.ai, whose outlined button has no layout-affecting border. The pre-copyright gap was retuned so the panel keeps its 526px height throughout.

Improved

Blog-mode header is a pinned overlay topbar

The blog-mode header is pinned as an overlay bar with the page content scrolling underneath it (rather than an opaque row that reserves space above a separate scroll pane). It is filled with the base page colour (#f1f1f1) so it reads as a solid topbar instead of letting scrolled content show through, and the scroll pane reserves top padding equal to the header height so nothing starts hidden. Docs mode is unchanged.

Improved

Mermaid diagrams follow the site palette

Mermaid diagrams are now rendered with your site's colours instead of Mermaid's stock grey/lavender. Neko feeds the resolved theme primary ramp and accent ramp into Mermaid's base theme as themeVariables, so node fills, borders, lines, and text match the rest of the site — and it renders a light and a dark variant of every diagram, so the colours adapt automatically in dark mode. No configuration is needed; a diagram that sets its own theme in an %%{init}%% directive is still rendered verbatim so explicit overrides keep working. Documented at components/mermaid.md.

New

`neko watch --live` — live preview without the editor

neko watch gained a --live flag (alias --no-editor) that keeps live-reload but turns off the in-browser editing chrome. The header edit button, the sidebar pencil icons, the drag-to-reorder handles, and the Monaco editor modal are all omitted — so localhost matches exactly what a neko build output ships, while the page still rebuilds and refreshes on every file change. Useful for a read-only local preview or for demoing the site without exposing the edit and reorder controls. Documented at guides/live-editing.md.

New

Blog mode — the marketing-site look

A new top-level mode: blog switches the site chrome from the documentation look to the curiosity.ai marketing look: a borderless header with the logo used on its own as a wordmark (no more duplicated branding title next to it), call-to-action buttons, and a full-width footer. The page/header/CTA palette comes from theme.base; blog mode is light-only by default, and defining a theme.dark palette opts into dark mode and brings the theme toggle back. Header CTAs are configured with a new actions: list (text / link / variant: primary|outline), and the footer grows columns, social, badges, logo and tagline fields that render as a dark, rounded marketing footer in blog mode. In blog mode the site search also moves out of the header to a bar at the top of the post list (still reachable anywhere with ⌘K). mode defaults to docs, so existing sites are unchanged, and it is inherited by multi-repo child sites. Documented under configuration/core/project (mode, actions, footer) and the neko-yml skill.

Improved

Blog header and footer match curiosity.ai

Blog mode's chrome now mirrors the curiosity.ai blog. The header sits in a taller (80px) bar with the logo rendered as a compact ~21px wordmark and the content inset from the capped row so it lines up over the footer columns. It clusters the nav links immediately to the right of the logo (instead of centring them between the logo and the CTA buttons), at 14px / weight 500 with 22px gaps, and — like curiosity.ai — the header chrome (nav + CTA pills) can render in Inter even when theme.font sets a brand font for the content and footer. Neko no longer pins that itself; the site keeps the header on its own face with a header { font-family: … } rule in the stylesheet it loads via theme.font.url. The actions pills are pushed to the far right. The marketing footer is now edge-to-edge: the dark rounded panel spans the full content pane to both screen edges (blog mode drops the reserved scrollbar gutter so no sliver of page shows down the right edge), while its inner content stays centred at layout.maxWidth, laid out as equal columns (brand + link groups) with 15px/600 titles and near-white 15px links. Footer trust badges and social links accept image icons — an icon: that points at a logo file (/assets/…svg|png|webp) renders as an image (a ~28px badge, or a bare ~18px social glyph with no chip) instead of a UIcon glyph — and the copyright line carries a small cookie glyph, matching curiosity.ai. This is achieved by not capping the content row in blog mode — the header and post grid keep their own centring — so the footer can break out to the pane edges. docs mode is unchanged.

Improved

Inline blog search that surfaces tags

In blog mode the search bar at the top of the post list now searches inline instead of opening the ⌘K modal. Typing filters the search index live and renders the matching posts straight below the box — in place of the post grid — with keyboard navigation (↑/↓/↵) and Esc to clear. Each result shows the post's cover image as a thumbnail and the page's tags as chips, with any tag matching the query highlighted, so a search like release surfaces every post tagged that way at a glance. Tags and the cover are exported as discrete tags / cover fields in search.json (tags stay full-text searchable too), and both the thumbnail and chips also appear in the docs-mode ⌘K modal. docs mode keeps its modal search. Documented at configuration/core/project.md#mode.

Improved

Blog post cards show their tags

Each post card on a blog-mode index now renders its tags as visible pills above the author/date row (using the inverted blog palette), so the bucket a post belongs to is clear at a glance — and they line up with the tag-filter chips at the top of the list. Documented at configuration/core/project.md#mode.

Improved

Blog post cards match curiosity.ai

The blog index post cards now mirror the curiosity.ai resources grid. Each post is a single dark, rounded tile (the base palette's ink colour with bg-coloured text, so it tracks the theme) at a fixed height with the cover image faded behind the content and gently zooming on hover. The title sits top-left with an up-right arrow in the top-right corner, and the author and date share a bottom row split by a hairline rule — author on the left, date on the right. This replaces the previous light card with the image on top and a separate description block. docs mode index pages get the same look, falling back to the curiosity defaults (#1f1f1f / #f1f1f1) where the blog palette isn't defined.

New

Configurable base font

A new theme.font setting picks the site's base typeface. Neko itself pins no font — it ships no default web font and emits no font-family rule unless you opt in, so with theme.font unset the browser/system sans-serif stack applies. Set theme.font.family (and an optional theme.font.url stylesheet — a Google Fonts link, a CDN, or a self-hosted /assets/….css) to render the whole site — header, content, and footer — in your brand font. For example, family: Plus Jakarta Sans reproduces the curiosity.ai blog typography. The family is used as the base font-family with a , sans-serif fallback; a full comma-separated stack is accepted verbatim. theme.font is inherited by multi-repo child sites. Documented at configuration/core/project.md#theme-font and the neko-yml skill.

New

`neko sync-api-docs` — API pages from source

A new neko sync-api-docs command regenerates csharp-docs blocks on API-reference pages from the public surface of real source code. Pages mark a block with <!-- api:source start repo="…" file="…" type="…" -->; Neko reads the named type(s), keeps only public/protected members with their XML doc comments, strips method bodies and every private/internal member, and rewrites the block — so documented signatures never drift from the code that ships, and no implementation is vendored into the docs. Source roots come from apiDocs.roots in the root neko.yml only — nested sub-project configs aren't consulted and there's no CLI override, env-var, or hard-coded path fallback, so roots are declared once at the root. It runs by default before build and watch (disable with --no-api-sync); when a source root isn't available the block is left untouched so the build still succeeds against the committed snapshot. Documented at guides/cli.md.

Improved

`sync-api-docs` logs synced doc comments

Each refreshed api:source block now prints a line naming the source (repo:file), the page it wrote, and how many XML doc comments it synced — so a build/watch/sync-api-docs run shows what was pulled from source, alongside the existing skip / not-found notices.

Changed

Tailwind compiled at build time — no CDN

Neko now ships a static assets/tailwind.css generated at build time by a pure-C# port of Tailwind's utility generator. This replaces the Tailwind Play CDN (cdn.tailwindcss.com), which compiled CSS in the browser on every page load and caused a flash of unstyled content on navigation — most visible in dark mode, where the dark: utilities did not exist until the CDN script ran. The generator scans each (sub-)site's emitted HTML/JS for the classes actually used and emits only those, plus the Preflight reset and the prose typography styles, resolving the per-site primary/accent palette. No Node, no npm, no CDN, and no downloaded binary — just .NET, honouring Neko's embedded-resources rule. Output parity with the official Tailwind v3.4 CLI is enforced by tests.

Changed

Navigation icons are now opt-in

Icons in the top navigation are hidden by default and re-enabled per context under a new nav: block: headerIcons (top-bar links and dropdown triggers), dropdownIcons (items inside flyout menus), and pivotIcons (the pivot tab bar). Each defaults to false; the icon: values stay in neko.yml, so a single flag toggles them on. When dropdown icons are off, the icon column is removed so item text aligns left. This is independent of nav.icons.mode, which still governs the left sidebar. Documented at configuration/core/project.md#nav.

New

`neko check-links` command

A new neko check-links command builds your project into a throwaway folder and verifies every link in the generated site. Internal page links are resolved against the files actually written to disk — including clean, extension-less URLs and folder index pages — asset references are checked for existence, and #fragment anchors are matched against the id/name attributes of the target page. External http(s) links are skipped by default; pass --external to probe them over the network, and --no-anchors to skip fragment checking. Broken links are grouped by target — so a navbar or footer link broken on every page is reported once with an occurrence count, not once per page — and the command exits non-zero when anything is broken, so it can gate a CI pipeline. Documented at guides/cli.md.

Improved

`check-links`: redirect reporting + fewer false positives

neko check-links gained a --redirects flag that flags external links which still work but only resolve after an HTTP redirect — handy for keeping links on their canonical URL. Redirects are listed in their own section and are advisory (they never change the exit code). External probing now also falls back to GET when a server answers HEAD with a non-success status, so hosts that reject HEAD (such as nuget.org) are no longer reported as false positives. Documented at guides/cli.md.

Improved

Faster builds: working Tesserae cache

The compiled output of ```tesserae blocks is now cached across builds. Previously the on-disk cache was only consulted when the output folder already held assets, but every build starts by wiping that folder — so each build re-ran the (multi-second) H5 transpile from scratch. The compiled shared assets are now persisted alongside the cache manifest and restored into the freshly wiped output on a cache hit, so unchanged code blocks compile once and are reused on every later build. Failed compiles are no longer cached, so a transient error no longer sticks.

Improved

Parallel Tesserae compilation

```tesserae samples now compile in parallel in a cache-warming pass before the page render, instead of one at a time inline. On a cold build every sample across the whole site is compiled concurrently (up to Environment.ProcessorCount, override with tesserae.maxParallelism in neko.yml); the render pass then reads each one from cache. An in-memory result cache means neko watch rebuilds reuse compiled samples for the life of the process without touching disk.

New

`neko gen-tesserae-heights`: sized previews, no layout shift

A new neko gen-tesserae-heights command sizes ```tesserae live previews so they no longer reflow the page as they render. Previews use a fixed 400px placeholder iframe, so short samples (e.g. a tags input) left dead space and tall ones (e.g. a full sidebar) were clipped behind an inner scrollbar. The command compiles every sample, measures its rendered height with a headless browser (snapframe/Playwright), and bakes a height=NNN token into each fence (```tesserae sample.js height=360). A normal build/watch then reads that token and reserves the right space up front — no browser runs during a build, so there's no layout shift and no browser dependency in the build pipeline. The run is incremental and resumable: samples that already have a height= token are skipped (so re-runs only measure new samples), each file is saved the moment its sample is measured, and the measure phase reports per-sample timing and progress. Pass --force to recompute everything. Commit the updated Markdown so the heights ship with your docs; tune the measurement viewport with tesserae.measureWidth. Documented at guides/cli.md and components/tesserae.md.

Improved

Stable, network-free Tesserae version

The resolved Tesserae NuGet version — part of the sample cache key — is now resolved once and recorded on disk (no expiry), then reused verbatim on every later build and neko watch restart. A fresh start makes no NuGet round-trip before serving cached samples, and a new Tesserae release no longer silently invalidates every cached sample. Pin an exact version with tesserae.version in neko.yml (or delete the .neko-cache/ folder) to move to a different version. The shared H5 runtime (h5.js, css, …) is also stored once per version rather than once per sample.

Changed

Build artifacts moved into the project, not OS temp

Neko no longer writes build artifacts to the OS temp directory. The Tesserae compile cache (compiled samples, shared runtime, and the recorded Tesserae version) now lives in a .neko-cache/ folder in the project root, the neko check-links throwaway build runs there too, and a single-project build with no --output now defaults to the project's .neko folder (honouring the output: key) instead of temp — matching multi-repo mode. Add .neko-cache/ to your .gitignore.

Improved

Parallel page generation

The HTML-generation pass now renders pages in parallel across all CPU cores instead of one at a time. Output is unchanged: the search index is still assembled in a stable, deterministic order, so search.json and every page are byte-for-byte identical regardless of how many cores run the build.

Improved

Incremental single-page rebuilds in `neko watch`

Editing a Markdown page under neko watch (including saving from the built-in editor) now regenerates just that page instead of rebuilding the whole site. The watcher reuses the navigation, sidebar, backlink, and search state from the last full build, re-renders only the changed file, and refreshes search.json and the Tailwind stylesheet. A change falls back to a full rebuild when it is structural — a new or deleted file, a .yml/config edit, or a frontmatter change that affects other pages (title, label, icon, order, visibility, layout, tags, date, password) or a change to the page's outgoing links (which feed other pages' backlinks). In a multi-project site a change now rebuilds only the owning sub-project, not every sub-site.

Improved

Redesigned changelog timeline

The changelog timeline now renders as sections with entries. Each release's section headings (#) become labelled headers with an icon, and each entry — authored as a ::: change {badge="…" title="…"} block — renders its badge in a left column, vertically aligned, with the title and description next to it. Every version header is sticky and links to the closest NuGet package for that month, so the version you are reading stays pinned until the next release scrolls into view.

Improved

Collapsible sidebar sections

Top-level sidebar sections are now collapsible <details> groups with a chevron, expanded by default. The section containing the current page is expanded on load, and typing in the filter box expands every section. Previously the top-level sections were static headers that could not be collapsed.

Improved

Sidebar remembers what you collapse

The sidebar now stays put as you move between pages. Each collapsible section's open/closed state is remembered per browser (in localStorage, the same way scroll position already is) and restored on the next page — so a section you collapse stays collapsed while you navigate, and only the section containing the page you land on is force-opened. The disclosure chevron now points right when collapsed and rotates down when expanded, Notion-style, and that rotation only animates on a real click — it snaps silently into place while the saved/active state is being restored on load.

Improved

Smooth password-decryption reveal

On a protected page the decrypted content (and the on-this-page table of contents and sidebar entries it brings) fades in via the View Transition API (document.startViewTransition) instead of popping in, so unlocking — or navigating between protected pages with the session key cached — is a clean cross-fade rather than a flash. The sidebar keeps its scroll position and open sections across navigations because that state is restored from localStorage before the first paint. Ordinary navigation is a normal page load (no whole-page cross-document transition), and prefers-reduced-motion disables the fade entirely.

Improved

Protected pages: unlock once, navigate without a flash

Navigating between password-protected pages no longer re-prompts or flashes the password form. The key derived from the password (PBKDF2, 100,000 iterations — deliberately slow) is now cached in sessionStorage on the first unlock, and every page sharing that password reuses it, so later pages decrypt straight away instead of re-deriving the key on each navigation. The salt is derived deterministically from the password (so the key is identical, and cacheable, across the site) while a fresh random nonce per page keeps AES-GCM confidential. The prompt is now hidden by default and revealed only when there is no usable cached key, so it never appears mid-navigation. A hard refresh or a new tab still asks for the password once. Documented at guides/password-protection.md.

Improved

Protected pages leak nothing and render in one step

A password-protected page now encrypts its whole content column — breadcrumbs, body, previous/next links, and backlinks — as a single payload, instead of only the article body. The on-this-page table of contents ships no heading text (it is rebuilt in the browser from the decrypted content), and the <title> is masked to the site name until unlock. So a locked page no longer leaks its headings, title, prev/next, or backlinks in the page source, and everything is revealed together in a single step when unlocked — no more content-then-sidebar staged render. The TOC rail keeps its width while locked, so unlocking doesn't shift the layout. (The left navigation sidebar still lists page URLs site-wide, with labels masked as Protected.) Documented at guides/password-protection.md.

Changed

Sidebar icons are now opt-in

The left-sidebar navigation no longer renders icons by default. nav.icons.mode now defaults to none, which hides every navigation icon and reclaims the space they reserved, for a tighter, text-first sidebar. To bring icons back, set the mode explicitly in neko.yml:

nav:
  icons:
    mode: all   # or: folders | pages | top

all restores the previous behaviour; folders/pages limit icons to folder or page entries; top shows them only on top-level items. Documented at configuration/core/project.md.

New

Content width cap on wide screens

The layout no longer stretches edge-to-edge on wide monitors. The header content, the pivot tabs, and the sidebar + content + TOC row are now capped at a shared maximum width and centred, so the page stops expanding past a comfortable reading width instead of pinning the sidebars to the far edges. The cap is set with a new layout.maxWidth key (default screen-2xl, 1536px); it accepts a Tailwind token (7xl), a full max-w-[…] class, or a raw CSS length, and full/none restores the previous full-bleed behaviour. Documented at configuration/core/project.md#layout.

Improved

C# docs follow the Microsoft Learn layout

The csharp-docs block now renders an API reference in the Microsoft Learn / DocFX style. Below the sticky type header it adds a Definition block listing the type's Namespace (from the enclosing namespace declaration), its Inheritance chain (Base → Type), and the interfaces it Implements. Each member group (Constructors, Properties, Methods, …) now opens with a summary table — a Name → anchor link plus the member's Description — sitting above the existing detailed member entries, so a reader can scan a type's surface at a glance before diving into the per-member docs. Documented at components/csharp-docs.md.

Improved

C# docs group method overloads (Microsoft Learn style)

The csharp-docs block now renders overloads — members that share a name but differ in signature — the way Microsoft Learn / DocFX does, instead of repeating the method once per overload. The set gets one header and a stable permalink anchor (the plain name, e.g. #Client.Connect); an optional shared intro from the standard <overloads> XML tag; an Overloads table that lists each signature (disambiguated by its parameter types) with its own summary; and then one complete, self-contained section per overload — a typed signature heading and anchor, the signature, its summary, and its own typed Parameters, Returns, Exceptions, and Remarks. Parameters are documented in full inside each overload so every one reads on its own, and the type's member summary table lists the set once. Documented at components/csharp-docs.md.

Improved

C# docs render inline doc tags and examples

csharp-docs now renders the inline XML-doc tags inside <summary>, <param>, <returns>, and <remarks> instead of dropping them: <c> and <see cref="…"/> / <see langword="…"/> become inline code, as do <paramref> / <typeparamref>, and <para> becomes a paragraph break. An <example> block now renders under an Examples heading with any nested <code> shown as a code box. This means doc comments copied from real source — which lean heavily on <see cref>, <c>, and <example> — render with their inline code and cross-references intact rather than as flattened, escaped text. Documented at components/csharp-docs.md.

New

Built-in styling for hand-authored API reference pages

The Microsoft Learn look is no longer limited to csharp-docs and endpoint blocks. A set of opt-in .api-* helper classes — authored as plain containers (:::api-definition, :::api-members, :::api-member, :::api-applies-to) — now ships its own styling in every site's <head>, so you can hand-write a class/command reference in Markdown and get the same Definition header, member Name | Description tables, parameter grids, and "Applies to" footer. Pages that use these helpers (or a csharp-docs type) automatically widen the content column to 72rem for the wider member tables.

Fixes

Fixed

Blog post covers resolve and tolerate spaces in the path

A blog post's cover: (and authorImage:) front-matter image is emitted straight into the page, so unlike body images it was never resolved against the asset tree. A relative cover: assets/… therefore resolved against the post URL (/blog/<post>/assets/…) and 404'd on single post pages, and a path through a folder with a space — e.g. assets/blog covers/2026-05.webp — broke for lack of URL-encoding. Front-matter image paths are now resolved to a root-relative /assets/… the same way body images are, and the emitted src percent-encodes spaces, so covers under folders like blog covers/ load on both the post page and the index grid. The single post cover also now uses a wide ~3:1 banner crop (was 16:9) to match the curiosity.ai blog.

Fixed

No console error in blog mode without a dark theme

The theme-switch script always wired a click listener onto the #theme-toggle button, but that button is omitted in mode: blog when no theme.dark palette is defined (blog mode is light-only by default). On those pages document.getElementById('theme-toggle') returned null, so the script threw TypeError: can't access property "addEventListener", themeToggleBtn is null on every page load — aborting the rest of the inline script. The listener is now attached only when the toggle button is present, so light-only blog sites load cleanly.

Fixed

Blog header text weight + dropdown chevrons match curiosity.ai

Two header details now line up with curiosity.ai. (1) Font smoothing. The marketing site renders the whole page with -webkit-font-smoothing: antialiased / -moz-osx-font-smoothing: grayscale; blog mode emitted neither, so the same Inter Display at weight 500 painted heavier on macOS/Retina and the nav links and CTA pills (e.g. Book a Demo) read bolder than the live site. Blog mode now pins the smoothing, so the chrome, body, and footer match. (2) Dropdown chevrons. The carets next to Solutions / Developers / Resources drew in the near-black nav ink (currentColor); curiosity.ai renders them in a muted grey (~#b6bec5). They are now faded with opacity-25 — ~25% of the #1f1f1f ink over the #f1f1f1 page reads as that grey and stays theme-agnostic — so the chevrons no longer look heavier and darker than the live site. docs mode is unchanged.

Fixed

Self-hosted blog header renders in its own font, not the system fallback

Completes the self-hosted-font fix below. Even after blog mode stopped pulling the Inter CDN copy when theme.font.url is set, it still emitted its own inline header { font-family: 'Inter var' } rule after the site's stylesheet — so that rule won by source order, yet a self-hosting site (curiosity.ai's blog maps the header to Inter Display Medium with header { font-family: 'InterDisplay', 'Inter' }) never defines 'Inter var', so the header silently fell back to the system sans-serif instead of the cut the site ships. The header chrome (nav links + CTA pills) therefore rendered in the wrong typeface, not pixel-matching curiosity.ai. Blog mode now only pins the header to Inter when Neko is the one providing Inter (the CDN copy); when theme.font.url is set the engine defers to the site's own header { font-family: … } rule, so the nav and CTA buttons render in the exact face the site self-hosts (Inter Display for curiosity.ai).

Fixed

Self-hosted blog fonts are no longer overridden by the Inter CDN

In blog mode the marketing header chrome is pinned to Inter so a brand body font never bleeds into the nav. To guarantee Inter is available, Neko also pulled it from the rsms.me CDN — but it loaded that stylesheet after the site's own theme.font.url, so a site that self-hosts Inter (curiosity.ai's blog maps 'Inter'/'Inter var' to Inter Display Medium for pixel-exact chrome) had its mapping silently overridden by the regular Inter text cut from the CDN, and gained an external font dependency it didn't ask for. Blog mode now skips the CDN copy whenever theme.font.url is set — the self-hosted stylesheet owns Inter, so the header renders in the exact cut the site ships. Sites that set a brand font without a stylesheet URL still fall back to the CDN Inter for the chrome.

Fixed

Protected pages no longer flash an empty screen on navigation

Navigating between password-protected pages (e.g. an academy) briefly showed a blank page with no content before the real page appeared. Navigation is a full page load, and a protected page ships its body encrypted — so the page paints empty and password.js only decrypts and injects the content a frame or more later (Web Crypto decryption is asynchronous), leaving an empty gap. A protected page now restores its body synchronously, before the first paint: once decrypted, a page's plaintext is cached in sessionStorage (alongside the session key that's already cached there, and cleared with the tab), and a tiny inline script injects it on the next visit before the browser paints — so a revisited page renders content on the first frame. Internal links are also decrypted ahead of time on hover/focus, so clicking through to a not-yet-seen page lands on rendered content too. The now-removed cross-document view-transition skip was dead code: Neko navigations are plain full page loads, so that transition never ran.

Fixed

Password-protected pages keep their styling

The static Tailwind stylesheet is generated by scanning the emitted .html/.js files for the classes actually used — but a password-protected page emits only an encrypted blob, so its real markup (and every class on it) was invisible to the scanner. Utilities used only on protected pages therefore got no CSS rule, and the decrypted content rendered unstyled — most visibly in dark mode, where dark: variants like dark:bg-primary-900/20 (alert backgrounds), dark:bg-gray-800 (code-block headers), and dark:text-white (step headings) were missing, leaving light boxes and washed-out text on the dark page. The build now harvests the class tokens from each protected page's plaintext before it is encrypted and feeds them to the stylesheet generator, so protected pages get the same complete CSS as public ones. Only class tokens are collected — no decrypted content is ever written to disk.

Fixed

csharp-docs Definition labels align with their values

The csharp-docs Definition block (Namespace, Inheritance, Implements) laid out its label/value rows in a two-column grid, but the page renders inside the prose typography context, which adds a top margin to every <dt>. That margin pushed each label (Namespace) down within its grid cell while the value (Curiosity.Library) stayed at the top, so the two halves of every row sat visibly out of line. The label cells now reset their margin like the value cells already did, so each row aligns on a single baseline.

Fixed

Image-path icons on badges and buttons no longer warn

A [!badge] or [!button] whose icon= points at an image file or URL (e.g. icon="/assets/logo.svg") now renders that image as an inline <img>, the same as [!file] already did. Previously the path was passed to the UIcons catalog lookup, which both emitted a spurious Warning: Invalid icon '…' — not found in UIcons catalog. at build time and rendered a broken <i class="fi fi-rr-…"> element. Image paths and raw <svg> are also exempted from the catalog warning at its source, so no caller can trip it.

Fixed

`neko watch` defaults to port 5050 on macOS

On macOS the dev server defaulted to port 5000, which the system AirPlay Receiver (ControlCenter) also binds — it answers requests with HTTP 403 Forbidden. Neko still bound the loopback address and logged a successful start, but the browser's request to localhost:5000 could land on AirPlay instead and return a 403. neko watch now defaults to port 5050 on macOS (5000 elsewhere) to sidestep the collision; override either with --port/-p.

Fixed

Steps after a list no longer disappear

A >>> steps block dropped every step after one whose content ended in a list. When a step's body finished with a bullet or numbered list and no blank line separated it from the next >>> Title delimiter, the cursor was left inside the still-open list item rather than the step group — so the parser treated the next delimiter as the start of a brand-new, nested steps block buried inside the list, which the renderer never surfaced. Every following step (and any content after the whole block) silently vanished. The parser now walks up to the enclosing step and closes it before opening the next one, so all steps render as siblings and the list stays attached to its own step. Only >>> was affected — +++ tabs and ||| columns took a different parsing path.

Fixed

Tesserae live previews survive a neko upgrade

A ```tesserae live preview could render blank after upgrading the neko binary while keeping the project's .neko-cache. The compiled-sample cache key included the Tesserae version but not the neko output format, so a newer build reused an older build's cached HTML — which referenced runtime assets the new build no longer writes (e.g. h5.min.js vs h5.js). Those scripts 404'd, the H5 runtime never initialised, and the preview <iframe> stayed empty. The cache key now carries a format version (stale entries are recompiled automatically), and the min/non-min asset variant is chosen deterministically so a build's generated <script>/<link> hrefs always match the files it writes to disk.

Fixed

Tesserae live previews follow the page's dark mode

A ```tesserae live preview renders in its own <iframe>, which stayed in Tesserae's light theme even when the surrounding docs page was in dark mode — a bright panel in the middle of a dark page. The compiled sample now carries a small theme bridge: because the preview is an about:srcdoc iframe it shares the page's origin, so on load it reads the dark class off the parent <html> and applies Tesserae's tss-dark-mode (and the matching color-scheme) to its own <body>. The page's theme toggle then postMessages every iframe.tesserae-preview so previews flip live with the rest of the page, with a fall back to the OS prefers-color-scheme if the parent can't be read.

Fixed

Tesserae previews follow the theme before they finish rendering

A ```tesserae live preview followed the page's dark mode only after the sample finished compiling and booting — until then it flashed a bright white box in the middle of a dark page, because the theme bridge ran at the end of the sample's <body> and the surface colour only resolved once Tesserae's stylesheet had loaded. The preview now paints the right colour from the very first frame: the <iframe> element itself carries the docs surface colour (bg-white dark:bg-[#222222], Tesserae's own light/dark backgrounds), and a tiny script in the sample's <head> — before any stylesheet — reads the parent dark class and sets the document background, color-scheme, and tss-dark-mode on <html> so the Tesserae CSS variables cascade dark the instant the stylesheet loads. The <body>-level bridge and live theme toggles are unchanged.

Changed

csharp-docs type header scrolls with the page

The csharp-docs type header (kind badge, type name, signature, Definition block) is no longer pinned to the top of the viewport while scrolling. On long type pages the sticky header consumed vertical space and overlapped the member docs; it now scrolls away normally like the rest of the content.

Fixed

Search breadcrumbs name the sub-project they came from

In a multi-project site, a search result's breadcrumb trail is built from the sub-project's own sidebar, so a page at workspace-data-and-integrations/guides/csv showed only Guides — dropping the project that distinguishes it from a same-named section in another sub-site. Every indexed page and section now leads with the project's friendly name, so the first crumb always names the project the result belongs to (e.g. Connect & Ingest › Guides). The name is drawn from the navbar — the text of the links entry pointing at the project's root — so it is a single, shared source that needs no per-project setup. Override it (or name a project absent from the navbar) with a new optional breadcrumb.label key; the full order is navbar label → breadcrumb.labelbranding.labelbranding.title → title-cased mount path. Documented at configuration/core/project.md#breadcrumb.

Fixed

Sticky changelog version header pins cleanly

The version header on the changelog timeline now has two states. In flow it is a rounded, blurred, inset card. When it sticks to the top while you scroll, the card styling drops and it becomes an edge-to-edge glassy bar that spans the whole content pane, pinned flush against the top — so the current release's entries slide cleanly underneath it instead of peeking around a rounded box or through a gap above it. The state is detected with an IntersectionObserver sentinel.

Fixed

Mermaid diagrams no longer hijack page scroll

A rendered ```mermaid diagram is wrapped in panzoom for drag-to-pan and the zoom buttons, but panzoom also bound the mouse wheel to zoom — so scrolling the page with the cursor over a diagram zoomed the diagram instead of scrolling past it. The wheel is now ignored (beforeWheel), so the page scrolls normally over a diagram while drag-to-pan and the zoom-in / zoom-out / reset buttons keep working.

Fixed

Code blocks no longer float as a dark inset box

Fenced code blocks render inside a wrapper card, and the card was meant to own the background, spacing, and rounded corners while the inner <pre>/<code> stayed transparent. The classes that neutralise Tailwind Typography's defaults landed on the <code> element, so the <pre> kept its own dark background, vertical margin, padding, and rounding. In light mode that surfaced as a dark code box floating inside the lighter card with gaps above and below and doubled padding. The wrapper is now tagged neko-code-block and a stylesheet rule resets the inner <pre>, so the code fills the card edge-to-edge with a single layer of padding in both light and dark mode.

Fixed

Folder index page highlights its own sidebar entry

A folder's index.md / readme.md renders as a sidebar entry whose link ends in /index, but the page is served at the folder URL (/foo/, /foo, or .../index.html). The active-link check compared the two literally, so the entry — often the folder's "Introduction" or "Overview" — was never highlighted while you were reading it. Sidebar matching now canonicalises both sides (dropping .html, a trailing slash, and a trailing /index) before comparing. Index links match by exact path only, so the folder entry lights up on the folder page without also lighting up for its sibling pages.

Fixed

Nested sidebar chevrons rotate with their own section

A collapsible sidebar group nested inside another section had a stuck chevron: it always pointed down regardless of whether that sub-section was open or closed. The rotation relied on Tailwind's group-open, which matches any ancestor .group[open] — so a nested chevron was forced down whenever its parent section was open. Chevron rotation is now scoped to each <details>'s own open state, so chevrons at every nesting level turn sideways when collapsed and point down when expanded, matching the top level.

Fixed

Changelog in a sub-project no longer renders empty

In a multi-project site (a folder with its own neko.yml nested under the root), a changelog living inside the sub-project rendered as an empty page. The root build excludes the sub-project's Markdown from scanning, but still discovered the sub-project's changelog folder and emitted an aggregated page with zero entries — and the root site's catch-all file server served that empty page at the shared URL, shadowing the sub-project's real changelog. The root build now skips changelog folders that belong to an excluded sub-project, so the sub-project's own build owns the page.

Fixed

gen-images skips commented directives

neko gen-images and neko gen-dark-images now skip [!img-gen] directives (and assets/img-gen/*.png references) that sit inside an HTML comment. After the first generation pass the original directive is preserved as a <!-- ... --> block above the rendered image — re-running the command was matching the commented-out directive again, burning API tokens and producing nested comments / orphan PNGs. The discovery regex is now paired with a comment-span filter so commented directives are detected and ignored.

Fixed

Sub-project intro pages no longer duplicated in the sidebar

The root index.md of a project is renamed to Home in the sidebar — but in a multi-project site every sub-project's own root matched that rule, so each sub-site's intro page appeared as a "Home" sidebar entry pointing at the sub-site root, duplicating the highlighted pivot tab that already links there. A sub-project root is now omitted from its own sidebar (it stays reachable from the header pivot and parent nav); only the true site root (no route prefix) still shows a "Home" entry.

Fixed

visibility: hidden keeps pages out of the sidebar and search

visibility: hidden and visibility: private — set on a page's frontmatter or a folder's index.yml — now drop that page (or the whole folder subtree) from the sidebar and the search index, while the page is still built and reachable by direct link. Previously the key was parsed but ignored, so "hidden" pages still showed in the navigation. protected is unchanged — it stays in the sidebar and is gated by the password flow.

Fixed

Readable code blocks in light mode

The default light-mode syntax theme is now GitHub (github) instead of tokyo-night-light. The code-block wrapper paints its own near-white background and drops the highlight theme's background, but tokyo-night-light's token colours are mid-tones tuned for its own light-grey background, so code washed out and was hard to read on the near-white surface. The GitHub theme uses near-black text (#24292e) built for a white background, so light-mode code reads clearly; dark mode is unchanged (tokyo-night-dark). Override either with theme.highlight.light / theme.highlight.dark in neko.yml.

Fixed

Code text no longer washes out on the light card

Code could render in a faint grey on the light wrapper card. Two related causes: Tailwind Typography styles <pre>/<code> with low-specificity :where() rules at the same weight as the highlight theme's .hljs, so when the prose rule won the cascade, plain tokens (identifiers, type names, punctuation) took prose's pre-code colour — a light grey tuned for prose's dark <pre>; and a block highlight.js doesn't recognise (an unknown language, or a ```tesserae block whose compile fell back to its raw source) gets no .hljs class at all, so the whole block rendered in that grey. The code-block now re-asserts the base text colour per mode at higher specificity on .neko-code-block pre code (light #24292e, dark #9aa5ce) — independent of .hljs, so highlighted, unknown, and fallback blocks all stay legible while highlighted .hljs-* spans keep their own colours. A ```tesserae block that fails to compile now also falls back to a syntax-highlighted C# block instead of an unhighlighted language-tesserae one.

Fixed

Tesserae code preview: highlighted C#, even padding, .cs filename

The Code view of a ```tesserae block rendered its C# source unhighlighted, with the first line indented while the rest sat flush against the edge, and a .js filename. All three had one root cause: Markdig tags a fenced block language-<info> while parsing, so the renderer's later Info = "csharp" never reached the emitted class — the <code> stayed language-tesserae, which highlight.js doesn't recognise, so it never became display:block and its padding rendered as a first-line inline indent. The Tesserae renderer now swaps the class to language-csharp (so the source is highlighted and the block gets even padding), and presents the filename as .cs since the source is C#. A defensive .neko-code-block pre code { display:block } keeps padding even for any other language highlight.js doesn't recognise.

New

A code block's chrome="macos" (and chrome="osx") now renders the macOS traffic-light dots, the same as chrome="mac". Only the exact value mac was matched before, so the far more common chrome="macos" silently fell back to the default single-dot header.

New

Show a different sample than what runs in Tesserae blocks

A ```tesserae block is compiled and displayed as-is by default. When a sample can't run as-is in the sandboxed preview iframe, an // <overwrite-sample-code>// </overwrite-sample-code> region now supplies the version to show in the Code tab: everything outside the region is compiled and run (and not shown), everything inside is shown verbatim and never compiled. Both the cache-warming and render passes derive the compiled source the same way, so a sample compiles once. Documented at components/tesserae.md and in the tesserae skill.

Fixed

Each Tesserae sample on a page runs its own code

When a page held more than one ```tesserae block, every preview ran whichever sample compiled last and then threw Class 'App' is already defined. The per-sample compiled app (App.min.js) was being written into the shared, per-version assets directory and referenced with a <script> tag — overwriting the previous sample's app — while each preview also inlined its own app. The minified app file is now excluded from the shared assets (it is only inlined), so each preview runs its own code.

Fixed

gen-tesserae-heights no longer drops the first sample on a cold start

neko gen-tesserae-heights measures each live sample in a headless Chromium page. The very first navigate of a run cold-starts the browser, and that launch occasionally returned before a PageId was reported — so whichever sample ran first was skipped outright (-> no measurement), before any capture was even attempted, and kept its placeholder height. The initial navigate is now retried a few times with a short delay, so a cold start can't silently drop the first sample.

v26.5

Features

New

redirectSlug frontmatter key

Added a redirectSlug page frontmatter key that exposes a page at a short, stable URL of the form /redirect/<slug>. When set, Neko writes a tiny HTML file at redirect/<slug>.html whose only job is to redirect the visitor to the page's actual URL via <meta http-equiv="refresh"> (with a JavaScript fallback and a visible link). The redirect page is marked noindex and carries a <link rel="canonical"> back to the real page, so it doesn't compete with the original in search engines. Useful for short, shareable, stable links — printed material, QR codes, emails, social posts — that survive future page re-organisation. Folder index pages collapse their target to the folder URL (docs/index.md/docs/), and the route prefix is applied automatically in multi-repo builds. Slugs are a flat namespace under /redirect/ and must not contain / or \; duplicate slugs across pages are detected at build time (first wins, the rest logged as warnings). Docs at configuration/page.md#redirectslug, and the frontmatter skill in the starter template has been updated.

New

pageLinks navigation in neko.yml

Added a pageLinks configuration in neko.yml for rendering site-wide links at the top of every page's "On this page" navigation. Each entry takes a label, icon, url, and optional target; the url template supports three URL-encoded placeholders resolved at click time — ${page} (page title), ${url} (absolute page URL), and ${selection} (the visitor's current text selection, or empty). Useful for "Report an issue", "Suggest an edit", and "Quote this page" actions. Links only render on pages where the TOC is visible. Docs live at configuration/core/project.md#pagelinks, and the neko-yml skill in the starter template has been updated.

New

neko gen-dark-images command

Added a neko gen-dark-images command that backfills missing dark-mode variants for images previously created by [!img-gen] (or any assets/img-gen/*.png reference authored manually). The command walks every .md file under --input, finds image references that don't already carry a {src-dark="…"} attribute, calls the OpenAI image-edit endpoint to regenerate each in dark mode using the configured imageGen.darkModePrompt, saves the result as <name>-dark.png next to the original, and rewrites the Markdown attribute — preserving any other attributes already on the image. Idempotent: paired images and <name>-dark.png files themselves are skipped, and dark variants already present on disk are relinked into the Markdown without a second API call. Where possible the dark generation matches the PNG header's pixel dimensions of the source image so the pair lines up visually; otherwise it falls back to imageGen.size. Docs at components/img-gen.md#backfilling-dark-variants-for-existing-images.

New

Expanded img-gen with project defaults and dark mode

Expanded [!img-gen] with project-level defaults, a sensible landscape default size, and automatic dark-mode variants. A new imageGen: section in neko.yml exposes a global systemPrompt appended to every image prompt, a size default (now 1536x1024 landscape out of the box, instead of the model's square default), and lightMode / darkMode toggles (both on by default). When lightMode is on, a "render for a light theme" instruction is appended to every prompt; when darkMode is on, Neko follows the light generation with a second call to the OpenAI image-edit endpoint using the freshly generated light image plus a "redo this in dark mode" prompt, producing a paired name-dark.png. The rewritten Markdown now carries a src-dark="…" attribute that the image renderer expands into two <img> tags (dark:hidden / hidden dark:inline-block) so the active theme picks the right variant automatically. The size attribute on a directive now accepts every TornadoImageSizes value (1024x1024, 1536x1024, 1024x1536, 2048x2048 / 2048x1152, 3840x2160 / 2160x3840, auto, plus any explicit <width>x<height> going through Custom), and two new per-directive overrides — light= and dark= — let an individual image opt out of the light-mode hint or the dark-variant pass. Docs at components/img-gen.md, snippets refreshed in templates.json, and both the img-gen and neko-yml skills updated accordingly.

New

sitemap.xml generated by default

sitemap.xml is now generated by default as part of every neko build / neko watch. The default for the sitemap configuration key in neko.yml flipped from false to true, so no opt-in is required. Each entry uses the page's clean (extensionless) URL — directory index.md pages collapse to their folder URL — and includes a <lastmod> derived from the source file's last-write time. Password-protected pages are excluded. Generation is skipped automatically when url is unset (or left at the placeholder localhost), since the resulting sitemap would not contain valid absolute URLs. In multi-repo / sub-project mode, only the root project writes the sitemap (sub-projects share the output directory and would otherwise clobber it).

New

searchExclude configuration option

Added a searchExclude configuration option for opting pages and folders out of the in-site search index. Set searchExclude: true in a page's frontmatter (or sibling .yml) to omit that single page from search.json, or set it in a folder's index.yml / <foldername>.yml to exclude every page under that folder recursively. Excluded pages are still built and reachable by direct link — they just do not appear in search results. Documentation lives at configuration/page.md#searchexclude and configuration/folder.md#searchexclude, and the page/folder skills (frontmatter, folder-index) have been updated accordingly.

New

neko update-skills command

Added a new neko update-skills command that refreshes the Neko-managed skills under an existing project's .claude/skills/ folder to match the versions bundled with the running CLI. Pass --path to point at a project (default: current directory) and --dry-run to preview the changes. The command requires a .claude/ folder to exist (otherwise it errors out and suggests neko new), replaces every Neko-shipped skill folder in place, and leaves any custom (non-Neko) skills untouched — reporting both the count of skills replaced/added and the names of the custom skills preserved.

New

img-gen component and neko gen-images

Added a new [!img-gen] component and a matching neko gen-images command that uses the LlmTornado NuGet package to generate images from inline prompts via OpenAI. Authors describe an image inside a [!img-gen ...] block; running neko gen-images --api-key sk-... [--image-model gpt-image-1] [--llm-model gpt-4o-mini] walks every Markdown file under --input, asks the chat model for a slug and alt-text (strict JSON), generates the PNG with the image model, saves it into the page's assets/img-gen/ folder, and rewrites the directive into a regular Markdown image with the original directive preserved as an HTML comment so it can be re-generated later. Only OpenAI is supported for now. The directive renders nothing in HTML until you run the command, and supports size, quality, background, and style attributes. Documentation lives at components/img-gen.md, with two neko-img-gen* snippets in templates.json and a new img-gen skill in the starter template.

New

neko new scaffolding command

Added a new neko new command that scaffolds a fresh hello-world documentation project (with neko.yml, three sample pages, and a .claude/ folder of skills) into the current directory or a directory passed via --path. The starter lives under .template/ in the repository and is zipped at build time and embedded as a resource in the CLI assembly. Pass --force to overwrite a non-empty target.

New

Roadmap component

Added a new Roadmap component that renders a kanban-style product roadmap board. Lanes (:::: lane) carry a title, count badge and accent colour; items (::: roadmap-item) carry a title, tag pill, optional date, vote count, and optional clickable link. Lane count badges reuse the icon-badge tint+ring style from grid cards, item tag pills follow the soft bg-{color}-100 / text-{color}-800 palette from [!badge], and lanes/items use rounded-2xl / rounded-xl with hover:border-primary-* to match Neko cards. Documentation lives at components/roadmap.md, with three neko-roadmap* snippets in templates.json.

New

Drag-and-drop sidebar reordering

Added drag-and-drop reordering of sidebar items while running neko watch. Items can be reordered within their parent group; on drop, the corresponding .md frontmatter order (or folder index.yml order) values are rewritten as multiples of 10, and the site reloads automatically. A new POST /api/neko/reorder endpoint handles the update.

New

neko snap screenshot command

Split screenshot capture into a dedicated neko snap command. neko build and neko watch no longer call Playwright; instead, run neko snap to capture missing screenshots, or neko snap --all to re-capture everything.

New

Lesson component

Added a new [!lesson] Markdown component that renders a curriculum-style track. Steps are auto-discovered from sibling .md files in the folder, ordered by their order frontmatter. User progress is persisted to localStorage per lesson.

New

Lesson step navigation block

Pages inside a [!lesson] folder now render a dedicated Go back: … / Next step: … navigation block at the bottom, with chevron icons and links to the previous and next siblings in the same curriculum order as the parent lesson page. Detected automatically — no per-page configuration required.

New

Learn Neko sample track

Added a Learn Neko sample track under lesson/ showcasing the new component end-to-end.

Improvements

Improved

HtmlGenerator split into partial classes

Split HtmlGenerator into focused partial-class files by concern (HtmlGenerator.Head.cs, HtmlGenerator.Navbar.cs, HtmlGenerator.Sidebar.cs, HtmlGenerator.Content.cs, HtmlGenerator.WatchMode.cs, HtmlGenerator.Scripts.cs). The 1,345-line Generate method is now a ~50-line orchestrator that delegates to single-purpose render methods (RenderBanner, RenderNavbar, RenderBreadcrumbs, RenderArticleBody, RenderPageNavigation, RenderFooter, RenderTocSidebar, RenderPageScripts, etc.), and the 327-line GenerateHead follows the same shape (RenderHeadMeta, RenderHeadTailwindAndTheme, RenderHeadMermaid, RenderHeadHighlightJs, …). Pure code reorganization with no behavioral changes — the emitted HTML for every page in Neko.Documentation is byte-identical to the previous output (only the intentionally-random encrypted-page payloads and tab GUIDs differ).

Improved

Search works across multi-repo builds

Search now works across every sub-project in a multi-repo build. Each sub-project's search.json is still written next to its own output, but at the end of the build (and every watch-mode rebuild) every sub-project's entries are merged into a single aggregated search.json at the root output. Document ids are now route-prefixed (workspace/core-concepts/graph-model.html instead of bare core-concepts/graph-model.html), and the client always fetches the root index — so a search from /workspace-deployment/ can find pages under /workspace/ and vice versa. The route-prefix segments also become slug tokens, so a query like workspace graph model boosts the right pages. Results from the sub-site the visitor is currently browsing also get a 1.6× score boost, so "local" hits surface first while other sub-sites stay reachable below them.

Improved

Friendlier search result breadcrumbs

Search results now show a friendly, extension-less breadcrumb under each hit (.../workspace/core-concepts/graph-model) instead of the raw .html URL, and a trailing index segment collapses to its folder. Pages living under any folder whose name starts with . or _ (e.g. _helpers, _reference-material, .template) are no longer indexed — these are treated as private scaffolding.

Improved

More prominent search result titles

Search results now make the page title visually more prominent — the title renders at text-base font-semibold (up from text-sm font-medium) so it dominates the snippet/breadcrumb beneath it. The indexed title also now falls back to the frontmatter label: (when title: is absent) and then to the first heading at any level (not just H1), so a page that opens with ## Graph Model and only declares label: "Graph Model" is indexed as Graph Model instead of the bare filename graph-model. Finally, the search slug drops trailing index segments (blog/index.html → slug blog, root index.html → empty), so a query for index no longer blanket-matches every section landing page via the slug. Pages named index.md remain indexed and findable through their title and content.

Improved

Deeper, more relevant search results

Overhauled the in-page search to surface deeper, more relevant results. The build-time indexer now emits one document per H2/H3 in addition to the page-level document, so search results deep-link directly to a section anchor instead of just the page. Each page document carries new slug, headings, type, parentTitle, and parentId fields. The slug splits the path into separate tokens (e.g. blog/index.htmlblog index), so queries like index now reliably return index.md even when the page title is something else. The client (MiniSearch) was upgraded to AND-combine multi-word queries, boost slug and title above body content, apply fuzzy matching only to longer terms, and de-duplicate page+section hits in favour of the deeper link. The results UI now shows a highlighted snippet around the first match, a parent-page breadcrumb on section hits, a loading state while the index is fetched on first use, and a recent-searches list (stored in localStorage) when the input is empty.

Improved

csharp-docs preserves accessor declarations

The csharp-docs block now preserves property and indexer accessor declarations ({ get; }, { get; set; }, { get; init; }, { get; private set; }, etc.) in the rendered signature. Accessor bodies and expression bodies are stripped but the accessor keywords themselves are kept, so the rendered API doc shows what is actually readable / writable at a glance.

Improved

Cleaner DocFx-style csharp-docs layout

The csharp-docs block now renders a much cleaner DocFx-style layout and stops leaking the parent class body into the type signature. Class declarations are rebuilt from their modifiers, keyword, identifier, type-parameter list, base list, and constraint clauses, so the body — fields, private members, etc. — never appears in the signature. The parent type now renders as a sticky header (kind badge, name with a small unobtrusive anchor link after it, signature, summary) that floats at the top of the csharp-docs section while the visitor scrolls through the constructors / properties / methods listed below it. Members are grouped by kind (Constructors → Properties → Methods → Events → Fields), each prefixed with a kind badge (Constructor, Property, Method, etc.) and qualified with the parent class name (e.g. DetailsList.OnColumnClick). The link anchor was moved to after the name, shrunk, and stripped of its underline. Signatures are now whitespace-normalized at render time, so column-aligned source code like public void OnColumnClick() renders as public void OnColumnClick(). Standalone members (fragments without an enclosing class) still render via SourceCodeKind.Script parsing so the legacy snippet form on components/csharp-docs.md keeps working.

Improved

Refreshed default light / dark themes

Refreshed the default light / dark themes to match the look-and-feel of docs.curiosity.ai. Introduced a new curiosity default theme (deep navy #050914 background in dark mode), a paired accent palette, and a neko-text-gradient utility used by the hero accent word.

Improved

Redesigned grid card variant

Redesigned the grid card variant to match the curiosity card style — rounded icon badge with palette-coloured tint, hover glow, no image required. Added a new palette attribute and a deterministic palette fallback.

Improved

Redesigned hero component

Redesigned the [!hero] component with an eyebrow label, gradient accent word (title-accent), and subtle radial glow accents. Default alignment is now left to match the reference.

Improved

Removed gradient card options

Removed all gradient card options (gradient, gradient-mode, gradient-colors, gradient-noise, gradient-speed) and the bundled makegradient.js asset. Use the new icon-badge style instead.

Improved

Removed --disable-snapframe flag

Removed the --disable-snapframe build/watch flag — capturing is now opt-in via neko snap.

Fixes

Fixed

label honored consistently

The frontmatter label: is now honored consistently wherever a page's display name is derived. Previously only the left sidebar fell back to label; the top navigation entry and the page <title> tag used title only and ignored label. Now the top-nav entry resolves label → title → file name (matching the sidebar), and the <title> tag falls back to label when no title is set. This means pages that follow the recommended pattern of setting label: (instead of the discouraged title:) get a meaningful title and nav label everywhere, not just in the sidebar.

Fixed

Heading anchor link spacing

The hover # anchor link next to headings no longer abuts the heading text. It's now positioned by its own width (-translate-x-full) with a fixed pr-2 gap, so the spacing stays consistent regardless of heading size — previously the fixed -left-6 offset was too small for larger headings, leaving the icon touching the text.

Fixed

Header logos resolve route prefix

Header logos (branding.logo / branding.logoDark) on multi-site builds are now resolved with the project's route prefix, so a sub-project at /tesserae emits <img src="/tesserae/assets/tesserae-logo.png"> instead of /assets/tesserae-logo.png. Previously the root-relative path generated by ResolveLogoPath was written out verbatim, which only happened to work when the same asset filename also existed at the site root.

Fixed

Favicon path HTML-escaping and auto-detection

Favicon injection now HTML-escapes the configured path before writing it into the <link rel="icon" href="..."> tag, so values containing &, ", <, or > (e.g. favicon.ico?v=1&cache=2) no longer produce broken markup. In addition, the long-documented default behavior is now actually implemented: when branding.favicon is not set, Neko auto-detects favicon.ico or favicon.png at the input root, sets the favicon link to /favicon.<ext>, and copies the file to the output root so the link resolves.

Fixed

neko.yml link normalization

Links in neko.yml (top-navigation links, dropdown items / footerItems, and banner.link) are now normalized at load time the same way Markdown links are — a trailing .md or .html is stripped so authors can paste the on-disk filename (e.g. link: /workspace/core-concepts/graph-model.md) and get the clean URL (/workspace/core-concepts/graph-model) in the rendered nav. Fragments (#section), query strings (?x=1) and external :// URLs are preserved unchanged.

Fixed

Centered-alignment pipe tables

Pipe tables whose delimiter row used centered-alignment markers (:---:) were rendered as a paragraph instead of a table. The EmojiParser was greedily matching :---: as an emoji named ---, consuming the alignment markers before Markdig's pipe-table parser could see them. Emoji names now require at least one alphanumeric character, so alignment markers are left intact. This restores the rendering of the All Icons table on the Icon component documentation page.

Fixed

Search shows H1 for untitled pages

Search results now display the page's first # H1 heading when the page has no title: set in its frontmatter, falling back to the file name only if neither is present. Previously, untitled pages always showed the bare file name in the search modal.

Fixed

Search indexes rendered content

Search now indexes the rendered page content instead of the raw markdown source. Auto-injected blog and changelog listings, callouts, tabs, and other component bodies are searchable, while YAML frontmatter is no longer mixed into the indexed text. Password-protected pages are skipped entirely (previously their frontmatter password and body leaked into search.json). The client honours --route-prefix for both the search.json fetch and result links.

Documentation

Docs

Styled inline code

Inline code (text wrapped in single backticks) is now styled with a subtle background, border, and rounded corners in both light and dark mode, matching standard documentation rendering. The Tailwind Typography default of showing literal backtick characters around inline code has been replaced with this visual style.

v26.3.16

Features

New

Inline PDF component

Added a new component for rendering PDF files inline in the text. You can now use the standard markdown image syntax pointing to a .pdf file to automatically render it in an iframe using pdf.js.

v26.3.12

Features

New

Global password protection

Added support for configuring a global password in neko.yml. You can now protect the entire documentation by defining password: "my-secret" in your global configuration. Individual pages can bypass this global protection by setting password: none in their frontmatter.

Documentation

Docs

Tesserae TODO sample app

Updated the Tesserae component documentation (components/tesserae.md) to include a full interactive TODO sample application that demonstrates building UI components and persisting state via window.localStorage.

v26.3.11

Features

New

csharp-docs code block

Added csharp-docs code block language mode which leverages Roslyn to parse C# code blocks containing XML comments and beautifully renders them with DocFx-like layouts detailing the summary, parameters, remarks, return types, and exceptions.

New

sitemap.xml generation

Added a sitemap boolean configuration option in neko.yml to automatically generate a sitemap.xml file containing all generated HTML pages, utilizing the configured url as the base address.

New

Monaco template auto-completion

Added Monaco Editor support for auto-completing templates for all valid components of neko starting with the "neko-" prefix. The template list is loaded dynamically from templates.json on the first render of the editor.

Improvements

Improved

Sticky sidebar search box

Made the sidebar search box sticky when scrolling the sidebar, allowing quick access to the filter functionality. This was achieved by updating the HTML generation in Neko/Builder/HtmlGenerator.cs to wrap the search input in a sticky container while maintaining the proper layout for the rest of the navigation list.

Documentation

Docs

Live Editing guide

Added a new guide for the Live Editing feature in Watch mode, including details on auto-completing templates.

v26.3.3

Snapframe Component

Features

New

Snapframe component

Added a new [!snapframe] Markdown extension that automatically generates screenshots of external websites using the SnapFrame .NET tool during the build process.

Improvements

Improved

Multi-line snapframe commands

Extended the [!snapframe] Markdown extension to support multi-line command execution, allowing interaction with the page before taking the screenshot.

Documentation

Docs

Merged image alignment docs

Merged the image alignment documentation into the main image.md file.

v26.3

Initial Release

Highlights

New

Initial Release of Neko v26.3

We are excited to announce the initial release of Neko, a powerful static site generator designed to help you create beautiful, documentation-first websites with ease.

Key Features

  • Markdown First: Write your documentation in standard Markdown. Neko handles the rest.
  • Rich Components: Enhance your docs with built-in components like Alerts, Badges, Tabs, and more.
  • Theming: Dynamic Tailwind themes configurable via neko.yml under the theme key. Users can specify a built-in palette (e.g., name: violet) and override specific shades.
  • Blog Support: Neko supports a 'Blog Mode' where files in a blog/ directory are processed as posts, sorted by date (descending), and displayed in a responsive grid layout of cards.
  • Changelog Support: Neko supports a 'Changelog Mode' where a folder marked with changelog: true collects version-named files, sorts them newest-first, and displays them in a vertical timeline layout.
  • Watch Mode: The CLI supports a watch command that serves the site on localhost and auto-reloads on file changes, including a built-in Monaco editor for quick edits.
  • Multi-Repo Mode: Simultaneously build, watch, and serve multiple sub-projects located in immediate subdirectories containing a neko.yml file.
  • Tesserae Support: Write and compile Tesserae C# code blocks directly in your Markdown, generating live interactive components.

Other Important Features

  • Markdown Custom Containers: Support for custom Markdown syntax like Icons, Badges, Alerts, Tabs, Columns, Steps, Generic Components, Code Snippets, Panels, Emojis, and Cards.
  • Navigation History: Tracks the last visited pages in browser localStorage, with a flyover popup UI.
  • Built-in Search: Full-text client-side search across your documentation using Minisearch.
  • Dynamic Card Backgrounds: makegradient.js integration for beautiful, dynamic card backgrounds.
  • Mathematical Formulas & Diagrams: Integrated support for KaTeX math formulas and Mermaid diagrams.

Features

New

Icon Search in Watch Mode Editor

  • Added a searchable list of icons in the watch mode editor modal.
  • Accessible via the Ctrl+I or Cmd+I keyboard shortcut.
  • Allows inserting the selected icon's name directly into the editor at the current cursor position.
New

Mermaid Diagram Zoom Controls

  • Added built-in zoom controls to Mermaid diagrams.
  • Hover over any Mermaid diagram to access Zoom In, Zoom Out, and Reset buttons.
  • Applied a minimum height of 400px to all Mermaid diagrams to give ample space for interacting with diagrams.

Improvements

Improved

Workflow LeaderLines Clipping Improvements

  • Modified the workflow component javascript to appropriately clip connection lines within the workflow container.