Changelog
Neko is under active development and follows a calendar-versioning (vYY.M) scheme. The most recent release is listed first.
Fixes
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
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.
New components
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
`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.
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.
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.
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.
`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.
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.
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.
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.
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.
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.
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.
`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.
`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.
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.
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.
`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.
`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.
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.
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.
`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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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).
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.
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.
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.
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.
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.
`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.
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.
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.
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.
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.
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.
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.label →
branding.label → branding.title → title-cased mount path. Documented at
configuration/core/project.md#breadcrumb.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Features
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
Learn Neko sample track
Added a Learn Neko sample track under lesson/ showcasing the new component end-to-end.
Improvements
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).
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.
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.
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.
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.html → blog 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.
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.
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.
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.
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.
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.
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.
Removed --disable-snapframe flag
Removed the --disable-snapframe build/watch flag — capturing is now opt-in via neko snap.
Fixes
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.
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.
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.
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.
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.
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.
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.
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
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.
Features
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.
Features
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
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.
Features
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.
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.
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
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
Live Editing guide
Added a new guide for the Live Editing feature in Watch mode, including details on auto-completing templates.
Snapframe Component
Features
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
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
Merged image alignment docs
Merged the image alignment documentation into the main image.md file.
Initial Release
Highlights
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.ymlunder thethemekey. 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: truecollects version-named files, sorts them newest-first, and displays them in a vertical timeline layout. - Watch Mode: The CLI supports a
watchcommand 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.ymlfile. - 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.jsintegration for beautiful, dynamic card backgrounds. - Mathematical Formulas & Diagrams: Integrated support for KaTeX math formulas and Mermaid diagrams.
Features
Icon Search in Watch Mode Editor
- Added a searchable list of icons in the watch mode editor modal.
- Accessible via the
Ctrl+IorCmd+Ikeyboard shortcut. - Allows inserting the selected icon's name directly into the editor at the current cursor position.
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
Workflow LeaderLines Clipping Improvements
- Modified the workflow component javascript to appropriately clip connection lines within the workflow container.