Repository navigation
Docs site config: Zensical features, Markdown extensions, API reference options and a strict build #701
Description
Parent
What to build
Readers get the fixes that need only configuration, and the build becomes strict so later tickets cannot regress it:
- Snippets render without the 4–8 spaces of indentation they carry today (
pymdownx.snippets.dedent_subsections), so a pasted block runs. - Picking the Annotated tab once switches every Assignment/Annotated pair on the site (
content.tabs.link), and tab anchors read as words (tabbed.combine_header_slugwith a lower-case slugify). - Hovering DDL, RLS, FK, PK, GUC, FFI and the other terms shows a definition (
abbr+ a glossary appended to every page throughsnippets.auto_append+content.tooltips). Glossary wording follows CONTEXT.md where it defines the term. #NNNreferences link to GitHub (pymdownx.magiclink, reposyn54x/ferro-orm); permalinks get a title (toc.permalink_title).- Theme:
navigation.instant,.instant.progress,.tracking,.path,.footer,content.code.select,content.action.copy; a third "follow system" palette toggle;extra.social(GitHub, PyPI);extra.statusbadgesnewanddeprecated.search.suggestis removed. - Plugins:
redirectswith anchor redirects replacing the hand-written stub for the old migrations page (the stub is deleted and each of its old anchors maps to the section that replaced it);llmstxt;socialwith background#B7410E. - API reference: Pydantic's inventory;
filters = "public",merge_init_into_class+ignore_init_summary,separate_signature,show_signature_annotations,signature_crossrefs,show_symbol_type_heading/_toc,summary,backlinks = "tree",scoped_crossrefs,relative_crossrefs,show_source = false; thegriffe-inherited-docstringsextension (added to the docs and dev dependency groups);show_overloads: falseandparameter_headingson theFielddirective.show_if_no_docstringstays on until the API completeness ticket (API reference completeness: every public object has an entry, pages named after their objects #722) turns it off. - The
**fieldsparameters ofcreate,upsert,get_or_createandupdate_or_createare annotated**fields: Any, so griffe stops warning. - The docs build runs with
--strictin the publish workflow and in a new docs job in CI. - A new
tests/test_docs_site.pybuilds the site strictly once per session and asserts that no snippet<code>block has every line sharing leading indentation.
Requires #699 (dependency upgrade) merged first.
Acceptance criteria
-
zensical build --clean --strictexits 0 with no warnings. - The old migrations URL and each of its anchors redirect to the section that replaced them; the stub page is gone from the source tree and the sitemap.
- On a built page with two tab sets, the Assignment/Annotated tabs share a link group and their anchors are word slugs.
- A page using DDL shows an
<abbr>with its definition. - The rendered Model API page shows no source blocks, no dunder members, no
__ferro_*__attributes, and linked type annotations in signatures. -
Fieldrenders once, without its six overloads. -
Relation.all()and.first()show their inherited docstrings. - The publish-docs workflow and the CI docs job build with
--strict.
Files owned
- Create:
docs/includes/abbreviations.md,tests/test_docs_site.py - Modify:
zensical.toml,.github/workflows/publish-docs.yml,.github/workflows/ci.yml,pyproject.toml,uv.lock,src/ferro/models.py(the four**fieldsannotations only) - Delete:
docs/pages/guide/migrations.md
Interfaces
- Consumes: nothing from other tickets
- Produces: Fixture
built_siteintests/test_docs_site.py: session-scoped, returns thePathof a site built withzensical build --strictinto a temporary directory. Used by the API completeness ticket (API reference completeness: every public object has an entry, pages named after their objects #722) and the enforce-guards ticket (Enforce the docs guards: rendered-site checks, inline blocks run by default, lambda and error-check lint in the suite #724). - Produces: Helper
rendered_pages(built_site) -> Iterator[tuple[str, str]]in the same module: yields(url_path, html)for every built page. - Produces: Glossary file
docs/includes/abbreviations.md, appended to every page; later tickets add terms to it only through the enforce-guards ticket (Enforce the docs guards: rendered-site checks, inline blocks run by default, lambda and error-check lint in the suite #724). - Produces:
redirect_mapstable inzensical.toml; the nav ticket (Navigation: six tabs, sentence-case labels that match page titles, moved pages redirected #723) adds entries for the pages it moves.
Test scenarios
- Happy path:
test_docs_site.pybuilds the site strictly and passes. - Error path: reverting
dedent_subsectionsmakes the indentation assertion fail on the quickstart page. - Integration: requesting the old migrations URL with an anchor in the built site lands on the redirect page that points at the new section.
Verify
uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheproviderBlocked by
None (can start immediately)
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
Recommendation table
Griffe extensions
| Extension | What it does | Ferro evidence | Verdict | Config |
|---|---|---|---|---|
dataclasses (built-in) |
Synthesizes __init__ for stdlib dataclasses in static analysis |
71 @dataclass in src/ferro (PassReport, Report, ExecutedStatement, Check, RowPolicy…) |
Already on (always loaded, nothing to add) | — |
unpack_typeddict (built-in) |
Expands **kw: Unpack[TD] into named params |
The only Unpack is **extra: Unpack[pydantic.fields._EmptyKwargs] in ferro.Field (an empty TypedDict) |
Skip. It crashes the build (KeyError: 'pydantic') unless you also set preload_modules=["pydantic"], and that preload crashes griffe-pydantic (see below). The payoff is removing one **extra param |
extensions = ["unpack_typeddict"] + preload_modules = ["pydantic"] |
griffe_pydantic |
Labels models pydantic-model, adds Config / Fields / Validators sections, labels fields and validators |
Public Pydantic models: PoolConfig, DatabaseSettings, FerroSettings (BaseSettings), ferro.query.Row, Model (use_attribute_docstrings=True) |
Consider (adopt only with the caveats fixed) | extensions = [{ griffe_pydantic = { schema = false } }] + preload_modules = ["pydantic_settings"] |
griffe_warnings_deprecated |
Reads PEP 702 @warnings.deprecated and adds a deprecation admonition |
Ferro's _deprecations.deprecated is a home-grown decorator that is never used. Runtime deprecations (BackRef[...] syntax, state.py) are warnings.warn calls the extension can't see. requires-python >=3.13, so stdlib warnings.deprecated is available |
Skip now; adopt together with switching _deprecations.deprecated to wrap warnings.deprecated |
extensions = ["griffe_warnings_deprecated"] |
griffe_inherited_docstrings |
Copies a parent member's docstring into an override that has none | ferro.Relation overrides Query.all() / first() without docstrings, so those headings render empty today |
Adopt | extensions = ["griffe_inherited_docstrings"] |
griffe_public_redundant_aliases |
Treats from x import y as y as public |
ferro/__init__.py declares __all__, the only as-alias is _deprecations as _deprecations |
Skip | — |
griffe_public_wildcard_imports |
Treats from x import * names as public |
No wildcard imports in src/ferro |
Skip | — |
griffe_runtime_objects |
Stores the live runtime object in obj.extra for custom templates |
No custom templates | Skip | — |
griffe_sphinx |
Uses #: comments above attributes as docstrings |
#: is used only on rowsecurity.py module constants (_COMMAND_CLAUSES, COMMANDS, _SETTING_KEY_RE) and checks.py privates, none of them on an API page |
Skip (reconsider if rowsecurity.COMMANDS gets documented) |
extensions = ["griffe_sphinx"] |
griffe_typingdoc |
Reads param docs from Annotated[..., Doc("…")] (PEP 727) |
No typing_extensions.Doc anywhere. Loading it crashed the build (KeyError: 'pydantic', the same Unpack path) and printed spurious rowsecurity.py:100–103 param warnings |
Skip | — |
griffe_autodocstringstyle |
Sets the docstring parser to auto for objects from external packages |
API pages render only ferro.* objects |
Skip | — |
griffe_generics (3rd-party) |
Rewrites T to the bound type in subclasses |
ProjectedQuery(Query[T]) and Relation(Query[T]) keep T unbound, so nothing to rewrite. Build: 0 changed lines |
Skip | — |
griffe_modernized_annotations (3rd-party) |
Optional[X] → X | None, List → list |
The handler's own modernize_annotations option does the same. Build: 0 changed lines (no Optional/Union reaches a rendered page) |
Skip (use the option if ever needed) | — |
griffe_fieldz (3rd-party) |
Injects dataclass/pydantic/attrs fields via fieldz |
Overlaps griffe-pydantic and the dataclasses extension. Build: 0 changed lines statically | Skip | — |
docstring_inheritance (3rd-party) |
Merges parent docstring sections into children (more granular than inherited-docstrings) | Ferro needs only the whole-docstring copy above | Skip (page fetch returned 503; description from the Griffe index) | — |
griffe_inherited_method_crossrefs (3rd-party) |
Replaces an inherited method's docstring with "Inherited from [Parent.m]" | Alternative to inherited-docstrings for Relation. It links instead of duplicating |
Consider as the alternative to inherited-docstrings (not built) | extensions = ["griffe_inherited_method_crossrefs"] |
griffe_fastapi (3rd-party) |
Documents APIRouter endpoints | No FastAPI | Skip | — |
| griffe-tui | Terminal browser for Griffe data. Not an extension, and not on the Griffe extensions page | — | N/A | — |
mkdocstrings-python options
Current: docstring_style="google", show_source=true, show_root_heading=true, show_root_full_path=false, show_category_heading=true, show_if_no_docstring=true, members_order="source"; inventories=[python].
| Option | What it does | Ferro reason | Verdict | Config |
|---|---|---|---|---|
inventories += pydantic |
Cross-links to Pydantic's Sphinx inventory | Fixes the one build issue (pydantic.fields.FieldInfo unresolved): the build reports "No issues found". Links BaseModel, Field, AliasPath, Discriminator, PydanticBaseSettingsSource in signatures |
Adopt | inventories = ["https://docs.python.org/3/objects.inv", "https://docs.pydantic.dev/latest/objects.inv"] |
separate_signature |
Puts the full signature in a code block under the heading | Ferro's API is signature-heavy (connect, Field, where, order_by(nulls=…)) |
Adopt | separate_signature = true |
show_signature_annotations |
Shows types in signatures | A typed ORM whose docs push static typing (QueryNode | Predicate, I-8). Today signatures show no types |
Adopt | show_signature_annotations = true |
signature_crossrefs |
Links types inside separated signatures | Internal autorefs went from 67 to 439 and external from 245 to 1343 | Adopt | signature_crossrefs = true |
merge_init_into_class (+ ignore_init_summary) |
Puts the __init__ params in the class signature and drops the separate __init__ entry |
Removes 18 separate __init__ entries (Session, PassReport, Check, RowPolicy, FerroError, Query…). Users call Session(...), not Session.__init__ |
Adopt | merge_init_into_class = true, docstring_options = { ignore_init_summary = true } |
show_symbol_type_heading / show_symbol_type_toc |
Adds class/meth/attr/func badges to headings and TOC | Pages mix classes, functions and attributes. Zensical CSS styles doc-symbol (61 rules) |
Adopt | both = true |
backlinks |
Adds "Used by / Returned by / Subclassed by" links under each symbol | Zensical implements this natively (577 backlink nodes, e.g. FerroError "Subclassed by", PassReport "Returned by" migrate()) |
Adopt | backlinks = "tree" (global-only) |
summary |
Adds Attributes/Methods/Classes summary tables at the top of classes and modules | Model and Query have dozens of members. Adds 22 "Methods:" and 27 "Attributes:" summaries |
Adopt | summary = true |
scoped_crossrefs + relative_crossrefs |
Enables [Check][] / [.save][] cross-references inside docstrings |
284 Sphinx roles (:class:, :meth:, :func:, :attr:) in Google docstrings currently render as literal text (e.g. Model's docstring shows ":class: ~ferro.checks.Check"). These options make the short Markdown replacement possible |
Adopt (then convert the roles; runner.py 38, builder.py 29, cli/output.py 22, nodes.py 20, generate.py 20, registry.py 19, models.py 16, exceptions.py 13) | scoped_crossrefs = true, relative_crossrefs = true |
filters = "public" |
Honours __all__, otherwise the underscore rule |
Same anchors as today (verified id-for-id) but tracks __all__ intent |
Adopt (no visible change now; safer as modules grow) | filters = "public" |
show_overloads (local) |
Renders each @overload |
ferro.Field has 6 overloads of ~50 params each, a wall of repeated text above the real signature |
Adopt locally on ::: ferro.Field |
options: show_overloads: false |
parameter_headings (local) |
Gives each parameter an anchor, TOC entry and inventory entry | Enables links such as ferro.Field(index) / Field(db_type) from the guides. Global use would bloat every TOC |
Consider locally on ::: ferro.Field |
options: parameter_headings: true |
show_inheritance_diagram (local) |
Mermaid class diagram per class | Useful only for the exception tree, which exceptions.md already draws as text. Globally it adds 26 diagrams |
Consider locally on ::: ferro.FerroError, otherwise Skip |
options: show_inheritance_diagram: true |
show_source |
Embeds source with line numbers | Dominates page weight: model 608 kB → 174 kB, queries 997 kB → 289 kB with it off. Source is on GitHub | Consider false (a product decision) |
show_source = false |
inherited_members (local) |
Renders inherited members | Relation would show 23 Query methods, duplicating the Query page. Backlinks plus inherited-docstrings cover it |
Skip (or local on Relation only) |
— |
docstring_section_style |
table / list / spacy | list reads better at phone width for long Parameters tables (Field). Built fine |
Consider "list" |
docstring_section_style = "list" |
modernize_annotations |
Rewrites Optional/Union | No old-style annotations reach rendered pages | Skip | — |
show_if_no_docstring |
Shows undocumented objects | Keep (current) | Keep | — |
members_order = "source" |
— | Keep (current) | Keep | — |
docstring_options.trim_doctest_flags / returns_named_value / warn_unknown_params |
Parser defaults are already true |
Ferro Returns sections parse correctly with the defaults | Keep defaults | — |
unwrap_annotated, show_signature_type_parameters, type_parameter_headings, find_stubs_package, force_inspection, show_labels, annotations_path |
— | No Ferro need: _core.pyi sits beside the package; forcing inspection would import the Rust extension at docs build time |
Skip | — |
Recommended zensical.toml block (verified building with exit 0, "No issues found")
[project.plugins.mkdocstrings.handlers.python]
paths = ["src"]
inventories = [
"https://docs.python.org/3/objects.inv",
"https://docs.pydantic.dev/latest/objects.inv",
]
[project.plugins.mkdocstrings.handlers.python.options]
docstring_style = "google"
docstring_options = { ignore_init_summary = true }
merge_init_into_class = true
show_source = true # consider false (3x smaller API pages)
show_root_heading = true
show_root_full_path = false
show_category_heading = true
show_if_no_docstring = true
members_order = "source"
filters = "public"
separate_signature = true
show_signature_annotations = true
signature_crossrefs = true
show_symbol_type_heading = true
show_symbol_type_toc = true
scoped_crossrefs = true
relative_crossrefs = true
summary = true
backlinks = "tree"
extensions = ["griffe_inherited_docstrings"]
Add griffe-inherited-docstrings to the docs dependency group in pyproject.toml (next to zensical / mkdocstrings-python, lines 61–62 and 81–82).
Local options, also verified:
::: ferro.Field
options:
show_overloads: false
parameter_headings: true
::: ferro.FerroError
options:
show_inheritance_diagram: true
If griffe-pydantic is adopted, add it with the pydantic_settings preload. Do not preload pydantic, because that crashes the build:
extensions = ["griffe_inherited_docstrings", { griffe_pydantic = { schema = false } }]
preload_modules = ["pydantic_settings"]
Details
griffe-pydantic (Consider)
What changed in the build (static mode):
PoolConfig,DatabaseSettings,RowandModelget thepydantic-modellabel and a Config section (frozen: True,extra: forbid, and for Modelfrom_attributes,use_attribute_docstrings,arbitrary_types_allowed). PoolConfig and DatabaseSettings also get Fields and Validators lists, and fields and validators getpydantic-field/pydantic-validatorlabels.FerroSettings(aBaseSettings) is not detected unlesspreload_modules = ["pydantic_settings"]is set. With the preload it is detected (5 models instead of 3 on the connection page).
Caveats that keep it at Consider:
- Field constraints vanish from the displayed value.
max_connections = PydanticField(default=5, ge=1)renders asmax_connections = 5, andname = Field(min_length=1)renders asname. Today the constraint is visible. Unless each field's docstring states the constraint, docs lose information. - A private validator leaks. Model's Validators section lists
_validate_required_foreign_keys. schema = truesilently turned the extension off in static mode: the output was identical to having no extension. A JSON schema would need dynamic import offerro, which means building the Rust extension in the docs job.preload_modules = ["pydantic"]crashes the build (TypeError: 'NoneType' object is not iterable), so it can't be combined withunpack_typeddict's workaround.
Sphinx roles are a bigger rendering bug than any extension
Google-style docstrings across src/ferro contain 284 :class: / :meth: / :func: / :attr: roles. mkdocstrings doesn't interpret them, so Model's own docstring shows "as a tuple of :class: ~ferro.checks.Check objects". With scoped_crossrefs / relative_crossrefs on, they can become [Check][ferro.checks.Check] or [Check][]. The conversion is a separate docs task. No Griffe extension converts Sphinx roles (griffe-sphinx handles only #: comments).
Deprecations
ferro/_deprecations.py defines deprecated(reason=, since=, remove_in=, reference=), which wraps with functools.wraps and warnings.warn. Nothing uses it (grep @deprecated finds nothing), and griffe can't see it either way. Because Ferro requires Python ≥ 3.13, warnings.deprecated is in the stdlib. If the helper is rewritten to return warnings.deprecated(message)(obj), then type checkers flag call sites (PEP 702) and griffe_warnings_deprecated renders the admonition. Adopt the extension in that same change. Until then it is a no-op (built: 0 changed lines).
Remaining griffe warnings
models.py:730/758/817/850 warn No type or annotation for parameter '**fields'. Fix this in the source by annotating **fields: Any. Don't silence it with warn_missing_types = false.
Effects measured
- Options set (
site-opts):doc-signatureblocks went from 20 to 315, internal autorefs from 67 to 439, external from 245 to 1343, backlink nodes from 0 to 577, and there are 300 heading badges and 740 TOC badges. The anchor ids are identical to the baseline except that the 18*.__init__anchors are gone (merged into the class). - inherited-docstrings: only
relationshipschanged.Relation.all/Relation.firstgained their Returns and Examples. - Page weights (bytes): model 607,879 / 422,914 (options) / 173,623 (show_source off); queries 996,893 / 667,150 / 288,532.
Verified by building vs unverified
Verified (Zensical build exit 0 in the scratch copy, output text-diffed):
- All recommended global options together,
griffe_inherited_docstrings, and the pydantic inventory: build reports "No issues found". - Local
show_overloads: false,parameter_headings: true(anchorferro.Field(index)present), andshow_inheritance_diagram: trueon FerroError (onepre.mermaidemitted). - griffe-pydantic (static,
schema=false), with and without thepydantic_settingspreload. Itsschema=truemode is a no-op. Apydanticpreload crashes the build. unpack_typeddictandgriffe_typingdoccrash without a preload. Withpreload_modules=["pydantic"], unpack_typeddict builds.griffe_warnings_deprecated,griffe_generics,griffe_modernized_annotations,griffe_fieldzandgriffe_public_redundant_aliasesload cleanly and change nothing.show_source=false,inherited_members=true,docstring_section_style="list": build clean (sizes above).- Zensical passes handler options through unchanged to upstream mkdocstrings (read
zensical/compat/mkdocstrings.py), and its theme CSS has rules fordoc-symbol,doc-backlink,doc-labelandmermaid.
Unverified:
- Visual rendering in a browser: badge appearance, the client-side Mermaid render of the inheritance diagram, and backlink layout at phone width. Only HTML structure and CSS rule presence were checked.
griffe_inherited_method_crossrefs,griffe_runtime_objects,griffe_sphinx,griffe_autodocstringstyle,docstring_inheritanceandgriffe_fastapiwere not installed or built (judged from their docs and the codebase grep). The docstring-inheritance page returned HTTP 503.- griffe-pydantic in dynamic mode (needs the compiled
ferro._core). - None of these builds used a docs job identical to CI.
pydantic-settings>=2.0is a Ferro runtime dependency (pyproject line 12), so the preload should resolve there.
TL;DR
- A rendering bug, found by this audit: about 130 snippet sections render with 8 (or 4) spaces of leading indentation. Most
# --8<-- [start:x]markers indocs/examples/*.pysit insideasync def main():, so the embedded code shows up indented on the page. For example,guide/queries.md:65embedspredicates.py:filteringand the page showsadults = await User.... One option fixes it:pymdownx.snippets.dedent_subsections = true. I checked this in the trial build. - Tabs: give them readable anchors and link them site-wide. I-7 puts "Assignment / Annotated" tab pairs on every model example. Today their ids are
__tabbed_1_1. Settingtabbed.combine_header_slugandslugifygivesdefining-a-model-assignment. The theme featurecontent.tabs.link(a theme setting, not an extension) makes one click on "Annotated" switch every tab pair on the site and remembers the choice across pages. - Glossary tooltips: add
abbrplussnippets.auto_appendwithdocs/includes/abbreviations.mdfor DDL, RLS, FK, PK, GUC, FFI, GIL, IR and similar terms. In the prose outside code, SQL appears 104 times, DDL 53, FK 27, GUC 5, RLS 6 and FFI 6. Pair it with the theme featurecontent.tooltips. - MagicLink for
#NNNissue refs.CHANGELOG.mdis embedded throughchangelog.mdand has about 20 bare refs like(#647)that render as plain text today, plusapi/queries.md:34. In the trial build this produced 94 issue links on the changelog page. - The 12.x upgrade breaks nothing in the current config. Both the current config and the proposed one build. The only warning is the existing mkdocstrings autoref
pydantic.fields.FieldInfowarning.
Recommendation table
Line refs are relative to docs/pages/ unless noted.
| Extension | Gives | Where in Ferro docs | Verdict | Config |
|---|---|---|---|---|
abbr (Python-Markdown) |
<abbr title> hover text for acronyms. Zensical's "glossary" recipe pairs it with snippets auto_append. |
Acronyms in prose outside code: SQL 104, DDL 53, ORM 38, FK 27, IR 7, GIL 7, RLS 6, FFI 6, PK 6, GUC(s) 8, UUID 5, ASGI 3, DBAPI 3. Dense pages: guide/row-level-security.md (GUC, RLS), concepts/architecture.md (IR, FFI, DDL), concepts/performance.md (GIL). |
Adopt | [project.markdown_extensions.abbr] plus auto_append (see snippets) plus theme feature content.tooltips |
def_list (Python-Markdown) |
<dl> term/definition lists. |
Option and flag lists in reference/cli.md:15-20,56-62,101-105 and the key tables in reference/configuration.md:66-97 are tables today and work well as tables. Possible later use for parameter glossaries in the guide pages. |
Consider (low cost, but nothing uses it yet) | [project.markdown_extensions.def_list] |
footnotes (Python-Markdown) |
[^1] notes. With content.footnote.tooltips they also show as inline tooltips. |
No current use. Candidates are asides like concepts/architecture.md:43-50 (the "Implementation status" blockquote) and dated caveats in why-ferro.md. |
Consider | [project.markdown_extensions.footnotes] (+ theme content.footnote.tooltips) |
attr_list, md_in_html, admonition, tables |
Already on. Admonitions in use: note 13, warning 9, tip 4, danger 2, ??? example 3. |
Everywhere. | Keep | unchanged |
toc |
Permalinks. Zensical also documents permalink_title, title and toc_depth. |
All pages. | Keep, add permalink_title (screen readers announce the permalink with that text instead of "¶") |
permalink = true, permalink_title = "Anchor link to this section" |
pymdownx.snippets |
--8<-- file and section embedding. |
183 embeds, 181 of them by section (file.py:section). 46 example files use # --8<-- [start:x] markers. About 130 markers are indented 4 or 8 spaces (e.g. docs/examples/predicates.py:55, aggregations.py:50, traversal.py:161). changelog.md:1 embeds CHANGELOG.md, contributing.md:5 embeds a section of CONTRIBUTING.md. |
Keep, add dedent_subsections = true (fixes the bug) and auto_append for the glossary. Keep check_paths = true. Do not add url_download (no remote snippets; the default off is the safe choice). restrict_base_path defaults to true and should stay that way (10.21.3 fixed a traversal hole). |
see TOML below |
pymdownx.highlight |
Pygments highlighting. Current anchor_linenums, line_spans="__span" and pygments_lang_class are exactly Zensical's recommended set (line_spans is needed for hl_lines, pygments_lang_class for annotations). |
364 python, 84 text, 23 sql, 16 bash, 5 toml fences. None use title=, hl_lines or linenums. |
Keep as is. Skip auto_title: it labels every block "Python", which adds noise when 90% of blocks are Python. Skip global linenums: snippets are short and the line numbers would get in the way of copying. Instead, add per-block title="docs/examples/quickstart.py" or title="ferro.toml" where the file name matters, and hl_lines for the line a paragraph is about. Those are authoring changes, not config changes. |
unchanged |
pymdownx.superfences |
Nesting plus the mermaid custom fence. | 5 mermaid diagrams (concepts/architecture.md:30-62 etc.). Fences nested in tabs everywhere. |
Keep | unchanged |
pymdownx.inlinehilite |
#!python x inline highlighting. |
0 uses. | Keep (cheap, Zensical-recommended; useful for inline SQL such as #!sql ALTER TABLE) |
unchanged |
pymdownx.tabbed |
Content tabs. | Every I-7 example: "Assignment" / "Annotated" pairs, e.g. concepts/architecture.md:169,181, concepts/identity-map.md:27,59; install tabs getting-started/installation.md:10,16; reference/configuration.md:9,16,30,44 ("ferro.toml" / "pyproject.toml"). |
Keep, add combine_header_slug and slugify. Anchors become #defining-a-model-annotated instead of #__tabbed_1_2 (verified). No page links to a #__tabbed_* anchor today, so nothing breaks. Also add the theme feature content.tabs.link: a reader who picks "Annotated" once sees Annotated everywhere. |
see TOML |
pymdownx.details |
Collapsible ??? admonitions. |
3 ??? example blocks. |
Keep | unchanged |
pymdownx.emoji |
Icons and emoji, Zensical's to_svg and twemoji. |
Theme icons (lucide/sun); few in-page uses. |
Keep (config already matches Zensical's) | unchanged |
pymdownx.keys |
++ctrl+c++ key rendering. |
0 uses. | Keep (cheap; Ctrl-C to stop zensical serve or ferro migrate prompts could use it). Could be dropped with no visible change. |
unchanged |
pymdownx.mark |
==highlight==. |
0 uses in prose. Every == sits in code (lambda user: user.role == "admin"). |
Keep (12.0 now marks mid-word by default; no prose collision found) | unchanged |
pymdownx.smartsymbols |
(c)→©, -->→→, 1/2→½, ordinals. |
No prose triggers found. Every --> is in mermaid fences or code. |
Keep | unchanged |
pymdownx.magiclink |
Auto-links bare URLs; #123 → issue; !123 → PR; commit SHAs; shortens long GitHub URLs. |
CHANGELOG.md (embedded at changelog.md:1): 20+ bare (#647), #511, #636... at lines 39, 71, 75, 173, 454, 499... Also api/queries.md:34 (#291). Explicit links such as roadmap.md:7 [#291](...) are left alone. |
Adopt. Caveat: repo_url_shorthand also turns @word in prose into a GitHub user link (verified: @property → github.com/property). There are 0 such cases in prose today, and decorators are already written in backticks. Keep it that way: an @decorator outside backticks, in docs or in mkdocstrings docstrings, would become a link. |
see TOML |
pymdownx.tasklist |
- [ ] checkboxes (custom_checkbox). |
roadmap.md is a bullet list of gaps, not a checklist; no - [ ] anywhere. |
Skip for now (Consider if the roadmap turns into done/not-done) | custom_checkbox = true if adopted |
pymdownx.blocks.caption |
Figure and table captions (Zensical "Images" recipe). | 5 mermaid diagrams and many tables, none captioned. No images. | Consider (useful for captioning the architecture diagrams) | [project.markdown_extensions.pymdownx.blocks.caption] |
pymdownx.blocks.admonition / .details / .tab / .html / .definition |
///-style generic block syntax, the successor to the legacy extensions. |
The site uses legacy !!!, ??? and === throughout, which is Zensical's documented syntax. |
Skip (switching syntax brings no reader-visible gain, and Zensical's docs document the legacy forms) | none |
pymdownx.caret / pymdownx.tilde |
^^ins^^, ^sup^, ~~del~~, ~sub~. |
No use. Prose is about SQL and code, with no chemistry, exponents or strikethrough. | Skip | none |
pymdownx.critic |
CriticMarkup {++ ++} editing marks. |
Not a docs-review workflow here (PRs are). | Skip | none |
pymdownx.arithmatex |
MathJax/KaTeX math (+JS). | No math anywhere. Risk: SQL placeholders like $1 ... $2 (guide/raw-sql.md:53,83, in code today) would become math if written in prose. |
Skip | none |
pymdownx.b64 |
Inline images as base64. | No images. | Skip | none |
pymdownx.betterem |
Smarter */_ emphasis (12.0 rewrite, CommonMark-compliant). |
Not listed by Zensical; Python-Markdown's default already handles snake_case without intraword emphasis. |
Skip | none |
pymdownx.escapeall |
Backslash-escape any char. | No need. | Skip | none |
pymdownx.extra |
Bundle (betterem, superfences, footnotes, attr_list, def_list, tables, abbr, md_in_html). | The pieces are configured one by one, which is better. | Skip | none |
pymdownx.fancylists |
Lettered and roman-numeral lists. | Only plain numbered lists in the docs. | Skip | none |
pymdownx.bracketspan (new in 12.0) |
[text]{.class} spans. |
No need; attr_list covers it. |
Skip | none |
pymdownx.progressbar |
Progress bars. | None. | Skip | none |
pymdownx.quotes (new in 10.20) |
Blockquotes that don't merge lazily, plus > [!NOTE] GitHub-style callouts. |
2 blockquotes: guide/row-level-security.md:88-91 (quoted refusal text) and concepts/architecture.md:43-50 (a status note that would read better as !!! note). |
Skip (admonitions are the house callout; turn that one blockquote into an admonition instead) | none |
pymdownx.saneheaders |
Requires a space after # and plays well with magiclink's #123. |
No #Heading without a space found. |
Consider (cheap insurance once MagicLink is on, so #123 at line start can never become a heading) |
[project.markdown_extensions.pymdownx.saneheaders] |
pymdownx.striphtml |
Strips comments and attributes from output. | <!-- output: ... --> comments in guide/schema/data-steps.md:19,117,... are harmless. |
Skip | none |
pymdownx.pathconverter |
Rewrites relative paths. | Zensical resolves links itself. | Skip | none |
Proposed full markdown_extensions block
This is the tail of zensical.toml (it replaces everything from [project.markdown_extensions.attr_list] on). The Adopt set is abbr, magiclink, the snippets dedent_subsections and auto_append options, the tabbed slug options and toc.permalink_title. def_list and footnotes are commented "Consider" lines; uncomment them on first use.
# --- Python-Markdown core ---
[project.markdown_extensions.abbr]
[project.markdown_extensions.attr_list]
[project.markdown_extensions.md_in_html]
[project.markdown_extensions.admonition]
[project.markdown_extensions.tables]
# Consider, enable on first use:
# [project.markdown_extensions.def_list]
# [project.markdown_extensions.footnotes]
[project.markdown_extensions.toc]
permalink = true
permalink_title = "Anchor link to this section"
# --- pymdown-extensions ---
[project.markdown_extensions.pymdownx.details]
[project.markdown_extensions.pymdownx.inlinehilite]
[project.markdown_extensions.pymdownx.keys]
[project.markdown_extensions.pymdownx.mark]
[project.markdown_extensions.pymdownx.smartsymbols]
[project.markdown_extensions.pymdownx.magiclink]
provider = "github"
user = "syn54x"
repo = "ferro-orm"
repo_url_shorthand = true # #123 -> issue, !123 -> PR, SHAs -> commit
repo_url_shortener = true # bare github.com/syn54x/ferro-orm/issues/291 -> "#291"
normalize_issue_symbols = true # render !123 as #123
[project.markdown_extensions.pymdownx.superfences]
custom_fences = [
{ name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" },
]
[project.markdown_extensions.pymdownx.snippets]
base_path = ["."]
check_paths = true
dedent_subsections = true # fixes indented section embeds
auto_append = ["docs/includes/abbreviations.md"] # glossary, outside docs_dir
[project.markdown_extensions.pymdownx.highlight]
anchor_linenums = true
line_spans = "__span"
pygments_lang_class = true
[project.markdown_extensions.pymdownx.tabbed]
alternate_style = true
combine_header_slug = true
slugify = { object = "pymdownx.slugs.slugify", kwds = { case = "lower" } }
[project.markdown_extensions.pymdownx.emoji]
emoji_index = "zensical.extensions.emoji.twemoji"
emoji_generator = "zensical.extensions.emoji.to_svg"
Theme features that pair with the above (add to [project.theme].features):
"content.tabs.link", # one Assignment/Annotated choice applies site-wide
"content.tooltips", # styled tooltips for abbr titles, permalinks, copy button
A starting glossary, docs/includes/abbreviations.md (Zensical recommends keeping it outside docs_dir, here docs/pages, so it is not built as a page; check_paths = true makes a missing file a build error):
*[ADR]: Architecture Decision Record
*[ASGI]: Asynchronous Server Gateway Interface
*[CRUD]: Create, Read, Update, Delete
*[DBAPI]: Python Database API (PEP 249)
*[DDL]: Data Definition Language: CREATE, ALTER and DROP statements
*[DML]: Data Manipulation Language: INSERT, UPDATE and DELETE statements
*[FFI]: Foreign Function Interface: the Python-to-Rust boundary
*[FK]: Foreign key
*[FKs]: Foreign keys
*[GIL]: Global Interpreter Lock
*[GUC]: Grand Unified Configuration: a Postgres runtime setting
*[GUCs]: Grand Unified Configuration settings (Postgres runtime settings)
*[IR]: Intermediate representation
*[JSONB]: Postgres binary JSON type
*[ORM]: Object-Relational Mapper
*[PK]: Primary key
*[RLS]: Row-Level Security (Postgres)
*[TTY]: Terminal
*[UUID]: Universally Unique Identifier
SQL, API, JSON and URL are left out on purpose: the readers know them, and they appear so often that underlining every one adds noise. abbr never touches code spans or fences (verified). Abbreviations are case-sensitive whole-word matches, plural forms need their own entries (FKs, GUCs), and they apply in the changelog too (72 hits there in the trial build).
Build verification
- Current repo config (
uv run --no-sync zensical build --cleanin the repo): builds in about 2.3s. The one warning isunresolved autorefpydantic.fields.FieldInfoin api/fields.md(pre-existing, mkdocstrings), plus griffe notes about**fieldsannotations insrc/ferro/models.py:730,758,817,850. Because of that warning,--strictfails both before and after the change, so it is not a regression. I rebuilt the repo'ssite/with the original config afterwards. - Trial (
/private/tmp/claude-501/-Users-taylor-orca-workspaces-ferro-orm-Docs-rewrite/46c7594e-13a3-4dbd-b539-6be8e5f4b3e1/scratchpad/pymdownx-trial/): a copy ofzensical.tomlwith the block above (withdef_listandfootnoteson, to prove they load), the two theme features and the glossary file, plus copies ofdocs/pages,docs/examples,src,CHANGELOG.md,CONTRIBUTING.mdandREADME.md, built withuv run --no-sync --project <repo> zensical build --clean. Result: builds cleanly, same single pre-existing warning, same page set. Checked in the output:guide/queries/"Filtering with where()": the code now starts at column 0 (adults = await User...). Before, it had 8 leading spaces.changelog/: 94magiclink-issuelinks;guide/row-level-security/:<abbr>on GUC, GUCs, RLS, DDL, ASGI, ADR.guide/models-and-fields/: tab labelsfor="defining-a-model-assignment"/"defining-a-model-annotated"(was__tabbed_1_1/__tabbed_1_2).- Duplicate HTML ids: 8
api/*pages have duplicate__codelineno-0-N/__span-0-Nids (mkdocstringsshow_source = truesource blocks all numbered from 0). This happens the same way in the baseline build, so the change did not cause it. The tab slugs added no duplicate ids. Worth a separate ticket.
- Zensical's
buildsupports-f/--config-file, butdocs_dirand snippetsbase_pathresolve against the project directory, so I ran the trial from a copy of the project tree.
12.x upgrade notes (from 10.21)
None of these break the current config. In detail:
- 12.0: BetterEm, Mark, Caret and Tilde rewritten. Emphasis is CommonMark-compliant, and
==mark==now works mid-word by default (smart_mark = truerestores the old rule). Ferro enablesmarkonly. Every==sits in code, so nothing changes. BetterEm, caret and tilde are not enabled. - 12.0: removed deprecated slug functions
uslugify*andgfm*. The config doesn't use them. The proposed tabbed slug usespymdownx.slugs.slugifywithkwds, the supported form. - 12.0: removed old-style custom emoji index logic. Ferro uses
zensical.extensions.emoji.twemojiandto_svg, Zensical's supported index and generator, and the build is fine. - 12.0: removed Arithmatex
inline_*/fence_*format options. Not used. - 12.0: Details and admonition titles handle headers specially. No impact.
- 12.0 new: BracketSpan. Skip (see the table).
- 11.0 (breaking, B64 only): relative paths are restricted to
base_path. Not used. - 11.0.x: performance fixes in InlineHilite, Keys, Blocks.HTML, BetterEm, Tilde, Caret and MagicLink. Free.
- 12.1: SuperFences custom fences accept an optional
reset. The mermaid fence doesn't need it. - 12.2: Highlight
auto_title_mapentries and SuperFencestitle_mode="html"allow HTML in code-block titles. Optional; useful only if file-name titles need markup. Tilde/Caretpunctuationoption. Python 3.15 support. - 12.3: BetterEm
punctuationoption. Not relevant. - Snippets security (10.21.3): fixed a directory-traversal regression with
restrict_base_path(default on). Keep the default. - Also from 10.20: the new
quotesextension. Skip.
Other observations (not extension config)
guide/queries.md:63says the lambda parameter is "tby convention", but I-8 says to name it after the model (user), and the snippet right below usesuser. This is a docs-content fix.concepts/architecture.md:43-50: the "Implementation status (2026-06-26)" note is a blockquote. A!!! notewould match the house style, and its claims may be stale after the one-planner work in AGENTS.md I-1.
1. What changed between 0.0.45 and 0.0.71
Breaking or changed defaults
| Version | Change | Impact on Ferro |
|---|---|---|
| 0.0.46 | Validation checks unresolved_references, unresolved_footnotes, unused_* and shadowed_* are now off by default. 0.0.56 then deprecated unresolved_references; autoref checking moved to invalid_links. |
None. Ferro sets no validation keys, and invalid_links and invalid_link_anchors stay on. |
| 0.0.58 | x86 and armv7 musl wheels removed. | None. CI runs on ubuntu-latest with glibc. |
| 0.0.58 | Build moved to the ZRX scheduler. Search, mkdocstrings and autorefs were reimplemented natively. | Built clean (see §4). |
| 0.0.63 | Dotfiles are no longer copied into site/. |
None. |
| 0.0.68 | Python ≥ 3.11 required. | None. CI uses 3.13 and local uses 3.14. |
| — | search.suggest is not a Zensical flag. Neither the templates nor the JS bundle reference it; I grepped the installed 0.0.71 package. The new search engine has no suggestions. |
Dead key in Ferro's config. Remove it. |
New features (with "since" version)
| Version | Feature |
|---|---|
| 0.0.46 | Search result excerpts. Single-character tokens no longer highlight. |
| 0.0.47 / 0.0.48 | markdown-exec support. mkdocstrings can be configured as a Markdown extension. Relative and scoped cross-references fixed. |
| 0.0.51 | objects.inv and autorefs are cached across rebuilds. |
| 0.0.52 | CJK search and a larger search modal. |
| 0.0.53 | strict = true config key, an alternative to the --strict flag. |
| 0.0.54 | Peak memory cut 8–10×. mike defaults fixed. |
| 0.0.58 | Native plugin replacements for meta, redirects, minify, tags, literate-nav, awesome-nav. |
| 0.0.61 | Anchor redirects ("old.md#a" = "new.md#b"), redirect chains, cycle rejection. |
| 0.0.62 | mkdocs-callouts. watch ignores the config file. |
| 0.0.64 | Blog plugin (a port of Material's). |
| 0.0.65 | mike version_selector, rss plugin, mkdocstrings backlinks, enable_inventory, autorefs settings. |
| 0.0.66 | mkdocs-autoapi and mkdocs-api-autonav. Sitemap now lists every published page. |
| 0.0.67 | social plugin (social cards plus OG/Twitter meta), llmstxt plugin (llms.txt, per-page .md, optional full file), exclude plugin, gh-admonitions. UI 0.0.34 adds a "Copy as Markdown" button (content.action.copy). |
| 0.0.68 | audio and video plugins. Search results use instant navigation. |
| 0.0.69 | nav-weight and markdownextradata. Emojis preserved in llms.txt. Symlink watching. |
| 0.0.70 | exclude_docs, draft_docs, not_in_nav settings. UI 0.0.37 adds draft markers, rel="alternate" type="text/markdown" and describedby discovery links for llms.txt, and search accessibility fixes. |
| 0.0.71 | include-markdown plugin. Social cards keep palette defaults when colors are unset. No false restarts during file discovery. Oversized search fields are rejected. |
Not yet in Zensical: native versioning (on the roadmap; today only the mike fork), the privacy and optimize plugins (planned / in progress), git-revision-date and git-authors (planned), and gen-files (unsupported). Analytics is "being overhauled" (the setup/analytics page).
2. Feature table
"Ferro today" describes the current zensical.toml. "Config" is the TOML to add.
| Feature | Since | What it does | Ferro today | Verdict + reason | Config | Doc |
|---|---|---|---|---|---|---|
content.tabs.link |
≤0.0.45 | Tabs with the same label switch together across the whole site, and the choice persists across page loads and instant navigation. | Off | Adopt. There are 55 Assignment/Annotated pairs in 18 pages (rule I-7). Pick a style once and every page follows. uv/pip tabs sync too. |
"content.tabs.link" |
https://zensical.org/docs/authoring/content\-tabs/\#linked\-content\-tabs |
navigation.instant |
≤0.0.45 | SPA-style navigation over XHR. The search index stays loaded between pages. Needs site_url (set). |
Off | Adopt. Faster page changes; search results use it from 0.0.68. The old inline-script redirect page goes away with the redirects plugin, and the inline scripts left in concepts/architecture.md and identity-map.md are covered by the UI 0.0.25 fix that keeps inline-script attributes during instant navigation. Spot-check those two pages in serve. |
"navigation.instant" |
https://zensical.org/docs/setup/navigation/\#instant\-navigation |
navigation.instant.progress |
≤0.0.45 | Shows a progress bar when a page takes longer than 400 ms. | Off | Adopt. Free once instant navigation is on. | "navigation.instant.progress" |
same page, #progress-indicator |
navigation.instant.prefetch |
≤0.0.45 | Prefetches a page on hover. | Off | Consider. The docs call it experimental. | "navigation.instant.prefetch" |
same page, #instant-prefetching |
Instant previews (zensical.extensions.preview) |
≤0.0.45 | Hover popups for header links to targeted pages. | Off | Consider for api/* targets, so guide links into the API reference preview inline. It belongs to the Markdown-extension auditor. |
[[project.markdown_extensions.zensical.extensions.preview.configurations]] targets.include = ["api/*"] |
same page, #instant-previews |
navigation.tracking |
≤0.0.45 | The URL follows the active TOC anchor. | Off | Adopt. Long guide pages become shareable at the section a reader is on. | "navigation.tracking" |
same page, #anchor-tracking |
navigation.path |
≤0.0.45 | Breadcrumbs above the title. | Off | Adopt. The nav is three levels deep (Guide › Schema Management › …), and mobile readers lose their place. | "navigation.path" |
same page, #navigation-path |
navigation.footer |
≤0.0.45 | Previous/next links in the footer. | Off | Adopt. Getting Started and Schema Management are read in sequence. | "navigation.footer" |
https://zensical.org/docs/setup/footer/\#navigation |
navigation.indexes |
≤0.0.45 | Lets a section own an index.md page. |
Off | Consider. Rename guide/schema/overview.md to guide/schema/index.md so "Schema Management" itself links to the overview. The page moves, so it needs a redirect. Incompatible with toc.integrate. |
"navigation.indexes" |
same page, #section-index-pages |
navigation.prune |
≤0.0.45 | Drops nav markup that isn't visible. | Off | Skip. Incompatible with navigation.expand, and the site is small. |
— | same page, #navigation-pruning |
navigation.tabs.sticky |
≤0.0.45 | Tabs stay visible while scrolling. | Off | Skip. A matter of taste. | — | same page, #sticky-navigation-tabs |
toc.integrate |
≤0.0.45 | Moves the TOC into the left sidebar. | Off | Skip. The right-hand TOC is useful on long pages. | — | same page, #navigation-integration |
content.code.select |
≤0.0.45 | Line-range selection button on code blocks. | Off | Adopt. Cheap, and readers can share a specific line range in an example. | "content.code.select" |
https://zensical.org/docs/authoring/code\-blocks/ |
content.tooltips |
≤0.0.45 | Styled tooltips for title attributes and abbreviations. |
Off | Adopt. Better rendering for link titles and the palette toggle. | "content.tooltips" |
https://zensical.org/docs/authoring/tooltips/ |
content.footnote.tooltips |
≤0.0.45 | Shows footnotes inline on hover. | Off | Adopt. No effect unless footnotes are enabled (Markdown auditor). Harmless. | "content.footnote.tooltips" |
https://zensical.org/docs/authoring/footnotes/ |
content.action.copy |
0.0.67 | "Copy as Markdown" button on pages that have a .md twin (requires llmstxt). |
Off | Adopt together with llmstxt. |
"content.action.copy" |
https://zensical.org/docs/compatibility/mkdocs/plugins/\#llmstxt |
header.autohide |
≤0.0.45 | Hides the header while scrolling. | Off | Skip. It also hides the tabs bar, and the tabs carry the nav. | — | https://zensical.org/docs/setup/header/\#automatic\-hiding |
announce.dismiss / announcement bar |
≤0.0.45 | Release banner; needs a custom_dir override of the announce block. |
None | Consider. Ferro releases often ("0.25 adds ferro migrate squash"). It needs custom_dir = "docs/overrides" and a main.html that extends base.html. |
custom_dir = "docs/overrides" + "announce.dismiss" |
https://zensical.org/docs/setup/header/\#announcement\-bar |
search.suggest |
n/a | Not a Zensical feature (the Material/Lunr feature). | On (dead) | Remove. No template or JS references it. | delete line | https://zensical.org/docs/setup/search/ |
search.share |
n/a | Exists only as an i18n string. The new search has no share button. | Off | Skip. | — | same page |
Search separator / per-page search.exclude |
≤0.0.45 | Tunes tokenization and excludes pages or sections. | Default | Skip. Excluding changelog.md is possible (search: exclude: true), but release notes are useful search hits. |
— | https://zensical.org/docs/setup/search/ |
| Palette: automatic mode | ≤0.0.45 | Adds a third "system" toggle (media = "(prefers-color-scheme)"). |
Light/dark via media query, no "auto" state | Adopt. Once a user toggles manually there is currently no way back to following the OS. | see diff | https://zensical.org/docs/setup/colors/\#automatic\-light\-dark\-mode |
Custom scheme light-rust + primary = "custom" |
≤0.0.45 | Custom colors via CSS variables. | Used | Keep. It still works on 0.0.71 (verified in the build). The commented-out :root > * block in extra.css is dead code. |
— | https://zensical.org/docs/setup/colors/\#custom\-colors |
Theme variant (modern default / classic) |
≤0.0.45 | Classic keeps the Material look. | modern (default) | Skip. Modern is the default and extra.css targets variables that both variants share. |
— | https://zensical.org/docs/compatibility/mkdocs/migration/\#theme\-variant |
Fonts (font.text/font.code, font = false) |
≤0.0.45 | Google Fonts or system fonts. | Default (Google Fonts) | Consider font = false for privacy, since the privacy plugin that would self-host fonts is only "planned". Otherwise skip. |
[project.theme] font = false |
https://zensical.org/docs/setup/fonts/ |
Logo / favicon / icon.logo |
≤0.0.45 | Brand image in the header and browser tab. | None. No image assets in docs/pages. |
Consider. Any Ferro mark would do. Until one exists, [project.theme.icon] logo = "lucide/..." gives a placeholder, and the favicon stays Zensical's default. |
logo = "assets/ferro.svg", favicon = "assets/favicon.png" |
https://zensical.org/docs/setup/logo\-and\-icons/ |
extra.social |
≤0.0.45 | Footer icons. | None | Adopt GitHub and PyPI. | see diff | https://zensical.org/docs/setup/footer/\#social\-links |
extra.generator |
≤0.0.45 | "Made with Zensical" notice. | Default (shown) | Skip. Keep it. | — | same page |
repo_url / edit_uri / content.action.edit/view |
≤0.0.45 | Repo widget plus edit/view buttons. | Set correctly (edit/main/docs/pages/) |
Keep. | — | https://zensical.org/docs/setup/repository/ |
social plugin (social cards) |
0.0.67 | 1200×630 PNG card per page plus og:* / twitter:* meta. No extra install. |
None. Shared links show no preview image. | Adopt. Verified: 50 cards. The defaults render indigo because primary = "custom"; set cards_layout_options.background_color to the rust color. Cards cache in .cache/ (gitignored). |
see diff | https://zensical.org/docs/compatibility/mkdocs/plugins/\#social |
llmstxt plugin |
0.0.67 | Writes /llms.txt, a .md twin of each page, an optional llms-full.txt, and <link rel=alternate type=text/markdown>. |
None | Adopt. An ORM's users increasingly ask AI tools how to use it. Caveats seen in the build: (1) tab labels are dropped in the .md twins, so "Assignment"/"Annotated" become two unlabeled code blocks in a row (worth an upstream issue); (2) & in nav titles comes out as & in llms.txt; (3) globs list pages alphabetically, not in nav order (list files explicitly where order matters); (4) llms-full.txt is about 730 KB, mostly API reference. |
see diff | same page, #llmstxt |
redirects plugin + anchor redirects |
0.0.58 / 0.0.61 | Generates redirect pages with an anchor map and a fallback for unmapped anchors. | guide/migrations.md is a hand-written redirect page: a table plus an inline <script> hash map. It is built, it's in the sitemap and search, and it's not in nav. |
Adopt. Replace it one-for-one with redirect_maps (16 entries, same targets) and delete docs/pages/guide/migrations.md (the docs say no source file may remain). Verified: the generated page carries the same hash map and is no longer in the sitemap or the search index. |
see diff | https://zensical.org/docs/setup/redirects/ |
not_in_nav / draft_docs / exclude_docs |
0.0.70 | Keep pages out of auto-nav, preview-only drafts, exclusions. | n/a (explicit nav) |
Consider draft_docs for in-progress pages during the rewrite: shown in serve, left out of build. not_in_nav has no effect with an explicit nav. |
draft_docs = """\n*.draft.md\n""" |
https://zensical.org/docs/setup/basics/\#draft\_docs |
strict = true |
0.0.53 | Turns warnings into a failed build, from config. | Off; CI runs build --clean without --strict |
Adopt once the one warning is fixed (pydantic.fields.FieldInfo autoref in api/fields.md, mkdocstrings auditor). Then add -s in publish-docs.yml (or strict = true) so broken links fail CI. Verified that -s aborts today. |
strict = true |
https://zensical.org/docs/setup/validation/\#strict\-mode |
validation.* |
0.0.46/0.0.56 | Link and anchor checks are on by default; the others are deprecated. | Defaults | Keep defaults. Don't enable the deprecated checks. | — | https://zensical.org/docs/setup/validation/ |
Page metadata: description, icon, status, hide |
≤0.0.45 | Per-page meta description and OG text, nav icons, new/deprecated badges, hiding toc/nav/path/footer. |
No page has front matter, so every page's og:description is the site description. |
Adopt status + description. Define extra.status (built-in ids are new and deprecated) and mark pages like guide/schema/* new for 0.25 and howto/upgrade-guide.md. A per-page description: gives each social card and search snippet its own text (content work for the docs rewrite). Icons: Consider. hide: [navigation] on index.md: Consider. |
[project.extra] status.new = "Recently added" |
https://zensical.org/docs/authoring/frontmatter/ |
meta plugin |
0.0.58 | .meta.yml front matter per directory. |
None | Skip for now. It becomes useful if the status badges apply to whole folders. |
— | https://zensical.org/docs/compatibility/mkdocs/plugins/\#meta |
tags plugin |
0.0.58 | Page tags plus listings. | None | Skip. The explicit nav already structures the site. | — | https://zensical.org/docs/setup/tags/ |
minify plugin |
0.0.58 | Minifies HTML/JS/CSS. | None | Skip. GitHub Pages already gzips. Small gain. | — | https://zensical.org/docs/compatibility/mkdocs/plugins/\#minify |
Versioning (mike fork, extra.version.provider = "mike") |
0.0.30 (fork); version_selector 0.0.65 |
Version selector across deployed versions. | None | Skip for now. It needs pip install git+https://github.com/squidfunk/mike.git (a transitional fork with no new features) and the gh-pages branch push model, which clashes with Ferro's actions/deploy-pages artifact workflow. Native versioning is on Zensical's roadmap. Revisit when it lands; until then howto/upgrade-guide.md plus changelog.md cover old versions. |
— | https://zensical.org/docs/compatibility/mkdocs/mike/ |
blog / rss plugins |
0.0.64 / 0.0.65 | Blog with archives and categories; RSS/JSON feeds. | None | Skip. GitHub Releases and changelog.md already carry release notes. Consider a blog only if release posts become a thing. |
— | https://zensical.org/docs/setup/blog/ |
| Analytics + "Was this page helpful?" | ≤0.0.45 | GA4 plus a feedback widget. | None | Skip unless the maintainer wants GA. It needs extra.consent for GDPR, and the docs say analytics is being overhauled. |
— | https://zensical.org/docs/setup/analytics/ |
| Cookie consent / data privacy | ≤0.0.45 | Consent banner. | n/a | Skip (no analytics). | — | https://zensical.org/docs/setup/data\-privacy/ |
| Offline plugin | ≤0.0.45 | file:// browsing; forces use_directory_urls = false. |
None | Skip. The docs are web-hosted. | — | https://zensical.org/docs/setup/offline/ |
| Comment system (Giscus) | ≤0.0.45 | Comments via template override. | None | Skip. GitHub Discussions and issues are the channel. | — | https://zensical.org/docs/setup/comment\-system/ |
| Sitemap | ≤0.0.45 (fixed 0.0.66) | sitemap.xml. |
Generated, 50 URLs | Keep. Automatic. | — | https://zensical.org/docs/setup/navigation/\#instant\-navigation |
| 404 page | ≤0.0.45 | Default 404.html; overridable via custom_dir/404.html. |
Default generated | Keep. GitHub Pages serves 404.html automatically. |
— | https://zensical.org/docs/customization/\#custom\-error\-pages |
watch |
≤0.0.45 (symlink fixes 0.0.69, no false restarts 0.0.71) | Extra paths that trigger a full rebuild in serve. |
src, docs/examples, README, CHANGELOG, CONTRIBUTING |
Keep (redundant but harmless). mkdocstrings paths (src) and snippets base_path (.) are watched automatically, so every entry is already covered. Trim it to [] if serve feels noisy. |
— | https://zensical.org/docs/setup/basics/\#watch |
zensical serve options |
— | -a/--dev-addr, -o/--open, -f; -s is "currently unsupported" in serve. dev_addr is also a config key. |
n/a | Info only. | — | https://zensical.org/docs/usage/preview/ |
zensical build options |
— | -f, -c/--clean, -s/--strict. The docs say --clean should soon be unnecessary. |
CI uses --clean |
Keep --clean in CI and add -s later. |
— | https://zensical.org/docs/usage/build/ |
3. Proposed diff to zensical.toml
Also delete docs/pages/guide/migrations.md, because the redirects plugin replaces it. Feature list is alphabetized. search.suggest is removed. This exact file built clean (see §4).
@@ -82,20 +82,34 @@
[project.theme]
features = [
- "navigation.tabs",
- "navigation.sections",
+ "content.action.copy",
+ "content.action.edit",
+ "content.action.view",
+ "content.code.annotate",
+ "content.code.copy",
+ "content.code.select",
+ "content.footnote.tooltips",
+ "content.tabs.link",
+ "content.tooltips",
"navigation.expand",
+ "navigation.footer",
+ "navigation.instant",
+ "navigation.instant.progress",
+ "navigation.path",
+ "navigation.sections",
+ "navigation.tabs",
"navigation.top",
- "toc.follow",
- "search.suggest",
+ "navigation.tracking",
"search.highlight",
- "content.code.copy",
- "content.code.annotate",
- "content.action.edit",
- "content.action.view",
+ "toc.follow",
]
[[project.theme.palette]]
+media = "(prefers-color-scheme)"
+toggle.icon = "lucide/sun-moon"
+toggle.name = "Switch to light mode"
+
+[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "light-rust"
primary = "custom"
@@ -109,8 +123,61 @@
primary = "custom"
accent = "amber"
toggle.icon = "lucide/moon"
-toggle.name = "Switch to light mode"
+toggle.name = "Switch to system preference"
+[project.extra]
+status.new = "Recently added"
+status.deprecated = "Deprecated"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/github"
+link = "https://github.com/syn54x/ferro-orm"
+name = "Ferro on GitHub"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/python"
+link = "https://pypi.org/project/ferro-orm/"
+name = "Ferro on PyPI"
+
+[project.plugins.llmstxt]
+markdown_description = "Ferro is an async Python ORM: Pydantic models, a Rust core, SQLite and Postgres."
+full_output = "llms-full.txt"
+
+[project.plugins.llmstxt.sections]
+"Start here" = [
+ "index.md",
+ "why-ferro.md",
+ "getting-started/installation.md",
+ "getting-started/quickstart.md",
+ "getting-started/next-steps.md",
+]
+"Guide" = ["guide/*.md", "guide/schema/*.md"]
+"How-to" = ["howto/*.md"]
+"Concepts" = ["concepts/*.md"]
+"Reference" = ["reference/*.md", "api/*.md"]
+"Community" = ["faq.md", "roadmap.md", "contributing.md"]
+
+[project.plugins.redirects.redirect_maps]
+"guide/migrations.md" = "guide/schema/overview.md"
+"guide/migrations.md#three-ways-to-manage-schema" = "guide/schema/overview.md#the-ladder"
+"guide/migrations.md#auto-migration" = "guide/schema/auto-migrate.md"
+"guide/migrations.md#creating-tables-with-auto_migratetrue" = "guide/schema/auto-migrate.md#creating-tables-with-auto_migratetrue"
+"guide/migrations.md#applying-column-changes
…(truncated; see the audit report)
</details>
Activity
1 remaining item
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: worker-701 (reviewed by reviewer-701)
Branch:
sdd/701-docs-site-configPR: #729
Verify: passed d48d7cc
Review: Spec PASS, Quality PASS (one Minor: the pytest CI jobs rebuild the docs and fetch remote inventories).
Accepted deviations:
filters = ["!^_"](with"public", the site still renders__ferro_*__attributes), theapi/fields.mddirective options, and the docs group in the pytest jobs.Trial merge into
feat/docs-rewriteat a6fa522 (#726, #727, #728) passed.feat(docs): site configuration fixes and a strict docs build (#701) (#…
0x054 commentedon Oct 19, 2026
Merged in #729 into
feat/docs-rewritedocs: schema pages fixed against the generator, quoted step files pin…
docs(api): every export has one API entry under its public path (#722) (
docs: six-tab nav, page descriptions and redirects for moved pages (#723