diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md new file mode 100644 index 0000000..48550ed --- /dev/null +++ b/docs/01-getting-started.md @@ -0,0 +1,92 @@ +# Getting Started + +This guide gets the solution running locally and explains what happens during startup. + +## 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 + +## 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` + +## First-time setup + +From the repository root: + +```bash +cd Htmx.ApiDemo +npm install +``` + +Why this is required: + +- 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 + +From the repository root: + +```bash +dotnet run --project Htmx.ApiDemo/Htmx.ApiDemo.csproj +``` + +Default local URL: + +- `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 + +## What startup config does + +`Htmx.ApiDemo/Program.cs` configures: + +- MongoDB DI and index initialization (`EnsureIndexesAsync`) +- Cookie authentication + authorization +- Antiforgery middleware +- AOT-friendly JSON resolver chain using `AppJsonSerializerContext` +- Endpoint registration via generated mapping call: + - `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 + +## Optional: publish as 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 + +- Run `npm install` inside `Htmx.ApiDemo` +- Confirm `node -v` and `npm -v` are available + +### Mongo connection errors + +- Confirm MongoDB is running on `localhost:27017` +- Confirm `ConnectionStrings:DefaultConnection` in `Htmx.ApiDemo/appsettings.json` + +### App keeps redirecting to login + +- This is expected for unauthenticated routes +- Register at `/register` or sign in at `/login` diff --git a/docs/02-creating-a-page.md b/docs/02-creating-a-page.md new file mode 100644 index 0000000..29f1ea4 --- /dev/null +++ b/docs/02-creating-a-page.md @@ -0,0 +1,181 @@ +# 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. + +## How pages work + +Every page is a pair of files: + +| File | Purpose | +|---|---| +| `Templates/MyPage.htmx` | HTML markup with `$$SlotName$$` slots | +| `Templates/MyPage.htmx.cs` | C# class that fills slots + declares the Minimal API 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. + +## 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); //

+ RenderTitle(context.Next()); + context.Writer.WriteUtf8(_part1); //

+ RenderBody(context.Next()); + context.Writer.WriteUtf8(_part2); //

+ } +} +``` + +Your job is to write the concrete class that implements each `RenderXxx` method. + +## Step 1 — Create the `.htmx` template + +Create `Htmx.ApiDemo/Templates/MyPage.htmx`: + +```html +
+

$$Heading$$

+

$$Description$$

+
+``` + +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 + +## Step 2 — Create the `.htmx.cs` 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 = []; + + // 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) + { + var ctx = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + var page = new MyPage + { + Heading = "My New Page", + Description = "This is a minimal example page." + }; + + // 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; + } +} +``` + +## Step 3 — Add a sidebar link (optional but typical) + +Open `Templates/MainLayout.htmx` and add a nav entry inside the `