diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md
index 48550ed..df75fc1 100644
--- a/docs/01-getting-started.md
+++ b/docs/01-getting-started.md
@@ -1,32 +1,141 @@
# Getting Started
-This guide gets the solution running locally and explains what happens during startup.
+This guide walks you through everything you need to get the project running locally — from installing tools to understanding why certain architectural decisions were made.
+
+---
## What is in this solution
-- `Htmx.ApiDemo`: ASP.NET Core app (Minimal API + generated HTMX endpoints)
-- `Htmx.SourceGenerator`: Roslyn source generator that discovers `.htmx` files and generates endpoint mapping code
-- `Htmx.slnx`: solution file at the repository root
+| Project | Purpose |
+|---|---|
+| `Htmx.ApiDemo` | ASP.NET Core web app using Minimal APIs and server-rendered HTMX templates |
+| `Htmx.SourceGenerator` | Roslyn source generator that reads `.htmx` template files and generates endpoint mapping code at build time |
+
+The solution file is `Htmx.slnx` at the repository root.
+
+---
## Prerequisites
-- .NET SDK 10.x (target framework is `net10.0`)
-- Node.js + npm (used for Tailwind CSS compilation during build)
-- MongoDB running locally on `mongodb://localhost:27017`
+Install the following before cloning the repo.
+
+### .NET SDK
+
+The project targets `net10.0`. Download the .NET 10 SDK from [dot.net](https://dotnet.microsoft.com/download).
+
+Verify with:
+
+```bash
+dotnet --version
+```
+
+### Node.js and npm
+
+Tailwind CSS is compiled during the build using the Tailwind CLI via `npx`. Node.js must be installed.
+
+Download from [nodejs.org](https://nodejs.org). Verify with:
+
+```bash
+node -v
+npm -v
+```
+
+### MongoDB
+
+The app stores data in MongoDB. You need a local instance running on `mongodb://localhost:27017`.
+
+**Windows:**
+
+Download and install [MongoDB Community Server](https://www.mongodb.com/try/download/community). During installation, choose to run MongoDB as a Windows Service so it starts automatically.
+
+**Linux:**
+
+Follow the official guide for your distro at [docs.mongodb.com/manual/administration/install-on-linux](https://www.mongodb.com/docs/manual/administration/install-on-linux/). For Ubuntu/Debian:
+
+```bash
+sudo systemctl start mongod
+sudo systemctl enable mongod # start on boot
+```
+
+**MongoDB Compass (optional but recommended):**
+
+Compass is a GUI for browsing and querying your MongoDB data. Download it from [mongodb.com/products/compass](https://www.mongodb.com/products/compass). Connect it to `mongodb://localhost:27017` to inspect collections while developing.
+
+---
+
+## VS Code setup
+
+### Required extensions
+
+- **C# Dev Kit** — provides IntelliSense, debugging, and project support for .NET
+ Search for `ms-dotnettools.csdevkit` in the Extensions panel.
+
+- **C# (OmniSharp / Roslyn)** — included with C# Dev Kit but can also be installed standalone as `ms-dotnettools.csharp`.
+
+### Recommended settings
+
+Add the following to your workspace or user `settings.json`. This teaches VS Code to treat `.htmx` files as HTML (for syntax highlighting and formatting) and nests generated companion files under their parent in the Explorer sidebar so the file tree stays clean.
+
+```jsonc
+{
+ "files.associations": {
+ "*.htmx": "html"
+ },
+ "explorer.fileNesting.enabled": true,
+ "explorer.fileNesting.expand": false,
+ "explorer.fileNesting.patterns": {
+ "*.razor": "$(capture).razor.cs, $(capture).razor.css, $(capture).razor.js",
+ "*.htmx": "${capture}.htmx.cs, ${capture}.htmx.routing.cs, ${capture}.g.cs, ${capture}.css"
+ }
+}
+```
+
+Without `files.associations`, `.htmx` files open as plain text with no highlighting. Without file nesting, every generated `.htmx.cs` and `.htmx.routing.cs` file appears as a separate top-level entry in the Explorer, making it hard to navigate.
+
+### Optional extension
+
+- **Tailwind CSS Fold** — collapses long Tailwind class strings in the editor so markup is easier to read. Search for `stivo.tailwind-fold`. This is purely a cosmetic convenience and has no effect on the build.
+
+---
+
+## Understanding AOT
+
+AOT (Ahead-of-Time compilation) means the app is compiled to native machine code before it runs, rather than relying on the .NET JIT at runtime. This project has AOT enabled (`true` in the `.csproj`).
+
+### Why AOT matters here
+
+AOT produces smaller, faster deployments with no JIT warmup time. For a web app handling many requests, startup time and binary size are real concerns — especially in containerized or serverless environments.
+
+### What AOT prevents you from doing
+
+AOT is a significant constraint. It eliminates entire categories of patterns that are common in standard .NET development:
+
+- **No Entity Framework Core** — EF Core relies heavily on runtime reflection and expression compilation. It is not AOT-compatible. This project uses the MongoDB driver directly instead.
+
+- **No runtime reflection** — `Type.GetProperties()`, `Activator.CreateInstance()`, dynamic proxies, and similar patterns do not work (or produce warnings/errors) under AOT. If a pattern depends on inspecting types at runtime, it will not survive.
+
+- **Many NuGet packages are incompatible** — Any package that uses reflection internally (serializers, mappers, validators, ORMs, DI containers with convention scanning, etc.) may break. Check a package's AOT compatibility before adding it.
+
+- **Source generator-based serialization required** — Rather than `JsonSerializer.Serialize(myObject)` discovering properties at runtime, you must register types with a `JsonSerializerContext` subclass (see `AppJsonSerializerContext.cs`). The serializer then uses generated code instead of reflection.
+
+- **Route handler code generation** — ASP.NET Core's Minimal API generator produces code for request binding and response writing. Some third-party packages produce code that conflicts with this. If adding a package causes build errors in generated files, AOT incompatibility is the likely cause.
+
+The practical rule: before reaching for a package or pattern you know from standard ASP.NET Core, check whether it is AOT-compatible. The project will compile normally in Debug mode even with AOT-incompatible code — AOT issues typically only surface during `dotnet publish`.
+
+---
## First-time setup
-From the repository root:
+Clone the repo, then install the npm dependencies that the build needs:
```bash
cd Htmx.ApiDemo
npm install
```
-Why this is required:
+This installs the Tailwind CSS CLI package. The build runs `npx @tailwindcss/cli` as an MSBuild step, so if this is skipped the build will fail with a missing command error.
-- The app build runs Tailwind via `npx @tailwindcss/cli ...` in a custom MSBuild target.
-- Without `npm install`, build fails because the Tailwind CLI package is missing.
+---
## Run the app
@@ -36,57 +145,62 @@ From the repository root:
dotnet run --project Htmx.ApiDemo/Htmx.ApiDemo.csproj
```
-Default local URL:
+The app listens on `http://localhost:5120` by default (configured in `Htmx.ApiDemo/Properties/launchSettings.json`).
-- `http://localhost:5120`
-
-This comes from the launch profile in `Htmx.ApiDemo/Properties/launchSettings.json`.
+---
## Verify it works
-1. Open `http://localhost:5120`
-2. If you are not authenticated, middleware redirects to `/login`
-3. Create an account at `/register`
-4. Sign in and navigate the app
+1. Open `http://localhost:5120` in your browser.
+2. If you are not signed in, the middleware redirects you to `/login` — this is expected.
+3. Go to `/register` and create an account.
+4. Sign in and explore the app.
-## What startup config does
+---
-`Htmx.ApiDemo/Program.cs` configures:
+## What happens at startup
-- MongoDB DI and index initialization (`EnsureIndexesAsync`)
-- Cookie authentication + authorization
+`Program.cs` wires up the following in order:
+
+- MongoDB service registration and index initialization (`EnsureIndexesAsync`)
+- Cookie-based authentication and authorization
- Antiforgery middleware
-- AOT-friendly JSON resolver chain using `AppJsonSerializerContext`
-- Endpoint registration via generated mapping call:
- - `app.MapHtmxApiDemoEndpoints();`
+- AOT-compatible JSON serialization via `AppJsonSerializerContext`
+- All generated HTMX endpoints via `app.MapHtmxApiDemoEndpoints()`
-## Build behavior worth knowing
+---
-- Tailwind CSS is compiled before build into `Htmx.ApiDemo/wwwroot/css/output.css`
-- `.htmx` files are treated as generator inputs (``)
-- AOT is enabled (`true`), so reflection-heavy patterns can break publish/runtime
+## Build details
-## Optional: publish as AOT
+- **Tailwind** is compiled into `wwwroot/css/output.css` as a pre-build step.
+- **`.htmx` files** are passed to the source generator as ``. The generator reads them and produces the abstract base classes and routing code.
+- **AOT** is active on publish. Run a publish build early and often to catch incompatibilities before they accumulate.
+
+### Publish (AOT)
```bash
dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj -c Release
```
-Use this early to catch AOT issues while developing features.
+---
## Troubleshooting
-### Build fails on Tailwind command
+### Build fails — Tailwind command not found
-- Run `npm install` inside `Htmx.ApiDemo`
-- Confirm `node -v` and `npm -v` are available
+Run `npm install` inside the `Htmx.ApiDemo` directory. Confirm `node` and `npm` are on your `PATH`.
-### Mongo connection errors
+### MongoDB connection errors at startup
-- Confirm MongoDB is running on `localhost:27017`
-- Confirm `ConnectionStrings:DefaultConnection` in `Htmx.ApiDemo/appsettings.json`
+- Confirm the MongoDB service is running (`mongod`).
+- Check that `ConnectionStrings:DefaultConnection` in `appsettings.json` points to `mongodb://localhost:27017`.
-### App keeps redirecting to login
+### App always redirects to `/login`
-- This is expected for unauthenticated routes
-- Register at `/register` or sign in at `/login`
+This is intentional. Unauthenticated requests are redirected by the auth middleware. Register at `/register` first.
+
+### AOT warnings or errors on publish
+
+- Look at the warning message — it usually names the type or method causing the issue.
+- Remove or replace the offending package or pattern with an AOT-compatible alternative.
+- Run `dotnet publish` regularly during development so issues do not pile up.
diff --git a/docs/02-creating-a-page.md b/docs/02-creating-a-page.md
index 29f1ea4..b93879b 100644
--- a/docs/02-creating-a-page.md
+++ b/docs/02-creating-a-page.md
@@ -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
-
-
-
$$Title$$
-
$$Body$$
-
-```
-
-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 for zero-allocation output
- private static ReadOnlySpan _part0 => new byte[] { ... };
- private static ReadOnlySpan _part1 => new byte[] { ... };
- private static ReadOnlySpan _part2 => new byte[] { ... };
-
- public void Render(HtmxRenderContext context)
- {
- context.Writer.WriteUtf8(_part0); //
- }
-}
-```
-
-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`:
```
-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 `