Changelog
[Unreleased]
[13.1.0b1] — 2026-10-30
Security
- The social plugin fetched card fonts without network guards — the privacy
plugin had grown SSRF protections (private/link-local/metadata address
refusal, per-hop redirect re-checks, an https-downgrade block), but the social
plugin's font download had none, then fed the bytes to a font parser and
cached them. Font fetching now goes through the same guarded path.
Fixed
- Incremental builds showed wrong nav titles —
Page.titleprefers_title_from_render, which onlyPage.render()sets, so in an incremental
build every page that was not re-rendered fell back to a naive#-scan
(or its filename). A rebuilt page therefore labelled its siblings wrongly
while untouched pages kept the right label: on CTF Wiki 5 of 400 pages
disagreed, e.g. a setext heading (工具on a====underline) showed asOthersand an emphasised title lost its markup. Rendered titles are now
persisted per page and seeded back for pages a build skips; frontmattertitlestill wins because it is re-read from source every build. The nav
signature is also recomputed after rendering, so a cold build no longer
triggers one wasted full rebuild afterwards. - Search index state was never reset between
serverebuilds —serve
reuses one plugin instance (its config is cached by mtime), and the per-build
tracking sets (indexed pages, changed pages, old locations) were only
initialised in__init__. A page indexed in an earlier rebuild therefore
stayed in_indexed_this_buildand was never re-indexed after an edit, and
stale_changed_urisentries made the incremental Marz update delete docs
that were still present. All four sets now reset inon_pre_build. - Incremental search-index updates crashed the build — the incremental
Marz path re-declared fields thatIndexBuilder.from_indexhad already
restored, which raisesValueError: field "title" is already declared. The
call sat outside thetry/except, so every incremental build died and the
full-rebuild fallback never ran. The same root cause left two more defects
in that path:tagswas passed as a list (Marz requires a string) and a
field missing from the previous index was never declared. The full and
incremental builders now share one document/field helper. - Non-strict builds still failed on a single bad page — the page-populate
stage raised on any render error even without--strict, while the write
stage continued. One broken page now only warns (and is skipped) unless--strictis set, consistently across both stages. - Generated markdown pages crashed the build — a freshness check called
Path(None)for files with no source on disk. Generated pages are now
always treated as fresh. - Snippet-include pages rebuilt on every incremental build — the two
per-page stages resolved the--8<--include dependency set from different
markdown, so the cache kept pruning deps the other stage had just added.
File deps are now computed once and shared. INHERIT:cycles crashed with a stack overflow — a file inheriting from
itself (directly or in a cycle) raisedRecursionError. It now reports a
clearConfigurationErrornaming the cycle.- Duplicate YAML keys were silently accepted —
site_name:twice kept the
last value with no warning. Duplicate keys are now rejected with aConfigurationErrornaming the file, key and line numbers. docsforge checkskipped dict-style plugins —plugins: [{search: …, tags: …}]
validated only the first entry. Every plugin in a mapping is now checked.- The privacy plugin could drop adjacent tags — a tag-matching regex could
swallow two neighbouring tags (<img>next to<script>) into one match and
re-emit only the last, so an image vanished from the built page. Each tag is
now parsed and preserved exactly once. - External images with spaces around
=were never inlined — the privacy
matcher missed<img src = "…">, so those requests still leaked visitors'
IPs to third parties. Whitespace around=is now matched. --jobs/--strictblog pagination built on a discarded side effect —
paginated archive/category pages were constructed then thrown away, relying
on a constructor side effect and leaving them unnamed. The page is now used
and named directly.- Blog post
.meta.ymlmerging silently disabled — the blog plugin looked
up the meta plugin by a hard-coded entry-point name; if that lookup missed,
post metadata merging stopped with a confusing "expected created date" error.
It now falls back to finding the meta plugin by type. - A VS Code reload during a build orphaned the build process — the
extension'sdispose()didn't stop an in-flight build child, and on Windows
the Python process survived every reload.dispose()now kills it. renameAnchorcorrupted headings containing$— the replacement string
went throughString.replacesubstitution, so a title likeA $& Binserted
the whole match. A replacement function is used instead.- Windows: several Studio features silently never fired — link targets with
backslashes never matched, and the document-selector glob treated\as an
escape so every provider was dead on Windows. Paths are normalised andRelativePatternis used.
Performance
- Git revision dates are now opt-in — every page used to run a
git log --followsubprocess to show "last updated / created". On a
2,200-commit repo that measured 0.6s per page, so a 771-page build spent
roughly 475s in git subprocesses — by far the largest cost in a real build,
and a feature most sites never used (upstream ctf-wiki, whose config this
behaviour was modelled on, has no git plugin at all). Enable it withextra.git_revision_date: truewhen you want the footer; a site without it
now builds without touching git. - Asset-reference scanning uses libxml2 instead of
html.parser— the
asset optimizer re-parses every built HTML page to find asset references, and
that scan was the largest single cost in a cold build: 6.02s of a 19.4s
201-page build, 40% oncehtml.parser's internal regexes are counted._AssetReferenceParseris now backed bylxml's target interface, which
streams the same start/end/data events without building a tree. On the 202
built pages of that site the isolated parse drops from 6.98s to 0.91s
(7.7x), and the reference lists are identical — 202/202 pages, 25/25 edge
cases (void and self-closing elements, CDATA in<style>, entities, unquoted
attributes, comments, malformed markup, uppercase tags) and the full built
site is byte-identical. No wall-clock figure is claimed for the whole build:
on a shared machine the run-to-run spread (4.4s stdev on a 21s median) is
larger than the saving, so only the isolated parse measurement is meaningful.html.parserremains as an automatic fallback iflxmlcannot be imported. - Untouched locales no longer rebuild their whole search index — on a
multi-locale site, editing one page made every other locale rebuild its
Marz index from scratch, because an empty change set fell into the
full-rebuild branch instead of reusing the index already on disk. Measured on
CTF Wiki (771 pages, 3 locales): the incremental build spent 6.57s in Marz,
of which 2.66s was that waste. Unchanged locales now return the previous
bytes untouched. Entry recovery from source still forces a full rebuild,
since a re-derived cache makes the old index stale. - Incremental builds update only the changed search-index docs — the Marz
binary search index was rebuilt from scratch on every build, so a one-file
edit on a large site still paid the full index cost. Changed and removed
pages are now tracked per locale, the previous.marzis rehydrated, and
only those docs are removed/re-added. On the 771-page CTF Wiki a single-file
incremental build drops from ~18.5s to ~6.5s. Falls back to a full rebuild
when there is no usable previous index. concurrencynow defaults to every CPU core — the default wascpu_count - 1. Markdown conversion itself is pure Python and therefore
GIL-bound, so page rendering alone scales poorly (measured on a synthetic
201-page site,concurrency: 18.62s vsconcurrency: 48.74s). That
benchmark was not a git repository, which hid the real reason to
parallelise: a build inside a git repo runs agit logsubprocess per page
for the revision date, and subprocess I/O releases the GIL, so workers do
overlap there. On real sites that dwarfs the rendering cost. Still
configurable viaconcurrency:. Separately, the sidebar highlight now reads
the rendering page through a thread-local instead of a shared mutable flag.
Changed
minifycan be turned off — it was on with no way to opt out. Disable it
withplugins: [minify: {enabled: false}]to keep the generated HTML/CSS/JS
readable (debugging a layout, diffing output, or measuring the rest of the
build). It stays on by default.- The blog plugin is opt-in — it was auto-loaded, and it generates a
/blog/entrypoint page even when a site has no posts, so every site grew a
page it never asked for. Enable it explicitly withplugins: [blog]. The
other five core plugins (meta, tags, info, minify, i18n) plus search still
load automatically. Studio heading slugs now match the engine exactly — the extension
hand-rolled a heading-slug function that had drifted from the real engine
(it kept CJK, collapsed underscores, and trimmed edge separators, all wrong:
the engine keeps_, ASCII-folds CJK away, and preservesFoo-asfoo-).
The build now writes.docsforge/cache/slug-rules.jsonwith the separator
and unicode mode read from the site's actualtocconfig, so a custommarkdown_extensions: [{toc: {separator: "_"}}]is honoured; a customslugifycallable setspredictable: falseso the extension falls back
instead of trusting rules it cannot reproduce. The extension threads those
rules through every slug call site, treatstoc.unique()_1/_2suffixes
as valid anchors, and has a parity test pinned to reference vectors on both
sides (Pythonslugger.slugifyis now tested againstmarkdown.extensions.toc.slugifydirectly).The two per-page build loops are merged into one pipeline — the populate
and write stages each walked every dirty page and each parsed snippet deps.
They are now a single pipeline with one error policy and one dep-parse pass;
incremental output is byte-identical.One guarded-fetch helper replaces three drifted copies — a new
docsforge/fetch.pycentralises the SSRF guards, redirect re-checks and size
caps that privacy and social each had partial local copies of. The deadurl_cache.py(zero callers, weakest guards) is deleted.cairosvgis imported lazily by the social plugin — merely importing the
plugin cost ~4s becausecairosvgloaded at module top. The import is now
deferred to card rendering (~0.6s).
Removed
- Dead code:
files.pylegacy shims (src_paths,_files,_sort_files,_filter_paths),config_base.pyload_file/user_configs, the duplicated
deadget_schemain both config files,config_options.pyRepoURL,info.py_get_exclusion_patterns, andurl_cache.py.
Changed
- Python 3.10 is no longer supported; the floor is now 3.11 — 3.10 reached
end of life in October 2026, and upstream Material for MkDocs has dropped it
too.requires-pythonis now>=3.11, and CI tests 3.11, 3.13 and 3.15
instead of 3.10 and 3.12.
This also unblocks dependency upgrades that had started requiring 3.11:Markdown 3.11, pillow 13 and platformdirs 4.13 all declarerequires-python >= 3.11, so pinning them at 13.0.5 broke installation on
3.10. Ruff's target-version moves to py311 to match.
Code that only existed for 3.10 is gone: the ExceptionGroup fallback class
in docsforge/exceptions.py (the builtin is always available now), the
Python 2-era PY3/unicode shim in docsforge/paginate.py, and the"Z" → "+00:00" workaround before datetime.fromisoformat indocsforge/git_info.py, which 3.11 handles natively.
minify-gois skipped on Python 3.15 — it ships one wheel per
interpreter (cp310 through cp314) with no sdist and noabi3build, so on
3.15 the package could not be installed at all. The dependency now carries apython_version < '3.15'marker and the minify plugin serves unminified
output when the backend is absent, instead of failing at import. HTML, JS and
CSS are still correct, just not minified. The marker goes away once upstream
publishes a 3.15 wheel.
[13.0.5] — 2026-10-28
Security
- Blog post
slugcould write outside the output directory — the slug is
raw front matter and was interpolated into the post path, then normalized, soslug: ../../../tmp/xbuilt the page outsidesite/. Generated paths are now
rejected when they resolve above the docs directory. - PDF export followed
file://requests anywhere on disk — a page could use
raw HTML (<iframe src="file:///etc/passwd">) to pull local files into the
exported PDF, because only http(s) URLs were checked. Local requests are now
limited to the built site, and root-relative assets (/assets/…) are mapped
back onto it — so rendering is unchanged for legitimate pages. - The privacy plugin fetched internal addresses — any external
<img src>
is downloaded at build time, so a loopback, RFC1918 or cloud-metadata URL
(169.254.169.254) was fetched on the build machine and the response copied
into the published site. Non-public addresses are now refused, and every
redirect hop is re-checked.
Fixed
- Two files in different directories could overwrite each other — the
destination-collision check was scoped to one directory, sofoo.mdandfoo/index.mdboth built tofoo/index.html; the winner depended on thread
completion order and no warning was printed. Collisions are now detected
site-wide and reported. docsforge build --pdfbroke unlessdocs_dirwas literallydocs— the
project root was inferred from the directory name, sodocs_dir: content
looked forcontent/site. Resolution now goes through the config file, andsite_diris honoured.- A deleted PDF was never re-exported — freshness compared only the page's
HTML hash, so a removedpdf/index.pdfstayed missing while the build
reported "up to date". The output file's existence is now part of the check. - Two posts with the same title silently collapsed into one — both derived
the same slug, one page was overwritten, and the index linked both posts to
it. Duplicate slugs are now disambiguated with a-2suffix and a warning. tags_extra_filescould never be configured — the option wrapped a config
option insideType, so setting it raisedTypeErrorduring config load.shadow_tagscrashed the build — tags were compared against a list of
strings viaTag.__eq__, which asserts its operand is aTag.social: falsein front matter crashed the page — a scalarsocial:value
was treated as a mapping. A bare bool now acts as thecardsoverride, which
is what the option was for.post_readtime_words_per_minute: 0divided by zero — the rate was used
unclamped after passing validation.!ENVconfigs resolved to the wrongdocs_diron the--pdfand
dependency-check paths — those read the file withyaml.FullLoader, which
raises on!ENV; the exception was swallowed and the default used instead.- Social cards could hang or recurse forever — Google Fonts requests now
have shorter timeouts and a size cap, a manifest without font URLs fails as
"unavailable" instead of a laterFileNotFoundError, and an empty font
directory raises instead of calling itself untilRecursionError. - Studio missed every link containing a space — the link scanner stopped at
the first space, so[a](my page.md)and[a](<my page.md>)were invisible:
renaming left a dead link and no hover, definition or quick-fix fired. - The theme template directory was listed twice — the check compared a
Pathagainst astr, so it never matched and every template lookup stat'ed
the same directory twice.
Dependencies
- Python —
click8.4 → 8.5,Markdown3.10 → 3.11,pymdown-extensions
11.0 → 12.3,backrefs8.0 → 8.1,Pygments2.20 → 2.21,platformdirs
4.11 → 4.13,markupsafe3.0.3 → 3.0.4,marz-search0.3.0 → 0.3.1,minify-go2.24.17 → 2.24.24; optional extraspillow12 → 13,cairosvg
2.9.0 → 2.9.1,pypdf6.15 → 6.21,playwright1.62 → 1.64. The full suite
passes against the upgraded set. - Frontend —
katex0.19.0 → 0.19.1,lucide-static1.57 → 1.62,simple-icons16.35 → 16.36,@primer/octicons19.40 → 19.42,@fortawesome/fontawesome-free7.1 → 7.3,postcss8.5.26 → 8.5.32,autoprefixer10.6.1 → 10.6.2,sass1.105 → 1.106,svgo4.0 → 4.1,preact10 → 11. Vendored KaTeX, Mermaid and the search worker were rebuilt. - Tooling —
ruffis now pinned to the same version inpyproject.tomland
both workflows (0.17.3); they had drifted to 0.17.0 vs 0.16.6, so a localruff checkdid not match the gate.eslint10.12 → 10.13 andtypescript-eslint8.71 → 8.72.typescriptstays on 6.x: 7.0 is available
buttypescript-eslintstill constrains it to<6.1.0.
[13.0.4] — 2026-10-23
Added
announce:config key — the announcement bar no longer requires atheme.custom_dirtemplate override. A top-levelannounce:key renders it
from configuration:
announce:
text: v14 is out
link: /changelog/
link_text: see what changed
icon: material/party-popper
A plain string is shorthand for text. style switches between the default
(footer) and warning colors. text is escaped, so rich markup still needs
an {% block announce %} override — which continues to take precedence.
Changed
announce.dismissis now a default feature — the bundled Material theme
enables the dismissible announcement bar out of the box, so a site that fills
theannounceblock gets the “Don't show this again” button without having to
list the flag. As with every other theme feature, declaringtheme.features
replaces the defaults rather than merging into them.
Fixed
- Announcement bar docs rendered a literal shortcode — the customization
guide's{% block announce %}example used:material-party-popper:, but
that block is rendered after Markdown conversion, so the shortcode appeared as
plain text. The example now includes the icon template directly
({% include ".icons/material/party-popper.svg" %}), and the page has a
dedicated “Announcement bar” section covering setup, styling and dismissal.
Dependencies
- New icons — refreshing the vendored sets against the bumped packages adds
85 icons:lucide-static1.47 → 1.57 (+56, plus 13 redrawn),@primer/octicons
19.38 → 19.40 (+23) andsimple-icons16.32 → 16.35 (+6). The Material (MDI)
and Font Awesome sets are unchanged. - KaTeX 0.18.9 → 0.19.0 — carries an upstream breaking change: missing
character metrics now report throughstrictassymbolNotInFont. The theme
never sets a strict callback, so rendering is unchanged; verified inline,
display, CJK withstrict: "ignore"andParseErroron unknown commands. - Mermaid 12.0.0 → 12.2.0 — the vendored bundle shrinks by ~80 KB
(5.58 MB → 5.49 MB). - Tooling —
ruff0.16.10 → 0.17.0,typescript-eslint8.71.1 → 8.71.2 and@types/node26.6.4 → 26.6.5. Build-time only, no runtime effect.
Docs
- Positioned as a candidate drop-in replacement — the README compared
DocsForge against seven tools at once; that framing competed with a
recommendation incumbent users had already received. The comparison table is
gone, the claim is narrowed to MkDocs + Material for MkDocs (whose upstream is
winding down), and it now leads with the guarantee that makes it checkable. - Free migration support, including by email — if migration breaks anything,
we fix it for free. The offer has its own section on the support page in both
languages, with two routes: a GitHub issue for open-source sites, or email
with[MIGRATION]in the subject for anything private. - “Zero CDN calls” stated precisely — it is true for readers and not for the
build. The docs now separate the two and documenttheme.font: falseplusprivacy.assets_fetch: falsefor builds that must touch the network zero
times; that combination leaves no external URL in the output at all.
[13.0.3] — 2026-10-16
Fixed
extra.pwa.iconsreach the manifest — the extra-icons branch was
guarded byisinstance(extra, dict), butconfig.extrais aConfig
(aUserDict), so the check could never pass. Sites declaringextra.pwa.iconssilently shipped a manifest.json containing only their
favicon/logo, which left them non-installable. The guards now accept anyMapping.
[13.0.2] — 2026-09-23
Added
- Privacy patterns match hosts —
assets_include/assets_exclude
globs are now tried against the normalizedhost/pathand the bare
hostname, so*.clouddn.comexcludes a whole host instead of requiring
path-prefix guessing. Excluded URLs stay remote links, never downloaded.
Fixed
checkvalidates nav and extra assets —docsforge checknow
reports nav entries pointing at missing files (errors), pages on disk
missing from nav, malformed nav entries, and missing relativeextra_css/extra_javascriptfiles (warnings). i18n twins
(page.xx.md) are covered by their base entry and never reported.- Options-carrying builtins are no longer flagged — declaring a
built-in plugin with options (e.g.privacy: {enabled: false}) no
longer triggers the redundant-declaration warning, andcheck --fix
preserves such entries instead of stripping them. - Shutdown before serve starts no longer crashes — Ctrl+C during the
initial build used to end inRuntimeError: cannot join thread before it is started; the serve thread is now only joined after starting. - One Ctrl+C exits mid-build runs on the first press — page-render and
download waits polled unboundedly, parking in lock waits that swallow
console interrupts on Windows; they now poll on a short interval with
identical total waits, so one Ctrl+C always lands. - Broken external assets link to their real URL and are retried — a
page rewrite no longer waits on a background pre-warm download's
remote URL (which produced garbage like/https://...in the HTML and
404s on serve); it joins the running job, falls back to a synchronous
fetch, and failed URLs are retried on every build (no negative caching).
Also fixes an executor-construction deadlock when the first external
asset was submitted from a rewrite. - BCP 47 search locales map to supported languages — per-locale
search indices used raw locale tags (zh-tw,pt-BR), which Marz
indexed with generic whitespace tokenization and warned about. They now
map to their base language (zh-tw→zh), keeping real segmentation.
Fixed
- Search results highlight full query words — result snippets used raw
Marz index terms, which are stemmed (docsforge→docsforg), marking
partial words. They now highlight the matched query terms, the same
mechanism as?h=highlighting. - Demo tag index renders again —
tags.mdused adocsforge/themes/material/tagsdirective the tags plugin never matches
(listings_directiveismaterial/tags), so the page showed only stale
handwritten sections. It now uses the real directive and drops the manual
sections the generated listing supersedes. - Auto-nav uppercases Roman numerals — directory sections like
part-i
rendered as "Part i". Standalone Roman-numeral words now render uppercase
("Part I" … "Part V"). - Auto-generated nav uses real titles — without a
nav:key, pages
became empty sections labeled with raw filenames (my-page.md) plus
shorthand deprecation warnings. Auto-nav now emits explicit entries so
page titles resolve frontmatter → H1 → filename, and directories become
sections with humanized names. The demo site drops its explicit nav and
exercises this path. - Studio update flow leads with information — when engine and extension
updates are both available, one dialog shows both versions up front with
per-channel release notes before any pick; the method pick then executes
directly. Release notes open in VS Code's Simple Browser (external
browser as fallback), and extension notes link the real release tag
(prerelease notes previously pointed at a nonexistent tag). - Studio audit fixes — an unrelated open folder can no longer shadow the
configured project (init wizard included); cancelling a build resolves
quietly instead of reporting failure; links and footnotes inside fenced or
inline code no longer produce diagnostics, jumps, or rewrites; dismissal
keys and auto-check popups are per-channel freshness aware; offline
messaging dates the older channel; preview/docs links share the in-editor
browser fallback; the link picker fixes the exact occurrence; icon caches
invalidate on interpreter changes; broken explicit interpreters warn once
and share one validator;pickLinkFixis registered; full-document ranges
use exact positions; caches dispose on deactivate. - Update flow asks once — the remembered install is reused silently;
the picker appears only with no prior valid choice (the environment
command still always asks). The "don't ask again" dismissal now
distinguishes cached from fresh data, up-to-date messaging says when it
rests on cached versions, a broken explicitpythonPathwarns once per
session instead of silently falling through, and dead update-state fields
are removed.
Optimized
- Git dates are cheaper — every built page used to spawn 3 git
processes (rev-parse+ twologwalks, including a full-history--follow), costing ~12s of a 19s demo build. Nowrev-parseruns once,
one--followwalk yields both dates (its first line always equalslog -1, verified across all docs/demo pages), and pages pending render
are prefetched through a bounded pool. Same per-file git commands, so
output is identical. Demo full build: 14.9s → 11.9s. - Locales no longer crash on filename case — BCP 47 tags are
case-insensitive, but the theme translation import was a strictpartials/languages/<locale>.html: configuringzh-twmissed the
shippedzh-TW.htmland aborted the build. The import now resolves
Python-side (exact, then case-insensitive, thenen), with anen
fallback for hand-built render contexts (search/blog translators pass
the theme language explicitly).
[13.0.1] — 2026-09-17
Added
- Studio pip and install handling — installs and upgrades add
--break-system-packagesautomatically on PEP 668 externally-managed
interpreters (Debian/Ubuntu system Python),--userinstalls keep their
flag, and every flow asks which DocsForge install to use when several
interpreters have one (remembered choice pre-selected). The update flow
asks at most one confirmation per item (quick-pick choices execute
directly; release notes re-show the same dialog instead of recursing).
Fixed
- Parallel builds no longer merge navigation highlights — concurrent
page renders shared one nav tree, so templates snapshotted other pages'activeflags mid-render: sibling sections lit up together (old and new
pages merged in the sidebar) and duplicate sticky section headers
overlapped each other (46 of 50 pages on an 8-worker build).active
reads during a render now resolve per thread to exactly the rendering
page, its ancestors, and its locale counterparts — full parallelism
kept, serial output restored. Covered by a barrier-forced-overlap
regression test. - Locale output stays correct under parallelism — translated-nav
highlights are registered explicitly per render, and page templates read
the page-context locale first instead of the shared config value (which
could carry another page's locale). - Service worker stops stampeding first loads — the manifest sync pool
was unbounded (a bookkeeping bug removed the just-added task instead of
the finished one), so activation fired hundreds of concurrent no-cache
fetches; manifest refreshes fired on every navigation instead of real
reloads only. Both fixed; access-time writes no longer block responses,
and LRU tracking now records URL-string cache keys correctly. - Sidebar height clamp rescoped — the footer-overlap guard applied from
60em, clipping the full-height nav drawer on tablets; it now applies at
screen+ for both rails and tablet-landscape for the TOC rail only. - build_frontend.py refuses drifted dependencies — the script used to
build happily with stale node_modules (older icon artwork, older
mermaid/wasm) and even overwrote newer vendored sources. It now fails
fast with apnpm installhint, icon refresh is an explicit--refresh-icons(normal builds never touchsrc/), output syncs prune
deletions, and an empty twemoji fetch aborts instead of wiping the set. - Studio translation diagnostics replace the check command — missing
locale variants and orphan translations are now editor errors on the
file itself (refreshed with diagnostics, no command to run), soDocsForge: Check Translation Twinsis removed. Editable installs are
left out of update checks entirely (no engine update is ever offered for
them).DocsForge: Open Outputis removed (the Output view stays in the
sidebar). User-facing wording says translations, not twins.
[13.0.1b1] — 2026-09-17
Added
- Studio page workflows —
DocsForge: New Pagescaffolds a doc
(folder defaulting to the active file's directory, optional locale twin
stubs, textualnav:insert with section picker), andDocsForge: Check Translation Twinslists missing/orphan locale variants with one-click
stub creation or orphan deletion. - Studio completions for anchors, snippets, frontmatter —
#inside a
link completes target-document heading slugs (served by an
mtime-validated index at.docsforge/studio/headings.json),--8<-- "…"completes sibling and docs-tree files, and frontmatter
offers engine-backed keys plushide:/search:values (custom keys stay
legal and are never flagged). - Studio multi-root servers — each workspace folder gets its own
serve/build state and pidfile adoption; commands act on the active
file's project (with a picker when ambiguous), the status bar names the
folder, and Open Built Page uses the document's own server. - Studio offline update cache — successful version checks persist to
global state; offline manual checks reuse the last-seen versions labeled
cached, and the silent startup check refreshes only the sidebar badge
from cache instead of notifying.
Fixed
- Studio review fixes — New Page never overwrites an existing
translation with a stub (skipped twins are reported); rapid double
serve/build can no longer orphan a server process; build failures reject
with real errors instead of unhandled rejections;./and../link
prefixes complete; Rename Anchor rewrites same-page[text](#anchor)
links; slugs keep CJK (# 你好→你好); Format Document leaves fenced
code blocks alone;docsforge.formatOnSaveis honored via will-save;
Rename Document rejects escaping paths; ambiguous quick fixes ask instead
of guessing; mixed fresh/cached update rounds are labeled per channel;
dashed prereleases with local metadata parse; go-to-definition and hover
use the link under the cursor; Stop Server no longer hijacks to cancel
builds; pidfile PIDs must be numeric and diagnostics paths stay inside
the docs dir. Locales also match case-insensitively (pt-BR). - Second Studio review pass —
:icon:completions resolve through the
workspace interpreter's installed package (no longer always empty in user
projects), plus frontmattericon:values; cached offline versions are
display-only instead of offering doomed updates; snippet completion scans
the whole cached file list; explicit errors when no folder is open;
update checks share serve's project resolution; anchor completion is
case-insensitive with deduped titles; formatOnSave reads document-scoped
config; frontmatter skips the closing---and snippet completion is
suppressed in fenced code; scaffold folder validation is segment-wise.
Fixed
- Studio engine updates for beta/editable installs — the extension
truncated13.0.0b3to13.0.0, so a beta checkout looked current and
only the extension update was ever offered. Pre-release segments are now
preserved for comparison, editable installs (pip install -e .) are
detected viadirect_url.jsonand never silently replaced by pip (the
prompt names the checkout and offers an explicit replace), user-site
installs keep the--userflag on upgrade, and failed upgrades print the
manual retry command.
[13.0.0] — 2026-09-15
Highlights
DocsForge 13 is a correctness and performance major. Everything below
shipped across the 13.0.0b1–b3 betas (see those entries for the full
per-fix detail); since b3 only the updater messaging fix and a Studio
docs rewrite landed.
- Parallel incremental builds that converge — page rendering actually
usesconcurrencynow, snippet-include pages no longer rebuild forever,
static copies are incremental, and manifests are compare-before-write. - Chinese navigation correctness — own homepage, prev/next chains,
preservedi18n_titles, per-page descriptions no longer clobbered. - Offline-proof assets — one Google Fonts fetch per build with local
font fallback, atomic file writes, missing outputs restored, social
cards no longer deleted as "unused". - Calmer serve and CLI — interruptible rebuilds, 404s logged as info,
friendlier help/check/setup output. - Self-updating Studio extension — engine + VSIX updates from the
palette, sidebar, or a silent startup check, with pre-release opt-in.
Behavior changes
- Non-default-locale pages keep their own frontmatter/H1 titles and
descriptions; nav-label translations no longer rewrite them. - Dev-server 404 request logs moved from error to info level.
- Pre-release versions are excluded from update checks unless
docsforge.includePrereleasesis enabled.
[13.0.0b3] — 2026-09-15
Added
- Studio update check in the sidebar — the Actions view has a
Check for Updates item (plus a title-bar button) that shows the available
versions inline once a check finds something newer.
[13.0.0b2] — 2026-09-15
Added
- Studio can update itself — new
DocsForge: Check for Updatescommand
compares the installed engine (PyPI) and extension (GitHub releases)
against the latest versions, upgrades the engine with pip in the same
interpreter setup, and downloads + installs a newer VSIX with a reload
prompt. A silent startup check (45 s delay, once per version, off viadocsforge.autoCheckUpdates) notifies only when something is newer, anddocsforge.includePrereleasesopts betas into the check.
[13.0.0b1] — 2026-09-15
Changed
- Mermaid vendored asset upgraded from 11.17 to 12.0 — ELK is now the
bundled layout engine upstream,</br>is treated as a line break in
labels, and DocsForge pins the mermaid-11 appearance (layout: dagre,theme: default,look: classic) so existing diagrams keep their look;
per-diagram front matter overrides still win. The IIFE grows ~600 kB
gzipped (ELK inlined — counted in the precache manifest), and the CDN
fallback URL bumps tomermaid@12. Verified: all nine core diagram types
(flowchart, sequence, class, state, ER, gantt, pie, mindmap, journey)
render in Chromium with the pinned config, andmermaid.initialize()
accepts DocsForge's config unchanged.
Fixed
- Missing outputs are restored instead of staying gone — cached social
cards absent from the site (deleted by older optimizer revisions or by
hand) are copied back inon_post_build; they used to stay missing
forever on incremental builds because up-to-date cards are skipped and
only rebuilt pages copy. A post-optimization pass now does the same for
every other expected static output with a live source, while pages
(rebuilt when their output is missing) and optimizer-managed directories
(deliberate pruning) are never touched. - Social cards no longer deleted as "unused assets" — card PNGs are
referenced only via absolute same-originog:image/twitter:imagemeta
tags, which the reference scanner neither collected (nometahandling)
nor resolved (absolute URLs were dropped as external). The cleanup then
deleted every generated card in the same build. Meta image/video/audio
references are now collected and same-origin absolute URLs resolve
site-relative (foreign origins stay excluded); the reference cache is
keyed by origin so build/serve scans can't leak into each other. Help output tweaks — no emoji, no examples block, and command blurbs
shortened to fit instead of truncating mid-word.Dev-server 404s are info, not errors — every missing file (typo'd
links, favicon probes, drafts) used to log a redERRORline; 404s now
log at info level (other 4xx warn, 5xx stay errors). The log prefix also
no longer paints the-separator red/yellow along with the level name.Friendlier CLI output —
docsforge --helpshows examples and a
colorful command list,docsforge checkreports with green/red/yellow
status lines and hints, and the setup wizard uses color for its banner,
steps, errors, and summary.Builds survive slow or blocked networks — the social plugin retried an
unreachable Google Fonts host once per page (94 serial connect-timeout
cascades, one error per page, minute-long stalls). Timeouts are unchanged,
but each family is now fetched at most once per build: on failure cards
render with a best-effort local font (other cached families first, then
system fonts matched by style) after a single warning, and the next build
retries the fetch. Only when no local font exists at all do pages build
without cards. Card/layer pools also shut down without waiting on stuck
network workers, so a single Ctrl-C stops the build promptly instead of
needing a second one.Parallel page builds actually run in parallel —
_build_pageheld one
shared lock across template rendering and file writes, soconcurrency
was a no-op. The lock now covers only shared state (page.active,config._current_page,get_contextgit-meta writes, plugin hooks);
rendering and atomic writes run outside it.activeclearing is
sibling-aware and_current_pageis re-asserted per hook, so concurrent
builds no longer flicker each other's sidebar highlight.- Snippet-include pages converge on incremental builds — freshness checks
used plugin-only deps while the cache recorded snippet deps, so any--8<--page rebuilt forever; empty dep lists were also dropped. Both
check paths now compute snippet deps up front and share them withupdate_cache, which persists empty lists and prunes removed deps. - Incremental I/O no longer churns — static files copy with
dirty=True
and size+mtime_ns comparison, theme/extra/manifest outputs are
compare-before-write, orphan scans exclude generated outputs (assets/,
sitemap, search index, PWA files) and consult knowndest_uris, and all
file/cache writes are atomic (tmp +os.replace). - Chinese nav correctness — locale navs use their own homepage (not the
English object), get their own prev/next chain, preservei18n_titleson
translated and fallback pages (including already-materialized pages),
no longer clobber per-page descriptions or rewrite doc titles from nav
labels, recompute locale titles idempotently, and highlight fallback
entries bysrc_uri. - Social, search, tags and minify robustness — card layer dispatch is
lock-guarded (no sentinel deadlock), font downloads time out and pools are
created only when enabled, missing</head>no longer corrupts output,
search exclusion tracks element identity (nested same-tagdivs stay
excluded), tag listing IDs are regex-escaped with correct fragment
handling,?v=hashes survive serve rebuilds, and meta files sort once
with/-normalized paths. Serve rebuilds are interruptible and race-free —
KeyboardInterrupt/SystemExitre-raise instead of being swallowed, rebuild flags mutate
under lock, bind retries useerrno.EADDRINUSE, customsite_dirs are
ignored by path (not a hardcodedsite), wildcard hosts probe loopback,
privacy downloads drain until quiescent with errors surfaced, oversized or
undecodable assets are skipped safely, the config cache keys on(mtime_ns, size), shutdown joins the observer, andcheck --fixwrites
atomically.Config loading no longer stalls ~13s on the emoji index — the twemoji
index builder rebuilt the set of all known names once per vendored icon
file (O(N²) over ~10k SVGs), blocking every freshbuild/serve/check
invocation with no output. Taken names are now tracked incrementally;
cold load dropped to well under a second with identical results.Reference docs no longer swallow headings and diagrams into code blocks
— themarkdownexample fences in the diagrams, content-tabs,
admonitions, lists, creating-your-site, publishing usage, and
troubleshooting pages used triple backticks while containing triple-backtick
inner fences, so the outer block closed early and the following headings,
tables, and live diagrams rendered as code. The outer fences are now
quadruple backticks (English and Chinese twins fixed together).Translated tabs keep their icons on incremental builds — the tab-bar
icons come from each section index page's front-mattericon, but only
default-locale sources were preloaded before template rendering, so a
rebuilt Chinese page outside setup/reference lost the setup/reference/home
icons. Locale nav sources are now preloaded too, and the navigation
signature covers locale titles and icons so zh-only changes rebuild
correctly.Interrupting
serveduring the initial build no longer hangs — the
privacy download pool had no shutdown hook, so Ctrl-C left network threads
alive and the interpreter blocked joining them until every download timed
out. The pool is now cancelled on shutdown (and reaped on the nexton_config), HTTP responses are always closed, page-build executors cancel
queued work on interrupt, the cache-invalidate message prints once instead
of twice, and an unversioned partial cache reports asunversionedrather
thanv0. Long first builds also logRendering N pages.../Writing N pages...so slow progress is distinguishable from a hang.
[12.5.7] — 2026-08-22
Added
- Ruff lint gate in CI and release —
ruff checknow runs as its own job
in both the CI and release workflows, and the release is blocked on it, so a
correctness regression cannot ship. The gate covers both correctness
(pyflakes, bugbear, pylint errors) and style (pycodestyle, isort,
pep8-naming, pyupgrade, flake8-simplify and friends): DocsForge no longer
tracks upstream ProperDocs / Material for MkDocs, so the tree is held to one
consistent house style — double quotes, a 120-column limit, sorted imports,
no relative imports beyond the parent package. Every ignored rule and
per-file exemption is justified in[tool.ruff.lint]inpyproject.toml. A
newpip install -e '.[dev]'extra installs the exact pinned ruff version CI
enforces.
Fixed
The template parity gate now catches added files — CI verified the
committeddocsforge/templatestree against a freshbuild_frontend.pyrun
withgit diff --exit-code, which reports modified and deleted files but is
blind to new ones. A dependency that adds assets (such as alucide-static
bump shipping nine new icons) left them untracked and still passed, so the
committed tree silently drifted from the build output. Untracked build output
is now a CI failure that names the exact files to add.A VS Code typings bump can no longer break the release — the CI
studio
job compiled, linted and tested the extension but never packaged it, whilevsce packageenforces constraints the compiler does not — notably that@types/vscodenever exceedsengines.vscode. Because packaging only ran in
the release workflow, after the tag and the PyPI publish, such a bump would
have produced a published release with no.vsixattached. CI now packages
the extension, and@types/vscodeis pinned to theengines.vscode
compatibility floor rather than tracked by Dependabot.Dev-server path containment is now a tested boundary — the live-reload
server's directory-traversal guard was correct but inline, undocumented and
entirely untested: the one place where a request path becomes anopen()
call had no regression net. It is now_resolve_within_root(), a single
documented method that returnsNonerather than a path when a request would
escape the site root, with an explicit rejection of any..surviving
normalization as defense in depth. Eight unit tests pin the boundary against
traversal, percent-encoded traversal, root-prefix sibling directories
(/sitevs/sitezz), escaping symlinks, and absolute paths that would
otherwise hijackos.path.join. No behavior change: every path that resolved
before still resolves, and every path that was blocked is still blocked.Social cards no longer leak between builds —
SocialPlugin.manifestwas
a class attribute that instance code mutated, so every plugin instance shared
one dict and card entries accumulated across rebuilds within a singledocsforge servesession. It is now per-instance.Translated pages keep their index-page identity — a translation sibling
such asindex.zh.mdwas treated as a regular page instead of an index
page, so on multi-language sites the top navigation tabs lost their icons
in non-default languages (the icon comes from the tab's index page), and
translated section indexes were not merged into their section withnavigation.indexes. Translation siblings now share the default file's
stem identity, soindex.zh.mdbehaves exactly likeindex.md.privacyno longer crashes when a symlink cannot be created — on
filesystems without symlink support (notably Windows without developer
mode) the fallback path that writes the downloaded asset directly readres.content, which raisesRuntimeError: The content for this response was already consumedbecause the download had already been streamed to enforce
the size limit. The already-streamed bytes are now written instead.- PWA manifest and
theme-colorcarry a real color — both emitted the
Material palette name (e.g.teal) where a CSS color is required, so the
browser ignored the value and no theme color was applied. Palette names are
now resolved to their actual hex color, with literal CSS colors passed
through unchanged. - Builds work on Python 3.10 — a parallel page-render failure raised
ExceptionGroup, a 3.11+ builtin, so on 3.10 (whichrequires-python
allows) the error handler itself raisedNameErrorand masked the real
build error. A compatible fallback is used below 3.11. - TikZ compilation cannot hang the build — every LaTeX and SVG-converter
invocation now runs under a timeout (120s, overridable withDOCSFORGE_TIKZ_TIMEOUT). A malformed diagram that put TeX into an
interactive prompt previously blocked the build forever, since the
compilation runs inside a thread pool the build waits on. - TikZ compile counters are thread-safe — the compiled/skipped counters
and the content-hash map were mutated from the compile thread pool without
synchronization, so the reported counts could be wrong and hash entries
could be lost, causing unchanged diagrams to recompile on the next build. - Stale build caches are discarded on format change —
CACHE_VERSIONwas
never compared against the stored version, so a cache written by an older
format revision was read back as if current. It is now invalidated when the
format changes. cache.invalidate()clears every cache file —validation.jsonwas
missing from the list, so link/anchor validation data survived an
invalidation. All cache files now come from a single source of truth.docsforge fixbacks up your config — the auto-fix rewrotedocsforge.ymlthroughyaml.dump, silently discarding comments, blank
lines and quoting style. It now writes a.bakcopy first and says so.- Social card manifest errors no longer swallow interrupts — a bare
exceptaround the manifest read caughtKeyboardInterrupt, so Ctrl-C
during that read was ignored.
[12.5.6] — 2026-08-20
Added
- Offline mode toggle — a new
offline.modeconfig option lets you switch
the service worker off entirely. The defaultcache-firstmode is unchanged
(service worker registers, precaches every page and asset, and serves from
cache first for offline support). Settingoffline.mode: noneskips the
service worker entirely: nosw.js, nocache-manifest.json, nomanifest.json, and pages carry no registration code or manifest link — the
site is served purely from the network.
Fixed
- Theme signature is location-independent — the theme template signature
used absolute file paths, so the same templates installed at a different
path (e.g. GitHub Actions Python toolcache updates, or a reinstall into a
new environment) produced a different signature and invalidated the whole
build cache on every CI run, defeating incremental builds. The signature
now hashes template contents with paths relative to each theme directory,
so identical templates always yield the same signature regardless of where
they are installed.
[12.5.5] — 2026-08-20
Added
- File sizes in
cache-manifest.json— the manifest now ships asizes
map with the exact byte count of every built file. The service worker uses
it (plus the response'sContent-Length, measured from the body when
missing) to account quota evictions at their real size instead of the old
guessed 5 MiB per entry.
Changed
- Service worker quota eviction is exact — when the browser cache is
full, the SW evicts least-recently-used entries until the actually-freed
bytes (measured, not estimated) cover the required space plus a
proportional 10%-of-quota margin, then retries. The old flat 20 MiB free
margin is gone. Evicted entries are removed from the persisted
previous-files list, so a file that was evicted is never falsely recorded
as cached — the next sync re-fetches it instead of skipping it. - Manifest sync is budget-aware — the background sync now checks the
free space reported bystorage.estimate()before downloading: every
changed file reserves a flat 20 MiB of budget (the usage estimate lags
behind in-flight writes), and the sync stops once the budget is exhausted
instead of downloading files that would only be evicted again. Files that
don't fit are cached on demand when actually visited. A single resource
larger than the whole quota is never cached.
Fixed
- Safari (and any small-quota browser) no longer thrashes — previously
the sync downloaded the whole manifest, hit the quota wall, evicted the
just-cached files, and recorded them as cached anyway; offline coverage
silently degraded to a few tail files. Now the sync stops early and the
tracked-file list stays truthful. - TikZ diagram cache is content-based — unchanged diagrams are now
skipped by hashing the effective source (tex plus preamble) instead of
comparing file mtimes. Git checkouts and CI cache restores bump every
mtime, which previously recompiled all diagrams on every build; edits totikz_preamblenow also correctly trigger recompilation (mtime never
noticed preamble changes). - Theme signature is content-based — a full rebuild is triggered by
actual template content changes instead of stat (mtime/size) differences.
Reinstalling the docsforge package or restoring a CI cache stamps fresh
mtimes on identical theme templates; previously that invalidated the
whole build cache on every CI run, defeating incremental builds.
[12.5.4] — 2026-08-18
Added
- Twemoji SVGs vendored — the full twemoji set (4,000+ emojis, pinned
tojdecked/twemojiv17.0.3, the maintained fork of the archivedtwitter/twemoji) ships inside the package. Unicode emojis
(:smile:,:us:) are now inlined locally at build time, so no CDN
(jsdelivr/maxcdn) is referenced anymore — falling back to the CDN only
for codepoints missing from the vendored set. - License files shipped with bundled assets — Mermaid (MIT) and KaTeX
(MIT) licenses travel alongside the bundled scripts, and the lunr
stemmer modules ship their MPL-1.1 license (the license must accompany
the code in all cases). - Docker image runs as non-root — the image now creates a
docsforge
user (uid 1000) and drops privileges (USER docsforge). - TikZ default math preamble — bare diagram sources (no
\documentclass)
are now auto-wrapped in a standalone document withamsmath,amssymb,tikz,pgfplots,tikz-cdandtkz-euclidepreloaded, so a diagram is
just thetikzpicturebody. Newtikz_preambleconfig option injects extra
preamble lines.texlive-fonts-recommended(amssymb/amsfonts) added to the
Docker image — pgfplots, tikz-cd and tkz-euclide already ship intexlive-pictures. - TikZ SVGs embed fonts —
dvisvgmno longer runs with--no-fonts, so
diagram text stays selectable and searchable; it falls back to outlined
paths only when the TeX fonts are unavailable. - TikZ compilation no longer times out — the fixed 60-second per-step
limit is removed; large or slow diagrams (pgfplots surfaces, heavyforeachloops) compile for as long as they need.
Changed
- Global
concurrencysetting — new top-levelconcurrencykey
(default: CPU count − 1) sizes every parallel pool: Markdown page
rendering, page building, TikZ compilation, social card generation and
privacy downloads. The plugin-levelconcurrencyoptions ofsocialandprivacyare removed — setconcurrency:at the top level instead
(breaking; plugin-level usage now warns). - PDF export tabs are memory-capped — the base tab count comes from
--jobs, else globalconcurrency(default CPU count − 1), and is capped
by remaining memory (≈200 MiB per Chromium tab) so parallel rendering
can't OOM the build. scripts/fetch_twemoji.pymerged intobuild_frontend.py— refresh
the vendored twemoji set withpython build_frontend.py --fetch-twemoji
(pinned tag), then a normal build syncs it intodocsforge/templates/.- Migration scripts now served from the docs site —
migrate.sh,migrate.ps1andmigrate.pylive in the docs content and are copied
into the built site (docsforge copies static files fromdocs_dir), so
the one-liners are now:
curl -fsSL https://qqshi13.github.io/docsforge/migrate.sh | bash
irm https://qqshi13.github.io/docsforge/migrate.ps1 | iex
The scripts download migrate.py from the same site
(https://qqshi13.github.io/docsforge/migrate.py) instead of the raw
GitHub URL, and the getting-started migration guide now documents the
automatic one-liner (was: "no automatic migration command").
[12.5.3] — 2026-08-17
Added
- One-line migration scripts:
curl | bash(Unix) andirm | iex
(PowerShell) convert an existingmkdocs.yml/properdocs.yml/zensical.tomltodocsforge.ymlautomatically — navigation, theme,
plugins, and extensions. Warns about anything it can't migrate
(third-party plugins,INHERIT, hooks) and prints a report with contact
info (email + issues). !!python/name:tag support in config loading — mkdocs configs
commonly useslugify: !!python/name:pymdownx.slugs.uslugify; such
configs (e.g. OI-wiki's) now parse and round-trip cleanly.- SECURITY.md — private vulnerability reporting (GitHub advisory or
email), supported-version policy. - Support page (en + zh) with the migration one-liner, support channels,
and a "what to include" checklist. - Sidebar navigation icons on every docs page (Material set, en + zh).
Changed
- minify backend is now the Go minifier (
minify-go, wheels built fromtdewolff/minifyby our own cron workflow, incl. macOS arm64): the
maintained forks (min-html, min-js, csscompress) are consolidated into one
dependency. HTML pages now also minify inline<style>/<script>blocks;
SVG default-valued attributes (e.g.preserveAspectRatio="xMidYMid meet")
are stripped, non-default values and case-sensitive names (viewBox) are
preserved. Note: musllinux/Alpine is unsupported by the Go binding
(c-shared.socannot be dlopen'd on musl). paginatevendored into the package (docsforge/paginate.py) —
another dormant dependency (2017) removed from the install.- README: migration one-liner added; broken
install.sh/install.ps1
references fixed (the Studio extension ships as a release.vsix).
Fixed
- Config parsing in
docsforge check/ CLI / PDF now routes through the
docsforge YAML loader, so!!python/name:tags no longer crash config
discovery. - Migration script:
theme.name: null(local-theme setups) defaults tomaterial; external nav links are dropped with a hint (docsforge nav is
internal-only).
[12.5.2] — 2026-08-12
Added
- Blog RSS/Atom feeds: the blog plugin now generates
feed_rss_created.xml,feed_rss_updated.xml, andfeed_atom.xmlinto
the built site on every build (drafts excluded) — no more hand-maintained
feed files. Disable withplugins: [blog: {feed: false}]. - Docs reference expansion: 9 previously undocumented config keys
(exclude_docs,draft_docs,not_in_nav,extra_templates,tikz,hooks,watch, legacyremote_branch/remote_name),social+i18n
plugin option tables, a Lucide icon section, social-cards plugin docs, and
the zero-configextra.i18n_languagesform (en + zh).
Fixed
- Blog entrypoint stayed stale on incremental builds: adding, editing, or
removing a post did not re-render the blog index when the entrypoint's own
source was unchanged. Newon_page_depsplugin event + dependency-set
change detection keep blog views fresh; no-op builds still skip. - Duplicate built-in plugin loading: declaring a built-in plugin
(plugins: [blog: {...}]) loaded a second auto-loaded instance with
default config because the dedup check compared the un-namespaced name
against the namespaced instance counter. Every declared core plugin was
silently duplicated — now deduplicated correctly. - Demo content: stale "31 extensions / 7 plugins" claims corrected;
i18nadded to the plugin list in the launch post.
Changed
- Dependency floor raised to current releases: click 8.4.2, Jinja2 3.1.6,
Markdown 3.10.3, PyYAML 6.0.3, watchdog 6.0.0, pymdown-extensions 11.0.1,
backrefs 8.0, Pygments>=2.20.0(itsdefaultstyle output matches the
committedpygments.css), and all other runtime deps; extras include
playwright 1.62.0, pypdf 6.15.0, pillow 12.3.0, cairosvg 2.9.0.
[12.5.1] — 2026-08-12
Added
- Incremental PDF export cache:
docsforge build --pdfnow skips pages
whose built HTML is unchanged since the last export (per-page content
hashes in.docsforge/cache/pdf.json, the same mechanism as the site
build). A single edited page re-renders only itself; orphaned PDFs are
removed; only successfully rendered pages are recorded as cached. on_build_donehook event: runs once at the very end of a successful
build — after page rendering, link/anchor validation,on_post_build
plugins, asset optimization, PWA manifest + service worker generation, and
the cache save — so hooks can inspect or post-process the final build
output (e.g.sw.js,cache-manifest.json). Skipped when the build is
aborted in strict mode.
Changed
- E2E tests honor
PLAYWRIGHT_CHROMIUM_EXECUTABLE: browser launch sites
now use the same env var asdocsforge.pdf, so a system browser can run
the e2e suite locally without Playwright's bundled Chromium. Unset, the
suite falls back to the bundled browser (CI) and skips gracefully when
neither is available.
Fixed
- Docs corrections: plugin count (8 core auto-load + social opt-in) and
Markdown extension count (36 default + 3 built-ins) now match the engine;
TikZ claims corrected (.texfiles compiled at build, requires a LaTeX
toolchain); stale "Insiders" references removed from the migration guide
(mkdocs-material is migrating to Zensical); zh changelog backfilled for
12.2.0–12.4.0; broken zh self-anchor fixed; icon counts corrected
(16,500+ icons across five families). - Example-site integration test pointed at the pre-12.5.0
examples/site
path and silently skipped — now runs againstexamples/sites/docsforge-demo. - Demo site: three new blog posts (TikZ diagrams, suffix-mode i18n,
DocsForge Studio diagnostics); removed an orphaned page;latex-equations.md
added to the nav; TikZ figures now resolve correctly.
[12.5.0] — 2026-08-11
Added
- DocsForge Studio (VS Code extension,
studio/): renamed fromvscode-docsforge. Full editor intelligence without a language server —
diagnostics from the build'svalidation.json(broken links/anchors,
footnotes), outline, folding, definition (ctrl+click), hover with
broken-link notices, completion (:material-/:lucide-icons + doc
paths), Find All References, and "Rename Document"/"Rename Anchor" that
rewrite every inbound link (anchor-aware, translation-aware — renaming a.zhfile renames its base + all locale variants). Explorer renames are
intercepted automatically. "Fix all broken links (N)" code action, open
link target, "Open Built Page" at the serve URL, format-on-save
(opt-in), output panel inside the sidebar view. - Python environment management in Studio: detects an interpreter
(setting → remembered venv →.venv→ PATH), checks pip + docsforge,
offers venv / user / global installs. - Apache-2.0 license: switched from LGPL-3.0-or-later;
NOTICEwith
upstream attribution (ProperDocs/MkDocs BSD-2, Material MIT, icons). - Vendored KaTeX + Mermaid as manifest-tracked devDependencies
(previously frozen / hardcoded CDN):copy_katex()+copy_mermaid()in
the frontend build; Mermaid loads from the local asset with a CDN fallback. - pygments.css generated at build time from the installed Pygments
(was a frozen snapshot). - Studio CI coverage:
ci.ymlgained astudiojob (npm ci + compile +
lint + test) and absorbedfrontend.ymlas afrontendjob.
Changed
- Social plugin is opt-in: removed from the always-loaded core plugins
(it needs pillow + cairosvg); enable viaplugins: [social]. Docs and
demo configs updated accordingly. - Link/anchor validation fixes: anchor problems are now persisted to
validation.json(they were logged but never stored — the Studio
diagnostics had nothing to show); fixed a shared class-levellink_warningslist that duplicated warnings across every page. - Renamed directories:
vscode-docsforge/→studio/,docsforge-docs/→docs/,examples/site/→examples/sites/
(gitignore negation removed — the globalsite/rule covers it now). - TypeScript 6.0.3 in Studio (TS 7 is outside the typescript-eslint
peer range). - Demo deploy: wrangler-action v4 (no pinned wrangler version).
Fixed
- Social card font fetch could hang builds: Google Fonts requests had no
timeout (reverted, upstream-style). The default cache moved to.docsforge/cache/social. - Demo pipeline: fixed the unprivileged
rm -rf /var/lib/apt/lists/*
that failed every run;neural-network.texneededamssymb; added the
missingshannon-state-machine.tex.
[12.4.0] — 2026-08-09
Added
- Social cards plugin (
social): vendored port of mkdocs-material's
social plugin, flattened intodocsforge/core/social.py(OpenGraph card
generation with two-stage parallel rendering, cache + manifest, font
download). Each i18n locale page gets its own card. Needspip install "docsforge[social]"(pillow + cairosvg). - Lucide icon set: 2022 icons, usable as
:lucide-name:inline or in
theme icon configs; stroke-based rendering support for.md-icon. - Link/anchor validation on every build: link and anchor problems are now
reported on every build, not only the first — validation data is persisted
per page in the build cache and re-checked for pages that are not re-rendered. - Programmatic palette API:
window.docsforge.setPalette({scheme, primary, accent})applies and persists themes from inline scripts/embeds (e.g. the
theme playground), including combinations with no header toggle. - Examples: the demo site now lives in-repo (
examples/site/docsforge-demo),
with acustom_diroverride, a custom-filter plugin example, a hooks
example, and a CI build test; the demo deploys to Cloudflare Pages via
GitHub Actions (with TeX Live for TikZ).
Changed
- Repository hygiene: all vendored mkdocs-material MIT headers removed
(attribution consolidated in the license page); parity tooling removed;
frontend reproducibility enforced in CI and at release time. - The service worker revalidates the manifest on every real page load
(Navigation Timing API) — a redeployed site is picked up without a hard
refresh; the 5-minute update poll and the hard-refresh monitor were removed. - Sidebar height uses
dvhso the left (nav) and right (TOC) sidebars
scroll identically on iOS/iPadOS.
[12.3.0] — 2026-08-06
Changed
- Repository flattened. The monorepo
packages/level is gone: the core
package,pyproject.toml,src/,tests/, and the frontend pipeline live
at the repo root; the docs site isdocsforge-docs/and the VS Code
extension isvscode-docsforge/. All workflows, Dockerfile, dependabot
config, and docs links updated; the full package README is now the root
README. - svgo 4. The icon pipeline now uses svgo 4 (config migrated;
viewBox
preservation unchanged). Verified byte-reproducible build output. - pnpm 11 for the frontend build (the workspace file uses pnpm 10+ syntax
that pnpm 9 rejected), with theminimumReleaseAgesupply-chain gate
disabled so dependabot bumps to freshly published versions pass CI. - Frontend CI is reproducible again:
frontend.ymlfails if the build
output differs from the committeddocsforge/templates/, and the release
workflow rebuilds the frontend fromsrc/before publishing the wheel. - The transition-time parity harness and baseline snapshot were removed
(their job is done);build_frontend.pymoved to the repo root. - CI runs the full test suite, including a dedicated Playwright e2e job.
[12.2.0] — 2026-08-06
Added
- Frontend built from source. The Material theme is vendored as source
undersrc/(mkdocs-material v9.7.7) and built by an
in-repo pipeline (scripts/build_frontend.py): esbuild bundles, sass +
autoprefixer CSS, svgo icons, minified templates and service worker. Every
shipped asset is reproducible, and parity gates
(scripts/check_frontend_parity.py) verify it per area on CI. - DocsForge frontend customizations now live in source instead of
hand-patches to minified bundles: service worker (manifest-driven delta
sync, locale-aware page serving, offline fast-404, manifest-aware candidate
skipping), stateless i18n language switcher (IndexedDB-backed), per-locale
search index, per-locale 404 pages, and theX-DocsForge-Instant-Nav
header that keeps instant navigation on the preferred locale.
Changed
- Service worker ships minified with
__DOCSFORGE_BASE_URL__/__DOCSFORGE_BUILD_HASH__placeholders;docsforge buildinjects the real
base path and a deterministic hash so SW bytes change between builds. - The SW skips page candidates absent from
cache-manifest.json— a stored
preference for the (unsuffixed) default locale no longer requestsindex.en.html(404s). - Blog posts without a
title:front-matter key no longer crash the build
when the meta plugin is active. scripts/check_frontend_parity.pygains--vs-ref(output diff vs a git
ref, no build) and sentinel-marker checks.
[12.0.0] — Unreleased
Changed
- BREAKING: Locale-agnostic i18n architecture. Translated pages are now emitted as siblings (
index.htmlandindex.<locale>.htmlin the same directory) and share the same public URL. The service worker stores the user's preferred locale in IndexedDB and serves the matching sibling on every request. This removes/<locale>/sub-sites, fallback pages, per-locale asset copies, and link/asset rewriting. - i18n search is locale-aware. Each locale has its own
search/search_index.<locale>.json; the frontend loads the index for the currently displayed language instead of always searching the default locale. - i18n language switcher reloads the same URL. Selecting a language writes the preference to IndexedDB and reloads the current page, so the switcher never produces stale locale-specific URLs.
Removed
- i18n
fallback_to_defaultoption. It is now ignored because fallback pages are no longer generated.
[11.5.4] — 2026-07-07
Fixed
- i18n link rewriting now handles unquoted and single-quoted
hrefattributes. The HTML minifier emits attributes likehref=second/, which the link rewriter previously missed, causing locale pages to link back to the default-language site. It now rewrites double-quoted, single-quoted, and unquotedhrefvalues. - i18n
nav_translationsnow apply to Page nav items. Previously only Section titles were translated; Page nav items now usenav_translationstoo, with frontmatter titles used as the fallback when no explicit nav title or translation is configured.
[11.5.3] — 2026-07-07
Fixed
- i18n fallback pages now inherit titles and nav overrides. Fallback locale pages (and translated pages created before navigation runs) no longer render nav items as "None"; they use the default page title and any custom
navtitle configured for that entry. - Locale links now rewrite for all pages, not just nav pages. Internal links from a translated page to a fallback/non-nav page now correctly stay inside the locale subtree instead of climbing out to the default-language URL.
[11.5.2] — 2026-07-07
Added
- i18n asset fallback. Translated assets (e.g.
assets/diagram.zh.png) are emitted under the locale path (zh/assets/diagram.png). If a translation is missing, the default asset is copied there automatically, so locale sites never lose images, CSS, or other docs assets. - Per-locale Material UI language. Translated pages now load the matching Material UI string file, so
<html lang>, search placeholders, and the language-switcher label follow the page locale instead of staying in the default language.
Fixed
- False-positive nav warnings for translated pages.
docsforge buildanddocsforge serveno longer log "pages exist in the docs directory, but are not included in the nav" for translated.zh.mdfiles handled by the i18n plugin. docsforge checknow treatsmaterial/i18nas a built-in plugin instead of a third-party plugin.
[11.5.1] — 2026-07-06
Fixed
- i18n language switcher used server-absolute URLs. The alternate URLs included the
site_urlsubpath, so when the templateurlfilter resolved them relative to the current page on sites deployed under a path (e.g.https://qqshi13.github.io/docsforge/), the links pointed todocsforge/docsforge/.... The i18n plugin now emits page-relative URLs (page.url) so the switcher and<link rel="alternate">tags stay inside the docs root.
[11.5.0] — 2026-07-06
Added
- Built-in i18n plugin (
material/i18n). Add translated files next to your default-language files (e.g.index.zh.mdbesideindex.md) and DocsForge builds a default site at the root plus one sub-site per locale under/<locale>/. Supports fallback pages, per-language navigation/title translations, a header language switcher,<link rel="alternate" hreflang="...">tags, per-locale search indexes, and per-locale sitemaps. - New documentation page: Multi-language sites.
Fixed
- Material theme now ships a default
paletteso builds no longer fail whentheme.paletteis omitted. - Removed
properdocs_version/mkdocs_versionlegacy fields from the template context. - VS Code extension build restored by rolling TypeScript back to
^5.9.3and@types/nodeto^22.20.0withcommonjs/nodemodule resolution.
[11.3.12] — 2026-06-27
Added
- Browser E2E tests (Playwright). A 5-test Chromium suite (
tests/e2e/) covering the service-worker behavior that can't be unit-tested: SW installs and caches the visible page, offline reload serves the cached page,search_index.jsonis served, and hover-prefetch caches the destination so it loads offline. The tests skip gracefully when no browser is available (so the defaultpytestrun is unaffected); a dedicated non-blockinge2eCI job onubuntu-latestinstalls Chromium and runspytest -m e2e.
[11.3.11] — 2026-06-27
Added
- VS Code extension tests (10). Added a mocha + ts-node suite for the extension's pure helpers: config-file discovery (
findConfig/hasConfig,.ymlpreferred over.yaml) and server-URL extraction from stdout. Extracted the helpers into a vscode-freesrc/pure.ts(behavior-preserving) so they're testable without launching VS Code. CI now runsnpm testin thebuild-vsixjob;test/is excluded from the VSIX.
[11.3.10] — 2026-06-27
Added
- Serve / live-reload unit tests (15). Covers
_find_available_port(free, in-use increment, all-in-use, firewall-dropped-SYN with the short-timeout WSL fix),_serve_url/_normalize_mount_path/_try_relativize_path, and the rebuild-queueing logic that guards against the infinite reload loop (events during a build are queued via_pending_rebuild, not signaled).
Changed
- Extracted the live-reload file-watch callback to a testable
LiveReloadServer._on_file_eventmethod (behavior-preserving).
[11.3.9] — 2026-06-26
Added
- Reproducible builds via
SOURCE_DATE_EPOCH.get_build_datetime()now honors the standardSOURCE_DATE_EPOCHenvironment variable (reproducible-builds.org). When set, the build date, pageupdate_date, sitemap<lastmod>, andsitemap.xml.gzmtime are all derived from that timestamp instead of the wall clock — so two builds of the same source produce byte-identical output (verified: identical content hash across clean builds). Defaults to the current time when unset.
Changed
- Search index entries are now sorted by location before serialization, guaranteeing byte-reproducible
search_index.jsoneven if the build loop populates entries in non-deterministic order.
[11.3.8] — 2026-06-26
Fixed
- Template edits now trigger a full rebuild. The build cache only tracked source-
.mdhashes, the config hash, and the package version — so editingbase.html, a partial, or atheme.custom_dirtemplate did not rebuild unchanged pages (stale output). The build now records a stat-only signature of all.html/.xmltemplates in the theme dirs (the 14k+.icons/excluded) and forces a full rebuild when it changes. Editingbase.htmletc. now rebuilds every page.
[11.3.7] — 2026-06-26
Changed
- Parallel Markdown rendering.
_populate_page(read source +markdown.convert, the CPU-heavy part of the build) now runs across aThreadPoolExecutor(up to 32 workers). The per-threadMarkdowninstance makesrender()thread-safe, so only the plugin event calls andconfig._current_pageare serialized via a lock. Template rendering (_build_page) stays serial to avoid the sidebar-active race. Cold build of the docs site: ~10s → ~7s; output is byte-identical across builds (verified over 5 runs).
[11.3.6] — 2026-06-26
Fixed
- DocsForge upgrades now trigger a full rebuild. The build cache only tracked source-
.mdhashes and thedocsforge.ymlhash, so upgrading DocsForge (new theme templates / service worker / build logic) did not rebuild unchanged pages — the new SW and templates wouldn't reach the built site until a source file was edited. The cache now records the package version and forces a full rebuild when it changes. - Config changes now actually rebuild unchanged pages.
cache.invalidate()deleted disk files but left the planner's in-memory hashes intact, so a config/package change didn't rebuild pages whose source was unchanged (off-by-one: the change applied on the next build, not the current one).planner.invalidate()clears both in-memory and disk state. Themeta(mtime/size hash cache) is retained across a version bump so the rebuild still skips re-reading unchanged sources.
[11.3.5] — 2026-06-26
Added
- Hover/focus link prefetch. Internal links are now prefetched on
mouseover/focusin(fire-and-forget through the service worker'sserveCurrentPage), so the destination page is already cached when the user clicks — page switches become instant. Same-origin only, deduped, skips same-page/anchor links.
Fixed
- SW update message aligned with the client. The SW now posts
DOCSFORGE_UPDATE_READY(the message namebase.htmlalready listens for) instead of the unuseddocsforge-updated.
[11.3.4] — 2026-06-26
Changed
- Build: mtime+size pre-filter for file hashing.
should_rebuildandcache-manifest.jsongeneration re-read and SHA-256'd every source.mdon every build. They now consult a{path: {mtime, size, hash}}cache (meta.json) and reuse the cached hash whenstat()reports the same mtime and size — a stat instead of a full read+hash. On a no-op build this makes the hashing phase stat-only (docs site: 0.87s → 0.66s; bigger gains on large sites).
[11.3.3] — 2026-06-26
Changed
- Build: skip asset optimization when the site is unchanged.
optimize_assetsre-scanned every HTML/CSS/JS on every build. It now runs only when a page was actually rebuilt or the source set changed; on a no-op incremental build it's skipped entirely (docs site: 1.61s → 0.87s).
[11.3.2] — 2026-06-26
Changed
- Build: skip the orphan-output scan when no source files were deleted.
find_orphaned_outputswalked the entiresite_diron every build, but orphaned outputs can only appear when a source is removed. The build now records the source-URI set (sources.json) and skips thesite_dirwalk when the set is unchanged or only grew since the last build. On a cache hit (typical incremental build) this removes a full output-tree traversal.
[11.3.1] — 2026-06-26
Changed
- SW: conditional manifest fetch.
fetchManifestnow usesfetch(cache-manifest.json, { cache: 'no-cache' })instead of cache-busting with?v=Date.now(). The static host returns 304 when the manifest is unchanged, so rapid navigations no longer re-download the full manifest each time — same freshness, less bandwidth.
[11.3.0] — 2026-06-26
Changed
- Service worker redesigned around "current page first". The SW now treats
docsforge serveand a deployed site identically and prioritizes the page you're actually viewing: - Install is non-blocking — just
skipWaiting(), no more pre-cache-all that held the SW inactive until every page was fetched. serveCurrentPage(request, manifest)runs on every navigation/page-switch: it fetches the manifest once, serves the current page from cache instantly if its hash is current, otherwise fetches+caches the fresh page and displays it (falling back to the stale cache offline). This is a separate function from the background sync.syncCacheFromManifest(manifest)runs in the background (throttled ≥10 min, deduped) using the same fetched manifest, caching every other page that is missing or whose hash changed.activateprimes the visible page first (the tab the user is on), then background-syncs the rest — so "first install caches the current page first" is literally true.- Applies to page switches too: programmatic HTML fetches (Material instant navigation) are detected via
Accept: text/htmland routed throughserveCurrentPage, not just hard navigations.
Fixed
- Manifest sync no longer re-hashes every cached HTML body.
cache-manifest.jsonstores source-.mdhashes but the SW caches built HTML, so the old body-hash comparison never matched and every page was re-fetched on every sync. The SW now diffs the manifest against the previously-synced per-file hashes plus checks cache existence — a page is re-fetched only when missing or actually changed. No body hashing.
[11.2.1] — 2026-06-26
Changed
- Service worker: far more efficient cache syncing. Previously
syncCacheFromManifestran on every navigation — a network fetch ofcache-manifest.jsonplus a re-hash (SHA-256 of the full body) of every cached page on each navigation after a deploy. Now: - Throttled to at most once per 10 minutes (and deduped so concurrent navigations share one sync).
- Diff-based — the previous manifest's per-file hashes are stored, so sync only re-fetches pages whose hash actually changed. Cached bodies are no longer re-hashed on every sync (only a one-time hash check the first time a URL is seen).
- On the docs site (44 pages) this removes ~44 SHA-256 ops + a manifest fetch per navigation, replaced by a single throttled fetch that touches only changed pages.
- The SW now posts a
docsforge-updatedmessage to open tabs when content changes (forward-compatible hook; harmless if no client listener is wired).
[11.2.0] — 2026-06-26
Changed
docsforge servenow uses the exact same service-worker caching strategy as a deployed site — no localhost special-casing. The previous versions special-casedlocalhost/127.0.0.1(first a hard network-only bypass, then a network-first variant) to keep livereload fresh. That made dev behave differently from production and broke offline dev. The SW now treats localhost identically to any other host: cache-first for HTML/assets, stale-while-revalidate for the rest, with background manifest-sync. Consequence: livereload auto-reloads may serve cached content until the SW's background sync catches up — the same freshness model as the deployed site. Dev is now faithful to production, including offline support after the server stops.
[11.1.9] — 2026-06-26
Added
- Docker image now publishes version-numbered tags. Previously only
latestandsha-*were pushed (thetype=semvermetadata never fired because the build job checks out a commit SHA, not the tag). Each release now also publishesghcr.io/qqshi13/docsforge:<version>(e.g.:11.1.9) and:<major>.<minor>(e.g.:11.1) for stable releases. - Friendly, helpful GitHub release notes. Release bodies are now generated from the matching CHANGELOG entry, with install/upgrade commands, Docker pull/run examples, and a VS Code extension download hint — instead of a bare "Full Changelog" link.
Fixed
docsforge servepages now work offline after the server stops. The service worker had a hard localhost bypass: onlocalhost/127.0.0.1it fetched from the network with no caching and no fallback, so once you stopped the dev server (or went offline) every page was blank. It now uses network-first for localhost — fresh content while the server runs (so livereload stays loop-free, the original reason for the bypass) — and caches successful responses so visited pages survive after the server is closed. The stale-cache reload loop stays fixed because the SW never serves stale HTML during a reload.
[11.1.8] — 2026-06-25
Fixed
- Config-check summary now appears at the start of
build/serveoutput, not the end.check()prints to stdout viaprint(), while the build logs to stderr vialogging(unbuffered). When the two streams are merged and piped — i.e. CI,docker run, or any| grep/| tail— stdout is block-buffered and doesn't flush until process exit, so the check block landed after the build logs even though it ran first.check()now flushes stdout before returning. (Regression guard added:test_regression_config_check_appears_before_build_logs.)
[11.1.7] — 2026-06-24
Added
docsforge serve --strict— the dev server now accepts--strict(matchingdocsforge build). Rebuilds treat warnings as errors; the server stays alive and logs the abort so you can fix issues without restarting. The flag propagatesstrict=TruethroughDevServer.serve→serve_module.serve→load_config.- Docker: customizable PDF browser. Documented how to point PDF export at a different Chromium/Chrome via
PLAYWRIGHT_CHROMIUM_EXECUTABLE(override the path, fall back to Playwright's bundled browser, or mount a host binary). Seedocs/advanced/docker.md.
Changed
- The Docker guide now lists all
--strict/ PDF-browser /--jobsoptions with copy-pasteabledocker runexamples.
[11.1.6] — 2026-06-24
Added
- 75 more tests (154 total):
test_files.py(File model, dest_uri/url mapping,get_fileswalk),test_config.py(load_config, defaults, env-tag substitution, validation),test_init.py(project scaffolding),test_search.py(SearchIndex entries/tags/jieba gating),test_tags.py(Tag model),test_privacy.py(FragmentParser, mime map),test_minify.py(JS/CSS/HTML minification),test_meta.py(meta-file merge).
Fixed
load_configcrashed withNameErroron invalid YAML. Theexcept yaml.YAMLErrorhandler referenced an unimportedyaml, so a syntax error indocsforge.ymlproduced a rawNameError: name 'yaml' is not definedinstead of a friendly error. (Found bytest_config.py::test_invalid_yaml_raises.)
[11.1.5] — 2026-06-24
Added
- Test suite. DocsForge now ships a pytest suite (79 tests) covering the incremental cache, config loading, the CLI front-end, utils, end-to-end builds, and a regression file with one named guard per historical bug. Previously the project had zero tests.
Fixed
Bugs found while writing the test suite:
- Incremental cache now works for non-root pages.
BuildPlanner.find_orphaned_outputsonly checkeddocs/<name>/index.mdfor an output atsite/<name>/index.html, missing the commonuse_directory_urls=Truemappingdocs/<name>.md→site/<name>/index.html. Every non-root page was therefore deleted as "orphaned" at the start of each build and rebuilt from scratch, defeating the incremental cache for them. Second build of the docs site dropped from ~4.7s to ~0.9s. detect_environmentalways reporteddocs_dir_exists: False. It referenced_open_config_filewithout importing it (the import lived in a different function's scope); the resultingNameErrorwas swallowed by a bareexcept, sodocs_dir_exists/has_indexwere never populated._open_config_filerejectedpathlib.Patharguments. It only handledstr/IO/None; aPathfell through to the file-descriptor branch and crashed on.seek(0). Now accepts anyos.PathLike.BuildPlanner.savedid not update the in-memoryconfig_hash. After writing the config hash to disk, a subsequentshould_full_rebuildon the same planner instance still saw the stale (None) value and forced a full rebuild.- Blog plugin
on_shutdowncould crash the build.rmtree(self.temp_dir)raisedFileNotFoundErrorwhen the temp dir was already gone (repeated builds in one process); cleanup is now idempotent.
[11.1.4] — 2026-06-24
Fixed
- Incremental cache dependency tracking actually works now. The v11.1.3 implementation had two defects that made it a silent no-op:
build.pypassedpage.content(rendered HTML) toDependencyTracker.get_file_deps, but thepymdownx.snippets--8<--include markers are consumed duringmd.convert(). It now passespage.markdown(the raw source, which retains the markers).- Include paths were resolved only relative to the source file's directory, but
pymdownx.snippetsresolves relative to its configuredbase_path(docsforge doesn't set one, so the default is the current working directory / project root). Includes are now resolved againstdocs_dir, the source file's directory, and the cwd. - Failed builds are no longer cached. In strict mode,
_build_pagere-raises, but the build loop still calledplanner.update_cacheafterward — marking a broken page as up-to-date so the next run silently skipped it. The cache is now only updated for pages that built successfully.
[11.1.3] — 2026-06-23
Fixed
- Incremental cache now tracks snippet includes.
DependencyTracker.get_file_depswas a stub returning[], so editing a file included viapymdownx.snippets(--8<-- "path") did not trigger a rebuild. Includes are now resolved relative to the source file and watched for changes. A latent bug inBuildPlanner.update_cachethat never stored dependency hashes (making the dep check a no-op) was also fixed. - Removed dead
_OPTIONAL_PLUGINScode path incli_core._check_optional_deps— the empty plugin→dependency map and its unusedplugin_namesloop. The realjieba/docsforge[chinese]check is retained.
Changed
- Repo hygiene — untracked build artifacts removed from git:
- Removed 42 committed PDF build outputs under
docsforge-docs/pdf/; the directory is now gitignored. docs/blog/index.md(auto-generated by the blog plugin) untracked and properly gitignored at the package level. The previous root-leveldocs/blog/index.mdpattern was slash-anchored and never matched the real path underdocsforge-docs/.- Deleted stale
docsforge-vscode-11.0.0-beta.2.vsixfrom disk.
[11.0.0b1] — 2026-06-19
Added
- VSCode Extension: Open Preview — Sidebar button opens the dev server in VS Code's Simple Browser via
simpleBrowser.api.open. - VSCode Extension: Open Docs — Sidebar button opens the DocsForge documentation site.
- VSCode Extension: Stop Build — Sidebar button to cancel a running build.
- VSCode Extension: Managed external server — Detects
docsforge servestarted in a terminal via.docsforge/server.jsonpidfile. - Pidfile —
docsforge servewrites.docsforge/server.jsonwith PID and URL for external tools. - Content-based cache busting — Downloaded external assets include a content hash in the filename.
- Docs badge — Compact and standard DocsForge badges.
Changed
- Service worker: bypass cache for localhost — SW detects
localhost/127.0.0.1and fetches from network. Prevents stale-cache reload loop during dev. - Livereload:
_rebuildingflag — File changes during a build are queued, not acted on immediately. One final rebuild fires afterward. - Tags template layout flattened —
fragments/tags/{layout}/tag.html→fragments/tags/{layout}-tag.html,listing.html→{layout}-listing.html.
Fixed
- Asset optimizer: unquoted HTML attributes — Made regex quotes optional to match Material's unquoted output.
- Privacy plugin:
url_relative_toargument order — Path was from file to page instead of page to file. - Privacy plugin: path normalization — Regex
/.matched.icons→_icons. Fixed. - Webserver:
.well-knownroute — Chrome DevTools probes returned 404. - Infinite reload loop —
_rebuildingflag + SW localhost bypass.
VSCode Extension
- Open Preview via
simpleBrowser.api.open - Open Docs sidebar button
- Progress notification dismisses on stop
- Pidfile detection with 3s polling
- Stop external server (kill by PID)
- Stop Build button
- Sidebar state sync for server and build status
[10.9.9] — 2026-06-18
Fixed
- Favicon 404 — Asset optimizer regex made quotes optional.
- Privacy font CSS path —
url_relative_to()argument order fixed. - Privacy path normalization —
/.regex too broad.
Added
- Pidfile —
.docsforge-server.jsonfor external server detection. - VSCode Extension: Open Preview, Open Docs.
[10.9.8] — 2026-06-18
Added
- Unified release workflow —
release.ymlwith version bump, commit, tag, release, PyPI, VSIX. - DocsForge badges — Compact (110×20) and standard SVG badges.
- Docs: Render, DigitalOcean deployment guides.
Changed
- Tags template layout flattened — Subdirectory → prefix naming.
[10.9.7] — 2026-06-18
Added
- Unified release workflow — Replaces
publish.yml+bundle-extension.yml. - VSCode Extension: sidebar improvements — 10+ bug fixes.
[10.9.6] — 2026-06-18
Fixed
- Privacy: nested URLs in downloaded CSS.
- Font-display swap for Google Fonts.
Added
- Hash-based cache manifest for offline sync.
.well-known/browser probes — 200 + empty JSON.
[10.9.5] — 2026-06-17
Changed
- Simplify build — Always complete output.
Fixed
- WSL port detection — Socket timeout.
- SW only re-caches on content change.
[10.9.4] — 2026-06-17
Fixed
- Python
__version__sync with release.
[10.9.3] — 2026-06-17
Fixed
- Hot-reload duplicate builds — Race condition.
[10.9.2] — 2026-06-17
Fixed
- Export plugin directory handling.
[10.9.1] — 2026-06-11
Fixed
[10.9.0] — 2026-06-11
Added
Git revision dates — Every page now automatically shows "Last updated" and "Created" dates from git history. No configuration required — works out of the box for any docs site in a git repository. The dates are read from
git logand formatted as human-readable strings (e.g., "Jun 11, 2026"). The existingsource-file.htmltemplate already supported this — now it's actually populated. Disable withextra.git_revision_date: falsein docsforge.yml.CLI serve options — Added
--no-open,--port, and--hostflags todocsforge serve:docsforge serve --no-open # Don't auto-open browser docsforge serve --port 3000 # Serve on port 3000 docsforge serve --host 0.0.0.0 # Serve on all interfaces
Changed
- All theme features enabled by default — The material theme now enables a rich set of features out of the box, so new sites get the full experience without needing a long
features:list in docsforge.yml. New defaults include: content.action.edit,content.action.viewcontent.code.annotate,content.code.copycontent.tooltipsnavigation.footer,navigation.indexes,navigation.sections,navigation.tabs,navigation.top,navigation.tracking,navigation.instant,navigation.instant.progresssearch.highlight,search.share,search.suggesttoc.follow
[10.8.12] — 2026-06-11
Optimized
- Markdown instance reuse — Added per-thread caching of
markdown.Markdowninstances inpages.py. Previously, every page created a new Markdown instance, re-initializing all extensions (pymdownx, codehilite, etc.) from scratch. With 36 pages and 10+ extensions, this was a significant overhead. Now: - Each thread gets a cached Markdown instance keyed by
(extensions, configs) - Instances are
reset()between pages instead of recreated - Build time improvement: ~2.5s (was ~3.4s) — ~25% faster
Especially impactful for large sites with many Markdown extensions
Streamlined build.py — Multiple internal optimizations:
- Removed redundant
if _page_lock: with _page_lock:branching in_populate_page(it was always called sequentially, no lock needed) - Removed nested
_do_build()closure in_build_pagethat added function call overhead per page - Cached
files.documentation_pages()result instead of calling it 3 times per build - Moved
hashlibimport from inline (inside_inject_sw_build_hash) to module top level Simplified lock handling: always acquire lock in
_build_page, removedNonefallback pathRemoved "Cyrus" from copyright headers — Found and fixed 3 remaining files that still had "Cyrus" in the copyright string:
theme.py,preview.py,filter_config.py
[10.8.11] — 2026-06-08
Changed
- Lazy background caching — Replaced the blocking
cache.addAll()during service worker install with non-blocking incremental background caching. Previously, the SW tried to download all pages at once during install, which could block the initial page load. Now:
- The SW installs and activates immediately — the current page loads without delay.
- After activation, other pages are cached one by one in the background via
backgroundCachePages(). - The current page is already cached by the
fetchhandler when visited. - A
DOCSFORGE_CACHE_COMPLETEmessage is sent to all clients when done, so the UI can show a subtle indicator (e.g., "All pages available offline").
[10.8.10] — 2026-06-08
Fixed
- Service worker pre-cache URL resolution —
PRE_CACHE_PAGESURLs were relative to the site root (e.g.,"./","advanced/customization/"), butcache.addAll()inside the SW resolves URLs relative to the SW script location (/assets/javascripts/sw.js). This caused all pre-cache requests to fail silently, leaving the cache empty and breaking offline support. All pre-cache URLs now prefixed with../../so they correctly resolve from the SW to the site root. - Offline 404 fallback for subpath deployments —
cache.match("/404.html")was hardcoded to the domain root, which is wrong for sites deployed under a subpath (e.g.,/docsforge/). The SW now computesBASE_URLfrom its own location and usesBASE_URL + '404.html'for the fallback.
Added
- PWA update notification — When a new DocsForge version is deployed, the service worker activates and sends a
DOCSFORGE_UPDATE_READYmessage to all open tabs. The page displays a fixed banner at the top: "A new version of this documentation is available." with Refresh and Dismiss buttons. Users can click Refresh to immediately load the new content, or Dismiss to keep reading the current version. - Periodic update checks — The page checks for service worker updates every 5 minutes (
registration.update()), so users get notified of new content even on long-running sessions without needing to reload manually.
[10.8.9] — 2026-06-07
Added
- Full PWA / offline support — DocsForge now generates a
manifest.jsonand pre-caches all HTML pages during the service worker install phase, enabling full offline browsing of all documentation pages after the first visit. manifest.json— Generated automatically with site name, description, theme color (extracted from palette), and start URL. Linked via<link rel="manifest">in the<head>of every page.- Pre-caching all pages — The service worker now receives a
__PRE_CACHE_PAGES__placeholder that is replaced at build time with the complete list of all built HTML page URLs. Duringinstall, the SW caches every page so they work offline immediately without needing to visit each page first. - Cache-first strategy — The service worker now uses
cacheFirstWithNetworkFallbackfor HTML documents (wasstaleWhileRevalidatewhich caused network requests on every navigation). This means pages load instantly from cache even online, with a background update. - Offline fallback — If a page is not cached and the user is offline, the SW serves the 404 page (or a generic offline message if 404 isn't cached).
Changed
- Service worker strategy — Switched from
staleWhileRevalidatetocacheFirstWithNetworkFallbackfor HTML pages and assets. This prioritizes offline reliability and instant loads over always-fresh content. For most documentation sites, this is the desired behavior.
Fixed
- Removed "Cyrus" from all docs — Eliminated all remaining references to "Cyrus" from documentation, site footer, and copyright strings. The
license.mdanddocsforge.ymlsite author/copyright now use "QQ" only.
[10.8.8] — 2026-06-06
Changed
- Always dirty builds — Removed the
build_typeparameter fromserve()and alldirty=Falsedefaults. All builds are now incremental by default. Thedirtyflag was a legacy concept that caused confusion — full rebuilds are only triggered when the config file changes (detected by hash), andclean_directory+cache.invalidate()are skipped unless the config changes.
[10.8.7] — 2026-06-06
Fixed
docsforge servecache invalidation —DevServer.serve()was not passingbuild_type='dirty'toserve_module.serve(), causing the dev server to do full rebuilds with cache invalidation on every file change. This made live reload painfully slow. Thebuildcommand already did dirty/incremental builds by default; nowservedoes too.
[10.8.6] — 2026-06-06
Fixed
- Asset 404s on sub-pages —
base_urlcomputed byget_relative_url()returned..without trailing slash for pages at subdirectories. Without the trailing slash,_get_relative_urltreated it as a filename (stripped it), causing CSS/JS/asset paths to resolve relative to the page directory instead of the site root. Added trailing slash normalization toget_context()and_build_template().
[10.8.5] — 2026-06-06
Fixed
- Sidebar race condition — ThreadPoolExecutor parallel page builds caused multiple pages to be marked
activesimultaneously. EachPage.activesetter propagates to its parentSection, so concurrent builds leaked active state between pages. This resulted in the sidebar showing multiple sections as expanded when only the current section should be. Restructured_build_pageto hold the existingRLockfor the entire template render + file write phase, ensuring only one page is ever active at a time.
[10.8.4] — 2026-06-06
Fixed
- Search index 404 on non-root pages —
base_urlinbuild.pywas computed backwards:get_relative_url('.', page.url)returned the path from root to page instead of from page to root. The JS injected this into page config asbase, then resolvedsearch/search_index.jsonagainst it, producing a duplicated path like/docsforge/getting-started/getting-started/search/search_index.json. Fixed toget_relative_url(page.url, '.'). - Sidebar overlapping footer on desktop — Added
max-height: calc(100vh - 2.4rem)to.md-sidebar__scrollwrapatmin-width: 60emto prevent the sidebar from extending past the viewport and overlapping the footer.
[10.3.3] — 2026-05-17
Added
- Versioned service worker — Each build generates a unique hash in the SW, ensuring browsers install the new version and purge old caches
- Auto cache cleanup — Old caches automatically deleted when new SW activates
- Offline support — All same-origin files cached; HTML uses network-first, assets use cache-first
- PWA-ready — Service worker registration in every built page
Fixed
- Service worker scope set to
/(root) instead of/assets/javascripts/so it can intercept all requests
[10.3.2] — 2026-05-17
Fixed
- Service worker scope fixed to
/so it can cache blog posts and documentation pages - Added
request.mode === "navigate"check for better HTML page detection
[10.3.1] — 2026-05-17
Fixed
.icons/directory now included in PyPI wheel build- Added
artifactspattern inpyproject.tomlto ensure Material theme icons are packaged
[10.3.0] — 2026-05-17
Added
- TikZ diagram support — Write TikZ diagrams in Markdown, automatically compiled to SVG at build time
- Theme playground — Interactive palette switcher with live preview
- Blog plugin — Built-in blogging with authors, tags, archive, pagination, and RSS feeds
Fixed
- Source repo blank spot fixed (removed fixed 234px width when no stars/forks)
- Theme persistence across page navigation (uses
__md_scopeinstead of per-page URLs) - Palette toggle button highlight sync
- 404 page styling
Changed
- Cleaned up unrelated development files from repo
- All repos use
mainas default branch
[10.2.0] — 2026-05-16
Added
- Vendored mkdocs + Material — Self-contained, no external dependencies
- GitHub Pages deployment — GitHub Actions workflow for auto-deployment
- PyPI publishing — Automated releases via GitHub Actions
[10.1.0] — 2026-05-10
Added
- Zero-config Markdown — 31 extensions loaded by default (all pymdownx + python-markdown). No
markdown_extensions:config needed. - KaTeX math — Vendored KaTeX (1.5MB) renders
$$...$$inline and display math. No CDN calls for readers, no config. - Pygments highlighting — Syntax-colored code blocks at build time. No client-side JS.
- Dark mode toggle — Light/dark mode switch in header. Auto-detects system preference.
- Auto-loaded plugins — search, tags, blog, info, meta, minify, privacy all work without config.
- Self-hosted fonts — Privacy plugin downloads and caches Google Fonts locally.
Changed
- Config file renamed from
properdocs.ymltodocsforge.yml - Theme namespace changed from
mkdocs.themestodocsforge.themes - Plugin system — 6 plugins removed, 7 remain as built-in defaults
Removed
typeset— Users can write Unicode directlyoptimize— Requires externalpngquantbinarysocial— Requires Pillow + CairoSVGprojects— Niche multi-project featureoffline— Privacy plugin covers most use casesgroup— Plugin orchestrator (niche)
[0.1.0] — 2025-05-10
Added
- Initial release