Rewrote all the docs - more noob friendly now.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-05 23:55:26 +05:00
parent e483bf73e7
commit f6ae86617c
35 changed files with 2159 additions and 2341 deletions
+136 -118
View File
@@ -1,58 +1,32 @@
# Creating a New Page
This guide explains the full lifecycle of adding a new page to the app: the template file, the code-behind class, the handler, and the sidebar link.
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.
## How pages work
---
Every page is a pair of files:
## What you want to achieve
| File | Purpose |
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` | HTML markup with `$$SlotName$$` slots |
| `Templates/MyPage.htmx.cs` | C# class that fills slots + declares the Minimal API handler |
| `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 Roslyn source generator (`Htmx.SourceGenerator`) reads every `.htmx` file at build time and generates an abstract base class for it. You then write a concrete class in the companion `.htmx.cs` file that inherits from that base.
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.
## How `$$SlotName$$` becomes code
---
Take this simple template:
```html
<!-- Templates/MyPage.htmx -->
<div class="p-6">
<h1>$$Title$$</h1>
<p>$$Body$$</p>
</div>
```
The generator splits the file on `$$...$$` patterns and produces:
```csharp
// auto-generated — do NOT edit
public abstract partial class MyPageBase : IHtmxComponent
{
protected abstract void RenderTitle(HtmxRenderContext context);
protected abstract void RenderBody(HtmxRenderContext context);
// static HTML segments stored as ReadOnlySpan<byte> for zero-allocation output
private static ReadOnlySpan<byte> _part0 => new byte[] { ... };
private static ReadOnlySpan<byte> _part1 => new byte[] { ... };
private static ReadOnlySpan<byte> _part2 => new byte[] { ... };
public void Render(HtmxRenderContext context)
{
context.Writer.WriteUtf8(_part0); // <div class="p-6"><h1>
RenderTitle(context.Next());
context.Writer.WriteUtf8(_part1); // </h1><p>
RenderBody(context.Next());
context.Writer.WriteUtf8(_part2); // </p></div>
}
}
```
Your job is to write the concrete class that implements each `RenderXxx` method.
## Step 1 — Create the `.htmx` template
## Step 1 — Write the template
Create `Htmx.ApiDemo/Templates/MyPage.htmx`:
@@ -63,119 +37,163 @@ Create `Htmx.ApiDemo/Templates/MyPage.htmx`:
</div>
```
Rules:
- Slot names are **PascalCase** and surrounded by `$$` — e.g. `$$MySlot$$`
- A slot can hold plain text, HTML, or another rendered component
- The file must be saved in `Templates/` (or a subfolder) so the `.csproj` `AdditionalFiles` glob picks it up
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
## Step 2 — Create the `.htmx.cs` code-behind
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`:
```csharp
using Immediate.Apis.Shared;
using Immediate.Handlers.Shared;
namespace Htmx.ApiDemo.Templates;
// Concrete template — inherits from the generated base
public sealed class MyPage : MyPageBase
{
private byte[] _headingData = [];
private byte[] _descriptionData = [];
private readonly byte[] _headingData;
private readonly byte[] _descriptionData;
// Use `init`-only setters to pre-encode strings to UTF-8 bytes once
public required string Heading { init => _headingData = value.ToUtf8Bytes(); }
public required string Description { init => _descriptionData = value.ToUtf8Bytes(); }
protected override void RenderHeading(HtmxRenderContext context)
=> context.Writer.WriteUtf8(_headingData);
protected override void RenderDescription(HtmxRenderContext context)
=> context.Writer.WriteUtf8(_descriptionData);
}
// Minimal API handler — discovered and registered by the source generator
[Handler]
[MapGet("/my-page")]
public static partial class GetMyPageHandler
{
public record Query; // add route/query parameters here if needed
private static ValueTask HandleAsync(
Query query,
IHttpContextAccessor httpContextAccessor,
CancellationToken token)
public MyPage(string heading, string description)
{
var ctx = httpContextAccessor.HttpContext
?? throw new InvalidOperationException("HttpContext is not available.");
// 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();
}
var page = new MyPage
{
Heading = "My New Page",
Description = "This is a minimal example page."
};
protected override void RenderHeading(HtmxRenderContext ctx)
=> ctx.Writer.WriteUtf8(_headingData);
// WriteHtmxPage: full HTML shell for direct browser loads,
// bare fragment for HTMX partial swaps (HX-Request header present)
ctx.WriteHtmxPage(page, title: "My Page", appName: "HtmxApp", pageTitle: "My Page");
return ValueTask.CompletedTask;
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:
```csharp
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;
}
}
```
## Step 3 — Add a sidebar link (optional but typical)
Then register it in `Program.cs` alongside the other endpoint registrations:
Open `Templates/MainLayout.htmx` and add a nav entry inside the `<nav>` block. Existing entries look like this:
```csharp
MyPageEndpoints.Map(app);
```
---
## Step 4 — Add a sidebar link (optional but typical)
Open `Templates/MainLayout.htmx` and add a nav entry inside the `<nav>` block:
```html
<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">
<!-- inline SVG icon here -->
My Page
</a>
```
Key HTMX attributes:
- `hx-get` — makes the navigation a partial swap instead of a full page reload
- `hx-target="#main-view"` — replaces only the content area, keeping the sidebar in place
- `hx-push-url="true"` — updates the browser URL bar so deep-links still work
The three HTMX attributes do the heavy lifting:
## How `WriteHtmxPage` decides what to render
| 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:
```
Request has HX-Request header?
YES → render bare fragment + set HX-Title response header (browser tab title updates)
NO → wrap fragment in MainLayout (full HTML page with sidebar, navbar, etc.)
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
```
The logic lives in `HtmxPageExtensions.WriteHtmxPage`. You never need to fork on this yourself.
You never need to branch on this yourself. `WriteHtmxPage` handles it.
## Slots that hold components
---
A slot does not have to render plain text. If you need to embed a reusable component, assign the component instance and call `Render` from the override:
## 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:
```csharp
public IHtmxComponent MyCard { get; }
public MyPage(IHtmxComponent myCard)
public sealed class MyPage : MyPageBase
{
MyCard = myCard;
}
private readonly byte[] _headingData;
private readonly IHtmxComponent _statusBadge;
protected override void RenderMyCard(HtmxRenderContext context)
=> MyCard.Render(context);
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
}
```
See [03-creating-a-component.md](03-creating-a-component.md) for the full component pattern.
In the template:
```html
<div class="flex items-center gap-3">
<h1>$$Heading$$</h1>
$$StatusBadge$$
</div>
```
See [03-creating-a-component.md](03-creating-a-component.md) for how to build your own components.
---
## Checklist
- [ ] `MyPage.htmx` created in `Templates/`
- [ ] `MyPage.htmx.cs` created with a class inheriting `MyPageBase`
- [ ] Each `$$Slot$$` has a matching `RenderSlot` override
- [ ] `[Handler]` + `[MapGet(...)]` (or `MapPost` etc.) on the handler class
- [ ] `ctx.WriteHtmxPage(...)` called from `HandleAsync`
- [ ] Build once — if a slot is missing its override, the compiler will tell you
- [ ] `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