Skip to content

Commit 177ef05

Browse files
authored
Merge pull request #56 from volfpeter/jinja-layouts
Jinja layouts
2 parents 0720fca + d0aa69b commit 177ef05

107 files changed

Lines changed: 1094 additions & 620 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
9+
[*.py]
10+
indent_style = space
11+
indent_size = 4
12+
13+
[*.jinja]
14+
indent_style = space
15+
indent_size = 2

.ignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
!dev
1+
!dev/

AGENTS.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
Unless explicitly requested:
44

5-
- **Do NOT** commit
5+
- **Do NOT** stage, commit, or push code
66
- **Do NOT** write tests
77
- **Do NOT** run code or applications
88

9-
**NEVER push code**
10-
119
**Never install anything**
1210

13-
**ASK** questions if something is unclear or ambiguous
11+
**ASK** questions if something is unclear or ambiguous, make no assumptions
1412

1513
Always aim for **simplicity**, **clarity**, be challenging to achieve this
1614

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Web development framework that brings the Next.js developer experience to Python
1717
- **Standard FastAPI** everywhere, so you can leverage the entire FastAPI ecosystem.
1818
- **JSX-like syntax** with async support for components, thanks to `htmy`.
1919
- First class **HTMX support** with `FastHX`.
20+
- Built-in **Jinja support** with zero configuration.
2021
- **Async** support everywhere, from APIs and dependencies all the way to UI components.
2122
- Support for both **JSON** and **HTML** (server side rendering) APIs.
2223
- **No JavaScript** dependencies
@@ -72,7 +73,7 @@ Consider supporting the development and maintenance of the project through [spon
7273
Similarly to Next.js, `holm` is built around the concept of [file-system based routing](https://volfpeter.github.io/holm/file-system-based-routing). This essentially means that your code structure is automatically mapped to a matching API:
7374

7475
- You do not need to manually define routes, every [application component](https://volfpeter.github.io/holm/application-components) is automatically discovered and registered in the application.
75-
- You do not need to manually wrap pages in their layouts, it is automatically done based on your application's code structure.
76+
- You do not need to manually wrap pages in their layouts; this is automatically done based on your application's code structure.
7677

7778
You can find all the necessary details on the [Application components](https://volfpeter.github.io/holm/application-components) page, and the [Quick start guide](https://volfpeter.github.io/holm/guides/quick-start-guide) can walk you through the process of creating your first application. The two are complementary documents, reading both is strongly recommended.
7879

@@ -115,7 +116,9 @@ When building web applications, performance should be compared to an application
115116

116117
### Templating language support
117118

118-
While certain features in `holm` rely heavily on the capabilities of `htmy` (for example its context and async support), you can still use other DSLs or templating languages (for example Jinja) in your application if you would like to. All you need to do is write a simple wrapper `htmy` component that internally offloads rendering to your framework of choice. You can find out more about this in the [htmy documentation](https://volfpeter.github.io/htmy/#compatibility-and-performance).
119+
`holm` ships with built-in **Jinja** support: `layout.jinja` files work out of the box as layouts, and `holm.JinjaTemplate` components render with zero configuration when `holm` owns the `htmy` renderer. See the [Jinja layout guide](https://volfpeter.github.io/holm/guides/jinja-layout) for details.
120+
121+
Since `htmy` is the rendering engine, you can also use other DSLs or templating languages by writing a simple wrapper `htmy` component that offloads rendering to your framework of choice. You can find out more about this in the [htmy documentation](https://volfpeter.github.io/htmy/#compatibility-and-performance).
119122

120123
## Development
121124

@@ -153,7 +156,7 @@ The most prominent frameworks in this category are Reflex and NiceGUI. They both
153156

154157
This category includes frameworks like FastHTML or Ludic, and this is where `holm` belongs as well, but it has some key differentiators.
155158

156-
First, `holm` brings the Next.js developer experience to Python with file-system based routing, automatic layout composition, and dynamic page metadata creation, and more. Thanks to `htmy`, it supports async code throughout the stack, even in components, and it also solves the prop drilling problem. While being built with `htmy`, it is easy to integrate with other templating libraries, like `Jinja` or `htpy`. And it provides all these features using standard, simple FastAPI patterns.
159+
First, `holm` brings the Next.js developer experience to Python with file-system based routing, automatic layout composition, and dynamic page metadata creation, and more. Thanks to `htmy`, it supports async code throughout the stack, even in components, and it also solves the prop drilling problem. While being built with `htmy`, it ships with built-in **`Jinja`** support and remains easy to integrate with other templating libraries, like `htpy`. And it provides all these features using standard, simple FastAPI patterns.
157160

158161
## License
159162

docs/api/holm.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@
2727
show_root_heading: true
2828
filters:
2929
- "!component"
30+
31+
::: holm.JinjaTemplate
32+
options:
33+
show_root_heading: true

docs/api/utils.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/application-components.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ This section summarizes the different components of `holm` applications. If you
66

77
*Layouts* are defined in the `layout.py` modules of packages as a callable `layout` variable (note that for example classes with an `__init__()` method, or `htmy.Component`s are callable, so they also qualify).
88

9-
As an alternative to Python-based layouts, you can also create `layout.html` files in packages, from which a Python layout equivalent is automatically generated.
9+
As an alternative to Python-based layouts, you can also create `layout.jinja` files in packages. These are standard Jinja2 templates, with the wrapped page (or any other child components) passed in as pre-rendered named slots. See the [Jinja layout guide](guides/jinja-layout.md) for a complete walkthrough.
1010

11-
Note: If both `layout.py` and `layout.html` exist in the same package, the Python layout takes precedence. HTML layouts at the application root level require a Python package structure.
11+
Note: If both `layout.py` and `layout.jinja` exist in the same package, the Python layout takes precedence.
1212

1313
Rules for *Python layouts*:
1414

@@ -17,14 +17,15 @@ Rules for *Python layouts*:
1717
- `layout` can have additional arguments (position or keyword and keyword-only, but not positional-only). These arguments must be FastAPI dependencies. They will be automatically resolved during each request.
1818
- Returning a tuple or a list from a layout is **not allowed** unless the value is a `htmy.ComponentSequence`. Tuples and lists are always interpreted and treated as component sequences, so you don't need to track what kinds of components pages and layouts return. See `htmy.is_component_sequence()` for more information.
1919

20-
Rules for *HTML layouts*:
20+
Rules for *Jinja layouts*:
2121

22-
- Must follow Python `str.format()` syntax.
23-
- [Page metadata](#page-metadata) is accessible as `metadata`.
24-
- The current FastAPI request is accessible as `request`.
25-
- Use the `<!-- slot[name] -->` slot syntax for content injection (processed by `htmy.Snippet` and `htmy.Slots` components).
26-
- The default slot is `children` (`<!-- slot[children] -->`), which is filled with the return value of the wrapped layout or page.
27-
- The entire HTML layout processing pipeline can be customized using the `str_to_layout` argument of `App()`, allowing you to plug in tools like Jinja2 for example.
22+
- Must be valid Jinja2 templates.
23+
- [Page metadata](#page-metadata) is accessible as `metadata`, e.g. `{{ metadata.title }}`.
24+
- The current FastAPI request is accessible as `request`, e.g. `{{ request.url.path }}`.
25+
- Route parameters (resolved FastAPI dependencies) are accessible as `route_params`, e.g. `{{ route_params.current_user }}`.
26+
- Use the `{{ slots.name }}` syntax for content injection. The default slot is `children` (`{{ slots.children }}`), which is filled with the return value of the wrapped layout or page.
27+
- Return a mapping from a page or layout to provide content for multiple named slots.
28+
- App-wide default slots can be configured with the `layout_slots` argument of `App()`, e.g. `App(layout_slots={"navbar": navbar})`.
2829

2930
By default, layouts automatically wrap all layouts and pages in subpackages. You can opt out of this behavior by wrapping the return value of a layout or page with the [`without_layout` utility](utilities.md#without_layout).
3031

docs/file-system-based-routing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Going through the [Application components](application-components.md) documentat
1212
- Underscores (`_`) are replaced with hyphens (`-`) in paths by default, unless a path segment corresponds to a path parameter, in which case the underscore is preserved.
1313
- Special files mark application components: `holm` looks for specific filenames within your application directory to discover application components and compose your application.
1414
- `page.py`: Creates a publicly accessible URL for a route segment.
15-
- `layout.py`: Defines a shared UI that wraps a route segment and its children. `layout.html` is also supported as an HTML-based alternative (requires package structure at root level), see [Application components](application-components.md).
15+
- `layout.py`: Defines a shared UI that wraps a route segment and its children. `layout.jinja` is also supported as a Jinja2-based alternative, see [Application components](application-components.md).
1616
- `actions.py`: Defines custom action endpoints for a route segment.
1717
- `api.py`: Creates custom API endpoints for a route segment.
1818

@@ -93,7 +93,7 @@ Besides the special files (`page.py`, `layout.py`, `api.py`, etc.), you can plac
9393

9494
In the example, `my_app/navbar.py` is not a special file, so it is not routable. It can define any components or utilities, like a `Navbar`, which can then be imported and used in the layouts, pages, or APIs of the application.
9595

96-
`holm` also doesn't consider anything outside the application package as an application component. We could create a `my_components/` directory next to `my_app/` and have anything in it, including `page.py` files for example. The content of this package would be ignored by `holm`, it is not within the application package.
96+
`holm` also doesn't consider anything outside the application package as an application component. We could create a `my_components/` directory next to `my_app/` and have anything in it, including `page.py` files for example. The content of this package would be ignored by `holm`, as it is not within the application package.
9797

9898
### Submit handlers
9999

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,55 @@
1-
# HTML layout with default slots
1+
# Jinja layout default slots
22

3-
This guide builds on the [HTML layout guide](html-layout.md) and the [HTML multi-slot layout guide](html-multi-slot-layout.md) to demonstrate how to use **default slots** for shared components that appear on every page.
3+
This guide builds on the [Jinja layout guide](jinja-layout.md) and the [Jinja multi-slot layout guide](jinja-multi-slot-layout.md) to demonstrate how to use **default slots** for shared components that appear on every page.
44

55
We will cover:
66

7-
- How to create a custom `str_to_layout` converter that provides default slots.
8-
- How to build a reusable navigation component that highlights the current page.
7+
- How to configure default slots with `App(layout_slots=...)`.
8+
- How to render default slots in a Jinja layout.
99

10-
The entire source code of this application can be found in the [examples/html-layout-default-slots](https://github.com/volfpeter/holm/tree/main/examples/html-layout-default-slots) directory of the repository.
10+
The entire source code of this application can be found in the [examples/jinja-layout-default-slots](https://github.com/volfpeter/holm/tree/main/examples/jinja-layout-default-slots) directory of the repository.
1111

1212
Before you continue, make sure you have installed `holm` and either `uvicorn` or `fastapi-cli`!
1313

1414
## File structure
1515

16-
The application uses a Python package structure (required for HTML layouts):
16+
The application uses a Python package structure:
1717

1818
```
19-
html-layout-default-slots/ # Root directory
20-
└── my_app/ # Application package
21-
├── __init__.py # Makes this a Python package (required for HTML layouts)
22-
├── main.py # Application entry point with custom str_to_layout
23-
├── layout.html # Root HTML layout with navbar and children slots
24-
├── navbar.py # Shared navigation component
25-
├── page.py # Home page
19+
jinja-layout-default-slots/ # Root directory
20+
└── my_app/ # Application package
21+
├── __init__.py
22+
├── layout.jinja # Root Jinja layout with navbar and children slots
23+
├── main.py # Application entry point
24+
├── navbar.py # Default navbar component
25+
├── page.py # Home page
2626
└── about/
2727
├── __init__.py
28-
└── page.py # About page
28+
└── page.py # About page
2929
```
3030

31-
## Create the navigation component
31+
## Create the application with a default slot
32+
33+
Default slots are configured when you create the `holm` application:
34+
35+
```python hl_lines="8"
36+
from holm import App
37+
38+
from .navbar import navbar
39+
40+
# `layout_slots` makes the `navbar` slot available to every component
41+
# in the application, including Jinja layouts, through
42+
# `htmy.jinja.DefaultSlots.from_context()`.
43+
app = App(layout_slots={"navbar": navbar})
44+
```
45+
46+
`layout_slots` takes a mapping from slot name to `htmy` component. These slots are injected into the `htmy` rendering context using `htmy.jinja.DefaultSlots` and merged with any explicit slots returned by the wrapped page or layout. When there is a slot name conflict, the explicit slot provided by the wrapped page or layout takes precedence.
47+
48+
## Create the default navbar component
3249

3350
First we create `my_app/navbar.py` with a navigation component that highlights the current page:
3451

35-
```python hl_lines="1 10 28-29 33-38"
52+
```python hl_lines="1 18 28"
3653
from fasthx.htmy import CurrentRequest
3754
from htmy import ComponentType, Context, component, html
3855

@@ -73,48 +90,20 @@ def navbar(context: Context) -> ComponentType:
7390
)
7491
```
7592

76-
These are the most important details you should notice:
93+
The most important details:
7794

7895
- `CurrentRequest.from_context()` is used to get the current FastAPI request from the `htmy` rendering context.
7996
- The `nav_item` helper applies highlighting when the link matches the current path.
8097

81-
## Create the custom layout converter
82-
83-
Create `my_app/main.py` with a custom `str_to_layout` converter that provides the navbar as a default slot:
98+
## Create the Jinja layout with slots
8499

85-
```python hl_lines="2-3 5 8-14 18"
86-
from holm import App
87-
from holm.typing import Layout
88-
from holm.utils import snippet_to_layout
100+
Create `my_app/layout.jinja` with slots for both the navbar and page content:
89101

90-
from .navbar import navbar
91-
92-
93-
def str_to_layout_with_navbar(content: str) -> Layout:
94-
"""
95-
Custom string to `Layout` converter function for the application.
96-
97-
It makes a `navbar` component available to every HTML layout in the `navbar` slot.
98-
"""
99-
return snippet_to_layout(content, default_slot_mapping={"navbar": navbar})
100-
101-
102-
app = App(str_to_layout=str_to_layout_with_navbar)
103-
```
104-
105-
The important bit here is the `str_to_layout_with_navbar()` function (which wraps `snippet_to_layout`), and how we pass it to `App()`.
106-
107-
The `default_slot_mapping` argument specifies components that will be automatically provided for slots unless the page explicitly overrides them. In this case it means HTML layouts will always have access to our `navbar` without pages having to return it themselves.
108-
109-
## Create the HTML layout with slots
110-
111-
Create `my_app/layout.html` with slots for both the navbar and page content:
112-
113-
```html hl_lines="14-15 18-19"
102+
```jinja hl_lines="4 14-15 18-19"
114103
<!doctype html>
115104
<html>
116105
<head>
117-
<title>{metadata[title]}</title>
106+
<title>{{ metadata.title }}</title>
118107
<meta charset="utf-8" />
119108
<meta name="viewport" content="width=device-width, initial-scale=1" />
120109
<link
@@ -124,12 +113,12 @@ Create `my_app/layout.html` with slots for both the navbar and page content:
124113
</head>
125114
<body class="container-fluid">
126115
<header class="container">
127-
<!-- The navbar component added by the layout converter goes to the navbar slot: -->
128-
<!-- slot[navbar] -->
116+
<!-- The navbar component provided by `layout_slots` -->
117+
{{ slots.navbar }}
129118
</header>
130119
<main class="container">
131-
<!-- Page content goes here, to the usual children slot: -->
132-
<!-- slot[children] -->
120+
<!-- Page content goes here, to the usual children slot -->
121+
{{ slots.children }}
133122
</main>
134123
<footer class="container">
135124
<p>© 2026 My App</p>
@@ -138,16 +127,16 @@ Create `my_app/layout.html` with slots for both the navbar and page content:
138127
</html>
139128
```
140129

141-
Our layout has two slots:
130+
The layout has two slots:
142131

143-
- `<!-- slot[navbar] -->` receives the navbar component from the default slot mapping.
144-
- `<!-- slot[children] -->` receives the page's main content.
132+
- `{{ slots.navbar }}` receives the navbar component from `layout_slots`.
133+
- `{{ slots.children }}` receives the page's main content.
145134

146135
## Create your home page
147136

148-
We can now create our home page in `my_app/page.py`, which is essentially the same as in the HTML layout guide:
137+
We can now create our home page in `my_app/page.py`, which is essentially the same as in the Jinja layout guide:
149138

150-
```python hl_lines="16"
139+
```python
151140
from htmy import Component, html
152141

153142
metadata = {"title": "Home | My App"}
@@ -181,7 +170,7 @@ async def metadata(featured: bool = False) -> dict[str, str]:
181170
"""
182171
Dynamic metadata based on query parameters.
183172
184-
This function could be both sync or async. It's just a standard FastAPI dependency.
173+
This function could be either sync or async. It's just a standard FastAPI dependency.
185174
"""
186175
title = "Featured About" if featured else "About"
187176
return {"title": f"{title} | My App"}
@@ -227,3 +216,10 @@ You can now open the application in the browser:
227216
- `http://localhost:8000/about?featured=true`: Featured about page variant
228217

229218
You'll see the navigation bar on every page with the current page highlighted.
219+
220+
## Next steps
221+
222+
Now that you understand default slots:
223+
224+
- Review the [Jinja layout guide](jinja-layout.md) for the basics of Jinja layouts, custom `htmy` setups, and nested layouts.
225+
- Learn how to use multiple layout slots for more complex page structures in the [Jinja multi-slot layout guide](jinja-multi-slot-layout.md).

0 commit comments

Comments
 (0)