Files
Htmx/docs/02-creating-a-page.md
T
2026-05-05 23:55:26 +05:00

6.2 KiB

Creating a New Page

Think of a page as a form letter — the template is the letter with blanks left for personalisation, and your C# class is the person who fills those blanks in before the letter is sent. The build system generates all the plumbing between the two; you just write the template and the class.


What you want to achieve

By the end of this guide you will have a new page at a URL like /dashboard that:

  • Shows your own custom HTML
  • Loads instantly as a full page when you visit the URL directly
  • Swaps in as a smooth partial update when navigated to from the sidebar
  • Accepts data you pass to it from C#

The two files every page needs

File What it is
Templates/MyPage.htmx The letter template — HTML with $$Slot$$ blanks
Templates/MyPage.htmx.cs The person filling in the blanks — C# class + route handler

The build system (Htmx.SourceGenerator) reads your .htmx file and generates an abstract C# class with one RenderXxx() method per $$Slot$$. Your job is to inherit that class and implement each method.


Step 1 — Write the template

Create Htmx.ApiDemo/Templates/MyPage.htmx:

<div class="p-6 space-y-4">
  <h1 class="text-2xl font-bold text-foreground">$$Heading$$</h1>
  <p class="text-muted-foreground">$$Description$$</p>
</div>

Rules for slots:

  • Names are PascalCase surrounded by $$ — e.g. $$MySlot$$
  • A slot can contain plain text, HTML, or a rendered component
  • The file must live inside Templates/ so the build picks it up automatically

After saving this file and building, the generator emits MyPageBase — a class you will never edit but will inherit from.


Step 2 — Write the code-behind

Create Htmx.ApiDemo/Templates/MyPage.htmx.cs:

namespace Htmx.ApiDemo.Templates;

public sealed class MyPage : MyPageBase
{
    private readonly byte[] _headingData;
    private readonly byte[] _descriptionData;

    public MyPage(string heading, string description)
    {
        // Convert strings to UTF-8 bytes once in the constructor.
        // The Render methods then just write those bytes — no allocations at request time.
        _headingData     = heading.ToUtf8Bytes();
        _descriptionData = description.ToUtf8Bytes();
    }

    protected override void RenderHeading(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_headingData);

    protected override void RenderDescription(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_descriptionData);
}

The pattern here is deliberate: do all string work (formatting, encoding) in the constructor, so that Render is nothing but memory writes. This keeps request handling fast.


Step 3 — Write the route handler

Route handlers live in the same .htmx.cs file. They are plain static methods registered with Minimal API — no special framework, no base class, no attributes from removed packages:

namespace Htmx.ApiDemo.Templates;

public static class MyPageEndpoints
{
    public static void Map(IEndpointRouteBuilder app)
    {
        app.MapGet("/my-page", Handle);
    }

    private static IResult Handle(HttpContext ctx)
    {
        var page = new MyPage(
            heading:     "My New Page",
            description: "This is a minimal example."
        );

        ctx.WriteHtmxPage(page, title: "My Page");
        return Results.Empty;
    }
}

Then register it in Program.cs alongside the other endpoint registrations:

MyPageEndpoints.Map(app);

Open Templates/MainLayout.htmx and add a nav entry inside the <nav> block:

<a href="/my-page"
   hx-get="/my-page" hx-target="#main-view" hx-push-url="true"
   class="sidebar-link group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium
          text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
  My Page
</a>

The three HTMX attributes do the heavy lifting:

Attribute What it does
hx-get="/my-page" Fetches the page as a partial instead of a full reload
hx-target="#main-view" Drops the response into the content area, leaving the sidebar untouched
hx-push-url="true" Updates the browser URL bar so bookmarks and back-button still work

How the app knows whether to send a full page or just the fragment

When WriteHtmxPage is called it checks for the HX-Request header that HTMX sends on every HTMX-triggered request:

Direct browser visit (no HX-Request header)
  → full HTML: <html><head>...</head><body><sidebar/><main>YOUR PAGE</main></body></html>

HTMX sidebar click (HX-Request: true)
  → just your fragment: <div class="p-6 space-y-4">...</div>
  → plus an HX-Title header so the browser tab title still updates

You never need to branch on this yourself. WriteHtmxPage handles it.


Embedding a component inside a page

Slots are not limited to text. If you want to place a reusable component inside a slot, store it as a field and call Render from the override:

public sealed class MyPage : MyPageBase
{
    private readonly byte[] _headingData;
    private readonly IHtmxComponent _statusBadge;

    public MyPage(string heading, string status)
    {
        _headingData  = heading.ToUtf8Bytes();
        _statusBadge  = new Badge(status, variant: "secondary");
    }

    protected override void RenderHeading(HtmxRenderContext ctx)
        => ctx.Writer.WriteUtf8(_headingData);

    protected override void RenderStatusBadge(HtmxRenderContext ctx)
        => _statusBadge.Render(ctx.Next());  // ctx.Next() tracks nesting depth
}

In the template:

<div class="flex items-center gap-3">
  <h1>$$Heading$$</h1>
  $$StatusBadge$$
</div>

See 03-creating-a-component.md for how to build your own components.


Checklist

  • MyPage.htmx saved in Templates/ with $$PascalCase$$ slots
  • MyPage.htmx.cs has a class inheriting MyPageBase with all RenderXxx overrides
  • Route handler registered in Program.cs
  • Builds cleanly — the compiler will error if any slot override is missing
  • Sidebar link added to MainLayout.htmx if the page needs to be in the nav