Skip to content

Docs site config: Zensical features, Markdown extensions, API reference options and a strict build #701

Closed

Description

@0x054

Parent

#698

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_slug with 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 through snippets.auto_append + content.tooltips). Glossary wording follows CONTEXT.md where it defines the term.
  • #NNN references link to GitHub (pymdownx.magiclink, repo syn54x/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.status badges new and deprecated. search.suggest is removed.
  • Plugins: redirects with 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; social with 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; the griffe-inherited-docstrings extension (added to the docs and dev dependency groups); show_overloads: false and parameter_headings on the Field directive. show_if_no_docstring stays 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 **fields parameters of create, upsert, get_or_create and update_or_create are annotated **fields: Any, so griffe stops warning.
  • The docs build runs with --strict in the publish workflow and in a new docs job in CI.
  • A new tests/test_docs_site.py builds 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 --strict exits 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.
  • Field renders 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 **fields annotations only)
  • Delete: docs/pages/guide/migrations.md

Interfaces

Test scenarios

  • Happy path: test_docs_site.py builds the site strictly and passes.
  • Error path: reverting dedent_subsections makes 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:cacheprovider

Blocked 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, Row and Model get the pydantic-model label and a Config section (frozen: True, extra: forbid, and for Model from_attributes, use_attribute_docstrings, arbitrary_types_allowed). PoolConfig and DatabaseSettings also get Fields and Validators lists, and fields and validators get pydantic-field / pydantic-validator labels.
  • FerroSettings (a BaseSettings) is not detected unless preload_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:

  1. Field constraints vanish from the displayed value. max_connections = PydanticField(default=5, ge=1) renders as max_connections = 5, and name = Field(min_length=1) renders as name. Today the constraint is visible. Unless each field's docstring states the constraint, docs lose information.
  2. A private validator leaks. Model's Validators section lists _validate_required_foreign_keys.
  3. schema = true silently turned the extension off in static mode: the output was identical to having no extension. A JSON schema would need dynamic import of ferro, which means building the Rust extension in the docs job.
  4. preload_modules = ["pydantic"] crashes the build (TypeError: 'NoneType' object is not iterable), so it can't be combined with unpack_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-signature blocks 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 relationships changed. Relation.all / Relation.first gained 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 (anchor ferro.Field(index) present), and show_inheritance_diagram: true on FerroError (one pre.mermaid emitted).
  • griffe-pydantic (static, schema=false), with and without the pydantic_settings preload. Its schema=true mode is a no-op. A pydantic preload crashes the build.
  • unpack_typeddict and griffe_typingdoc crash without a preload. With preload_modules=["pydantic"], unpack_typeddict builds.
  • griffe_warnings_deprecated, griffe_generics, griffe_modernized_annotations, griffe_fieldz and griffe_public_redundant_aliases load 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 for doc-symbol, doc-backlink, doc-label and mermaid.

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_inheritance and griffe_fastapi were 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.0 is a Ferro runtime dependency (pyproject line 12), so the preload should resolve there.

TL;DR

  1. 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 in docs/examples/*.py sit inside async def main():, so the embedded code shows up indented on the page. For example, guide/queries.md:65 embeds predicates.py:filtering and the page shows adults = await User.... One option fixes it: pymdownx.snippets.dedent_subsections = true. I checked this in the trial build.
  2. 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. Setting tabbed.combine_header_slug and slugify gives defining-a-model-assignment. The theme feature content.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.
  3. Glossary tooltips: add abbr plus snippets.auto_append with docs/includes/abbreviations.md for 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 feature content.tooltips.
  4. MagicLink for #NNN issue refs. CHANGELOG.md is embedded through changelog.md and has about 20 bare refs like (#647) that render as plain text today, plus api/queries.md:34. In the trial build this produced 94 issue links on the changelog page.
  5. 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.FieldInfo warning.

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 --clean in the repo): builds in about 2.3s. The one warning is unresolved autoref pydantic.fields.FieldInfo in api/fields.md (pre-existing, mkdocstrings), plus griffe notes about **fields annotations in src/ferro/models.py:730,758,817,850. Because of that warning, --strict fails both before and after the change, so it is not a regression. I rebuilt the repo's site/ 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 of zensical.toml with the block above (with def_list and footnotes on, to prove they load), the two theme features and the glossary file, plus copies of docs/pages, docs/examples, src, CHANGELOG.md, CONTRIBUTING.md and README.md, built with uv 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/: 94 magiclink-issue links; guide/row-level-security/: <abbr> on GUC, GUCs, RLS, DDL, ASGI, ADR.
    • guide/models-and-fields/: tab labels for="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-N ids (mkdocstrings show_source = true source 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 build supports -f/--config-file, but docs_dir and snippets base_path resolve 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 = true restores the old rule). Ferro enables mark only. Every == sits in code, so nothing changes. BetterEm, caret and tilde are not enabled.
  • 12.0: removed deprecated slug functions uslugify* and gfm*. The config doesn't use them. The proposed tabbed slug uses pymdownx.slugs.slugify with kwds, the supported form.
  • 12.0: removed old-style custom emoji index logic. Ferro uses zensical.extensions.emoji.twemoji and to_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_map entries and SuperFences title_mode="html" allow HTML in code-block titles. Optional; useful only if file-name titles need markup. Tilde/Caret punctuation option. Python 3.15 support.
  • 12.3: BetterEm punctuation option. 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 quotes extension. Skip.

Other observations (not extension config)

  • guide/queries.md:63 says the lambda parameter is "t by convention", but I-8 says to name it after the model (user), and the snippet right below uses user. This is a docs-content fix.
  • concepts/architecture.md:43-50: the "Implementation status (2026-06-26)" note is a blockquote. A !!! note would 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 &amp; 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. added
    documentationImprovements or additions to documentation
    ready-for-agentUnblocked and fully specified; an agent may claim it
    on Oct 19, 2026
  2. added theissue type on Oct 19, 2026
  3. added
    size:MNeeds a plan; one or two local workers
    on Oct 19, 2026
  4. 1 remaining item

  5. self-assigned this
    on Oct 19, 2026
  6. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-701 (reviewed by reviewer-701)
    Branch: sdd/701-docs-site-config
    PR: #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), the api/fields.md directive options, and the docs group in the pytest jobs.
    Trial merge into feat/docs-rewrite at a6fa522 (#726, #727, #728) passed.

  7. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #729 into feat/docs-rewrite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workers

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions