htmy Documentation

repository·main·Indexed 19 days ago

https://github.com/volfpeter/htmy

An async, pure-Python server-side rendering engine for building HTML/XML documents using Python components instead of traditional templating languages. It features a React-like context to avoid prop-drilling, support for both sync and async function components via the @component decorator, and Async HTML streaming for improved TTFB. htmy includes built-in support for Markdown, Jinja templates, and internationalization, and is designed to work seamlessly with async frameworks like FastAPI.

Tokens
22.3K
Snippets
79
Records
99
Agent score
64%

What's inside htmy

  1. Overview of htmy features

    main

    htmy is an async, pure-Python server-side rendering engine designed to allow HTML generation using Python instead of a custom templating language.

    Key Capabilities

    • Async-first: Optimized for modern async tools and supports Async HTML streaming for improved Time to First Byte (TTFB).
    • React-like Context: Supports context to avoid prop-drilling.
    • Component Model: Supports both sync and async function components using decorator syntax.
    • HTML/XML Support: Includes all baseline HTML tags, supports native HTML/XML documents with dynamic formatting, and provides slot rendering without custom syntax.
    • Extensibility: Built-in support for Markdown, Jinja templates, and async JSON-based internationalization.
    • Developer Experience: Fully-typed, unopinionated (works with any CSS/JS framework), and features automatic property-name conversion (snake_case to kebab-case).
    • Error Handling: Includes a built-in ErrorBoundary component for graceful error management.
    • Compatibility: Works with Trio (via AnyIO) and can wrap other templating libraries.
  2. What is a component factory?

    main

    A component factory is a function that accepts arguments and returns a Component. Unlike full components, factories are evaluated immediately when called.

    Advantages:

    • Simplicity.
    • Better performance (the renderer receives a smaller, pre-resolved component tree).

    Limitations:

    • Cannot be async (they are called from sync code).
    • No access to the rendering Context.
    • Cannot act as context providers.
    • Immediately evaluated (can be undesirable for very large trees).

    Important Note on Sequences: If a factory returns a sequence of components (like a list), you must ensure it is correctly passed to the parent. For example, passing list[list[ComponentType]] to a parent is invalid; you must unpack the list or use a Fragment to ensure the parent receives a flat list[ComponentType].

    def heading(text: str) -> Component:
        return html.h1(text)
    
    def section(title: str, text: str) -> Component:
        # This is a factory that returns a component tree immediately
        return html.div(
            heading(title), 
            html.p(text)
        )
  3. What is an htmy component?

    main

    An htmy component (technically an HTMYComponentType) is any object that implements a sync or async htmy(context: Context) -> Component method.

    Additionally, the following types are treated as components:

    • str (Strings)
    • list or tuple containing HTMYComponentType or str objects.

    By implementing the htmy() method, you can turn existing business objects (like Pydantic models, ORM classes, or TypedDicts) into components without name collisions or compatibility issues.

    class MyBusinessObject:
        def __init__(self, data):
            self.data = data
    
        async def htmy(self, context: Context) -> Component:
            return html.div(f"Data: {self.data}")
  4. Use custom XML/HTML tags in Markdown

    main

    htmy allows you to include custom XML-like tags directly within your markdown files. These tags can be mapped to full htmy components via an etree.ETreeConverter.

    When a tag like <PostInfo author="John" /> is encountered in markdown, the converter looks for a mapping for PostInfo. If mapped to a class, the class's __init__ method must accept arguments that match the XML attributes.

    # markdown file (post.md):
    # <PostInfo author="John" published_at="1971-10-11" />
    
    # component definition:
    class PostInfo:
        def __init__(self, author: str, published_at: str) -> None:
            self.author = author
            self.published_at = published_at
    
        def htmy(self, context: Context) -> Component:
            return html.p("By ", html.strong(self.author), " at ", html.em(self.published_at), ".")
    
    # converter configuration:
    md_converter = etree.ETreeConverter({
        "PostInfo": PostInfo,
    })
  5. What are components in htmy?

    main

    In htmy, a component is any object that implements a sync or async htmy(context: Context) -> Component method. Technically, these are HTMYComponentType objects.

    Beyond objects with an htmy method, the following are also considered components:

    • str (strings)
    • list or tuple containing HTMYComponentType or str objects.

    This flexibility allows you to turn existing business objects (like Pydantic models or ORM classes) into components by simply adding an htmy method, or use simple functions decorated with @component.

  6. How function components and methods work together

    main

    In htmy, components can be composed in several ways:

    1. Nesting Components: A component can call another component (function or method) by passing its properties. The renderer will eventually resolve the context for all of them.
    2. Class/Instance Rendering: If a class implements htmy(self, context: Context), an instance of that class can be passed directly into other components as if it were a standard component.
    3. Method vs. Class Rendering: You can use a class instance as a component (via its htmy method) while simultaneously calling its specific decorated method components (like @component.method) to render different views of that same data.
  7. Prevent XSS attacks in htmy

    main

    htmy performs XML/HTML escaping by default, ensuring user input is sanitized and rendered safely.

    However, two specific components bypass this default behavior to allow raw HTML/XML input. You must manually ensure the input provided to these components is safe:

    1. Snippet: Used for efficient rendering of XML/HTML templates with dynamic placeholders.
    2. MD: Used for rendering Markdown. It converts Markdown to HTML and assumes the input text is safe.
  8. Use Context to share data without prop drilling

    main

    The Context (a Mapping) is managed by the renderer and allows you to share data with an entire component subtree.

    Context Providers

    Any component that implements a htmy_context() -> Context method acts as a provider. This method returns a dictionary of data that will be merged into the context for all child components.

    Context Consumers

    Components can access the context via the context: Context argument in their htmy method or function signature. You can retrieve specific data using the class or key as a lookup.

    from htmy import Component, Context, Renderer, component, html
    
    class UserContext:
        def __init__(self, *children, username: str, theme: str):
            self._children = children
            self.username = username
            self.theme = theme
    
        def htmy_context(self) -> Context:
            return {UserContext: self}
    
        def htmy(self, context: Context) -> Component:
            return self._children
    
        @classmethod
        def from_context(cls, context: Context) -> "UserContext":
            return context[cls]
    
    @component
    def welcome_page(text: str, context: Context) -> Component:
        user = UserContext.from_context(context)
        return html.html(html.body(html.h1(text, user.username), data_theme=user.theme))
    
    # Usage:
    # page = UserContext(welcome_page("Hello "), username="John", theme="dark")
    # await Renderer().render(page)
  9. How to call function and method components

    main

    When calling a decorated function or method component, you only pass the properties. You do not pass the context argument manually; the htmy renderer handles the context injection during the rendering process.

    • Function Component: my_component(props_value)
    • Context-only Component: my_component() (no arguments)
    • Method Component: instance.my_method(props_value)
    • Context-only Method: instance.my_method()
    # Function component with props
    user_list_item(user)
    
    # Context-only function component
    users_page()
    
    # Method component with props
    emily.profile_page(navbar_element)
    
    # Context-only method component
    emily.table_row()
  10. Install htmy

    main

    Install the core htmy package via pip:

    pip install htmy

    Optional Dependencies

    Depending on your needs, you can install optional dependency groups:

    • lxml: Recommended for more secure, faster, and flexible HTML/XML processing (e.g., for Markdown).
      pip install "htmy[lxml]"
    - **`jinja`**: Provides support for rendering Jinja templates via the `htmy.jinja` module.
      ```bash
    pip install "htmy[jinja]"
    • all: Installs all optional dependencies.
      pip install "htmy[all]"
  11. Use htmy with FastAPI

    main

    htmy is designed to work seamlessly with modern async Python frameworks like FastAPI. For specialized FastAPI integrations, consider the following ecosystem tools:

    • holm: A web development framework built on FastAPI, htmy, and FastHX that provides a Next.js-like developer experience.
    • FastHX: A declarative server-side rendering utility for FastAPI with built-in HTMX support.
  12. Set up Internationalization with I18n

    main

    To use internationalization in htmy, you need to follow two main steps:

    1. Create translation resources: Use plain JSON files organized by locale. For example, to support English, create a folder structure like locale/en/page/ and add a hello.json file containing your translation strings.
    2. Initialize and provide the I18n instance: Create an I18n instance pointing to your locale folder and include it in the rendering context using i18n.to_context() when calling the Renderer.

    Translation strings can use Python format syntax (e.g., {name}) which can be automatically populated via keyword arguments in the get method.

    from pathlib import Path
    from htmy.i18n import I18n
    
    # 1. Create translation resources (e.g., locale/en/page/hello.json)
    # Content: { "message": "Hey {name}" }
    
    # 2. Initialize I18n and provide it to the context
    base_folder = Path(__file__).parent
    i18n = I18n(base_folder / "locale" / "en")
    
    # When rendering:
    # rendered = await Renderer().render(Component(), i18n.to_context())