Skip to content

Add support for HTML layouts with Snippet, Slots, and a default text processor#53

Merged
volfpeter merged 22 commits into
mainfrom
feat/html-layouts
Feb 3, 2026
Merged

Add support for HTML layouts with Snippet, Slots, and a default text processor#53
volfpeter merged 22 commits into
mainfrom
feat/html-layouts

Conversation

@volfpeter
Copy link
Copy Markdown
Owner

@volfpeter volfpeter commented Jan 18, 2026

Closes #52

TODOs:

  • Let users fully customize the layout.html to Layout conversion
  • Example: HTML layout with children slot
  • Example: HTML layout with multiple slots and page returning dict with keys corresponding to layout slots
  • Example: HTML layout with custom layout.html to Layout conversion (for example defining default slots that are available in every layout)
  • Rethink public API, make more internals accessible in the documentation
  • Guide: HTML layout with children slot
  • Guide: HTML layout with multiple slots
  • Guide: HTML layout with custom layout.html to Layout conversion
  • Documentation

Summary by CodeRabbit

  • New Features

    • Add HTML-based layouts (layout.html) with multi-slot and default-slot support, plus a configurable str_to_layout converter and package-based resource loading.
    • New text-to-layout utilities and default text processor for templating.
  • Documentation

    • New guides and updated docs covering HTML layouts, multi-slot layouts, and default-slot patterns.
  • Examples

    • New runnable examples demonstrating HTML layouts and default/nav slot usage.
  • Tests

    • Added tests ensuring layout precedence and HTML-layout rendering.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 18, 2026

📝 Walkthrough

Walkthrough

Adds package resource loading and a string-to-layout conversion path to support plain HTML layouts, propagates a text-to-layout converter through App/API construction, updates typing/typing organization, adds utilities and examples for HTML multi-slot and default-slot layouts, plus tests and docs.

Changes

Cohort / File(s) Summary
Core model & resource loading
holm/_model.py
Add importlib.resources usage, package resource handling, new typing aliases, and PackageInfo.import_resource(...) to read and transform package resource text.
App wiring & layout fallback
holm/app.py
Add str_to_layout parameter to App and _build_api; fallback to transform layout.html when layout.py is absent; propagate converter into nested API/router builders and child routers.
Layout abstractions & transformer
holm/modules/_layout.py, holm/utils.py
Introduce LayoutDefinition/CustomLayoutDefinition protocol/dataclass, make_str_to_layout_definition_transformer, is_layout_definition, snippet_to_layout, and default_text_processor for HTML template -> Layout conversion and slot handling.
Typing reorg
holm/typing.py, holm/modules/_api.py, holm/modules/_page.py, holm/modules/_error.py
Restructure and expand public typing surface (Layout/Page/TextToLayoutConverter/APIFactory), move many imports under TYPE_CHECKING, and defer annotations to minimize runtime imports.
Examples & demos
examples/html-layout/**, examples/html-layout-default-slots/**, examples/html-multi-slot-layout/**
Add three complete HTML-layout example apps (single-slot, default-slot with navbar, multi-slot) including layout.html, pages, nav, and main bootstrap files; add README snippets.
Tests & test apps
test_app/html_layout/*, test_app/html_and_python_layout/*, tests/html_layout/*, tests/html_and_python_layout/*
Add HTML-layout and precedence tests; include sample layout.html and layout.py combinations to assert Python layout precedence over HTML and multi-slot rendering.
Public exports & docs
holm/__init__.py, docs/api/*, docs/guides/*, docs/*
Remove some re-exports (FastAPIDependency/FastAPIErrorHandler/ErrorHandlerMapping), add typing/utils docs, many new guides detailing HTML layout rules and examples, and update docs navigation.
Build, deps & CI
pyproject.toml, .github/workflows/build-docs.yml, mkdocs.yml
Bump htmy and several dev deps, expand mypy excludes, and rewrite GitHub Actions workflow for docs with caching and explicit steps.
Misc examples & housekeeping
AGENTS.md, ai/doc-guide.md, holm/logging.py, .ignore
Add project guidelines and doc style guide, tiny logging module docstring, and update ignore file; update copyright years in many examples/docs.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant App as App / Router Builder
    participant FS as Package Resources
    participant Transformer as str_to_layout / TextProcessor
    participant Renderer as Renderer

    Client->>App: Request route (GET /path)
    App->>App: Locate layout (check for layout.py)
    alt Python layout exists
        App->>Renderer: use Python layout_definition
    else HTML layout fallback
        App->>FS: import_resource("layout.html")
        FS-->>App: HTML text (or None)
        App->>Transformer: str_to_layout(HTML text)
        Transformer-->>App: LayoutDefinition (with slots/text processing)
        App->>Renderer: use LayoutDefinition
    end
    Renderer->>Transformer: process(content, context)
    Transformer-->>Renderer: formatted slots / HTML
    Renderer-->>Client: HTML response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Form handling #22 — touches holm/_model.py with resource-loading and import-related changes; closely related to package resource handling added here.
  • Feat/actions #31 — modifies action registration and app wiring in holm/app.py; overlaps with this PR's App construction and action sourcing changes.
  • Add documentation for file-system based routing #24 — shares documentation updates for file-system-based routing and examples; likely related to the new HTML-layout guides and examples.

Poem

🐰 In a burrow of code I nibble and play,
I stitch HTML with slots the bright way,
Python bows out when plain files delight,
Metadata dances, requests shine at night,
Hopping with joy — layouts bloom in my sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: HTML layout support with Snippet, Slots, and text processor functionality.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all coding requirements from issue #52: HTML layout support, layout.py precedence, dynamic loading/formatting, Metadata and Request exposure via TextProcessor, children slot support, and dict-based slot mapping.
Out of Scope Changes check ✅ Passed All changes are directly related to HTML layout support objectives. Year updates (2025→2026), dependency updates, documentation, examples, and API reorganization all support the core feature. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/html-layouts

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pyproject.toml`:
- Line 10: Update the pyproject dependency from "htmy[lxml]>=0.10.2" to
"htmy[lxml]>=0.10.1" and then audit type annotations that reference specific
html tag objects (e.g., usages like html.div, html.form) across the codebase:
replace those annotations with htmy-provided types such as ComponentType or
Component (imported from htmy) where needed, and run type checks to ensure
compatibility with htmy v0.10.0+ breaking change.

Comment thread pyproject.toml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@examples/html-layout/my_app/layout.html`:
- Line 25: Update the hard-coded copyright year in the layout node that
currently reads "<p>© 2025 My App</p>" to the current year (change 2025 to
2026), or replace it with a dynamic year expression if your templating engine
supports it so it auto-updates in future releases.

In `@holm/_model.py`:
- Around line 178-184: The import_resource method should early-return None when
the application has no package root: add a check at the start of import_resource
that if self.package_name is in _no_app_package_roots then return None; this
mirrors import_module's handling (see import_module at line ~143), prevents
importlib.resources.read_text from raising ValueError for non-package apps, and
keeps behavior consistent with app.py's fallback flow.
🧹 Nitpick comments (1)
holm/_model.py (1)

185-195: Broad exception handling is acceptable but consider narrowing.

The static analysis flags the broad except Exception (BLE001). While defensive programming is reasonable for resource loading, consider whether you can enumerate more specific exceptions that importlib.resources.read_text might raise (e.g., TypeError, OSError).

The TRY003 hint about the long error message is a minor style concern—the current message is helpful for users encountering this error.

Optional: Narrow exception types
-        except Exception:
+        except (TypeError, OSError, AttributeError) as e:
             import traceback

             logger.warning(f"Failed to load resource {filename} from package '{package_name}'.")
             logger.warning(traceback.format_exc())
             return None

Comment thread examples/html-layout/my_app/layout.html Outdated
Comment thread holm/_model.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@holm/modules/_layout.py`:
- Around line 69-91: The html_to_layout function currently passes raw HTML to
default_text_processor which uses Python's str.format and will break on literal
braces; fix by ensuring html_to_layout uses a safe processor: implement a small
safe_text_processor that first escapes literal braces (replace "{" -> "{{" and
"}" -> "}}") then delegates to default_text_processor (or returns the text
unchanged), and call that processor when building the Snippet in html_to_layout
(referencing html_to_layout, default_text_processor, text_processor, Snippet,
and Text); alternatively update the default_text_processor itself to be
brace-safe so html_to_layout can keep its signature.
♻️ Duplicate comments (1)
holm/_model.py (1)

163-184: Non‑package apps will crash on layout.html loading.

import_resource raises ValueError for _no_app_package_roots, but AppConfig explicitly supports non‑packaged apps and app.py now calls this for layout.html. That path will raise at startup when layout.py is absent. Please mirror import_module and return None (and update the docstring) to keep graceful fallback.

🔧 Suggested fix
-        if package_name in _no_app_package_roots:
-            raise ValueError(
-                f"The resource ({filename}) you are trying to load is not in a Python package "
-                "and can not be safely loaded as a result."
-            )
+        if package_name in _no_app_package_roots:
+            return None
🧹 Nitpick comments (1)
holm/modules/_layout.py (1)

83-85: Silence unused ctx parameter.

Ruff flags the unused argument; renaming to _ctx keeps the signature without lint suppression.

♻️ Suggested change
-    def layout(props: Any, ctx: Context) -> Component:
+    def layout(props: Any, _ctx: Context) -> Component:

Comment thread holm/modules/_layout.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@holm/modules/_layout.py`:
- Around line 107-109: The layout component currently declares an unused
parameter ctx in the function signature (def layout(props: Any, ctx: Context) ->
Component) which triggers Ruff ARG001; rename the parameter to _ctx (i.e., def
layout(props: Any, _ctx: Context) -> Component) to indicate it is intentionally
unused and silence the linter, making sure to update any internal references (if
any) from ctx to _ctx or remove them if none exist.
🧹 Nitpick comments (1)
holm/_model.py (1)

186-195: Don’t swallow unexpected resource-load errors.
Returning None for all exceptions can mask decode/permission/package issues and silently skip layout.html. Consider re-raising unexpected exceptions after logging, or narrowing the catch to only “not found” cases.

♻️ Suggested tweak
-        except Exception:
+        except Exception:
             import traceback
 
             logger.warning(f"Failed to load resource {filename} from package '{package_name}'.")
             logger.warning(traceback.format_exc())
-            return None
+            raise

Comment thread holm/modules/_layout.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@examples/html-layout-default-slots/README.md`:
- Line 1: Replace the fragment first line in the README ("The \"HTML layout\"
guide with a custom HTML layout converter that provides a dynamic navbar to HTML
layouts.") with a proper heading and a short descriptive sentence plus a quick
run instruction: e.g., add a top-level heading like "HTML layout with dynamic
navbar" followed by one sentence describing the example (what it demonstrates:
custom HTML layout converter with dynamic navbar) and another short sentence on
how to run or view the example (e.g., commands to start the example or open
index.html). Update the README in the examples/html-layout-default-slots example
to reflect this heading-and-run format.

Comment thread examples/html-layout-default-slots/README.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@holm/typing.py`:
- Around line 56-60: The docstring for the LayoutFactory TypeAlias has a typo
"asynccallable"; update the text around LayoutFactory in typing.py to read
"async callable" (or "asynchronous callable") so the sentence becomes: "A layout
factory is a sync or async callable that expects a single argument (the layout's
properties) and returns the properties for its wrapper layout." Ensure the
corrected wording appears in the triple-quoted docstring immediately above the
LayoutFactory definition.

Comment thread holm/typing.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/workflows/build-docs.yml:
- Around line 20-33: Update the GitHub Actions workflow step that uses
actions/checkout@v5 to the current major release (v6); locate the "Checkout
repository" step that references actions/checkout@v5 and change the version tag
to actions/checkout@v6 so the workflow uses the latest checkout action.
- Around line 36-38: Update the GitHub Actions step that runs the package sync
so CI uses the lockfile: modify the "Install dependencies" step's command that
currently runs "uv sync" to include the --frozen flag (i.e., run "uv sync
--frozen") so installs are reproducible; ensure this change is made in the
workflow step where the name is "Install dependencies".

In `@docs/api/typing.md`:
- Around line 1-4: Remove the leading '#' so the mkdocstrings directive ':::
holm.typing' is not treated as a heading; instead create a normal heading line
(e.g., "## Typing" or similar) above it, put the '::: holm.typing' directive on
its own line, and keep the indented options block (options: / show_root_heading:
true) directly under that directive so mkdocstrings can process it.

In `@holm/typing.py`:
- Around line 56-62: Typo in the LayoutFactory docstring: change "asynccallable"
to "async callable" in the string that documents LayoutFactory (the TypeAlias
and its surrounding docstring) so the phrase reads "A layout factory is a sync
or async callable..."; locate the docstring that references LayoutFactory and
update the wording accordingly.

Comment thread .github/workflows/build-docs.yml
Comment thread .github/workflows/build-docs.yml
Comment thread docs/api/typing.md
Comment thread holm/typing.py
@coderabbitai

This comment was marked as off-topic.

@volfpeter
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Around line 9-11: Replace the bold-as-heading lines "**NEVER push code**" and
"**Never install anything**" in AGENTS.md with proper Markdown headings or list
items to satisfy MD036; for example change them to a top-level or subheading
(e.g., "## NEVER push code" and "## Never install anything") or convert them
into list bullets ("- NEVER push code", "- Never install anything") so they are
not bold-only lines acting as headings.

In `@ai/doc-guide.md`:
- Around line 68-72: The document contains unhyphenated compound adjectives
(e.g., the phrase shown around "Use **bold** for:" and the example "file-system
based routing") that should be hyphenated for consistency; update occurrences
like "file system based routing" to "file-system-based routing" and any similar
compound modifiers elsewhere (the other instances noted near the same sections)
so compound adjectives are consistently hyphenated throughout the doc.

In `@docs/application-components.md`:
- Around line 20-28: The documentation must note that HTML layouts are processed
with Python str.format() via default_text_processor, so literal braces in CSS/JS
must be escaped as double braces; update docs/application-components.md to
explicitly state that `{` and `}` must be written as `{{` and `}}` when using
the HTML layout pipeline (mention the html_to_layout() function in
holm/modules/_layout.py and default_text_processor so readers can find the
implementation), and add a short example or sentence showing a CSS rule or JS
object literal converted to doubled braces.

In `@docs/guides/html-layout-default-slots.md`:
- Line 3: The sentence is missing a space before the second link; update the
paragraph containing "[HTML layout guide](html-layout.md) and the[HTML
multi-slot layout guide](html-multi-slot-layout.md)" to insert a space so it
reads "...and the [HTML multi-slot layout guide](html-multi-slot-layout.md)"
(edit the line in docs/guides/html-layout-default-slots.md where that phrase
appears).

In `@docs/guides/html-layout.md`:
- Line 18: Update the fenced code block that currently uses only triple
backticks (` ``` `) to include a language identifier to satisfy markdownlint
MD040; specifically replace the bare opening fence with a language-specified
fence (e.g., change the opening "```" to "```text") so the code block is marked
as text.
- Around line 77-81: The docs fail to mention that layout HTML is processed as
Python format strings, so update the guide to call out that literal braces in
CSS/JS must be escaped; explicitly note that layouts created via html_to_layout
(in holm/modules/_layout.py) use default_text_processor and therefore any `{` or
`}` in the HTML/CSS/JS should be doubled as `{{` and `}}` to avoid format-string
interpolation errors.

In `@docs/guides/html-multi-slot-layout.md`:
- Line 10: Fix the typo in the example link: change both the link text and the
URL from "html-mutli-slot-layout" to "html-multi-slot-layout" in the
docs/guides/html-multi-slot-layout.md content where the sentence references the
examples/html-mutli-slot-layout directory so the displayed text and the target
URL match the actual repository directory name.

Comment thread AGENTS.md
Comment thread ai/doc-guide.md
Comment thread docs/application-components.md
Comment thread docs/guides/html-layout-default-slots.md Outdated
Comment thread docs/guides/html-layout.md
Comment thread docs/guides/html-layout.md
Comment thread docs/guides/html-multi-slot-layout.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/guides/html-layout-default-slots.md`:
- Around line 85-103: Update the import to use the public typing export: replace
the private-module import of Layout from holm.modules._layout with the public
type from holm.typing; in the example where str_to_layout_with_navbar calls
snippet_to_layout (and the App is constructed with
str_to_layout=str_to_layout_with_navbar), change the import statement so it
reads from holm.typing import Layout.
🧹 Nitpick comments (1)
docs/guides/html-multi-slot-layout.md (1)

46-83: Consider adding a note about escaping literal braces in HTML layouts.

Since HTML layouts are processed as Python format strings, users who include inline CSS rules (e.g., style { color: red; }) or JavaScript object literals will encounter errors. Literal braces must be escaped as {{ and }}.

Adding a brief note or tip in this section would help users avoid this common pitfall.

Based on learnings: "Literal braces in the HTML (e.g., CSS {} or JavaScript object literals) must be escaped as {{ and }}. This format-string behavior should be documented for users."

Comment thread docs/guides/html-layout-default-slots.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@docs/guides/html-layout-default-slots.md`:
- Around line 18-19: The fenced code block showing the directory tree in
docs/guides/html-layout-default-slots.md is missing a language identifier which
triggers markdownlint MD040; update that block by adding the language identifier
`text` after the opening triple backticks so the directory structure is fenced
as ```text ... ``` to satisfy the lint rule and render correctly.
- Around line 111-139: The HTML layout examples omit a warning that layouts are
processed with Python's format-string rules (via
default_text_processor/str.format in snippet_to_layout), so add a short note in
the "Create `my_app/layout.html`" section explaining that literal braces in
CSS/JS must be escaped as `{{` and `}}` (or the format-string escaping
mechanism) to avoid formatting errors; reference default_text_processor and
snippet_to_layout in the note so readers know why escaping is required and where
processing happens.

Comment thread docs/guides/html-layout-default-slots.md
Comment thread docs/guides/html-layout-default-slots.md
@volfpeter volfpeter merged commit 2a5c0fd into main Feb 3, 2026
3 checks passed
@volfpeter volfpeter deleted the feat/html-layouts branch February 3, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support plain HTML layouts

1 participant