Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Make the Tag Helpers and toolkit types available to Razor views in `_ViewImports

```html
@using Ramstack.HtmxToolkit
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Ramstack.HtmxToolkit
```

Expand All @@ -67,21 +68,27 @@ Render the configuration metadata in the document `<head>`:
</head>
```

Map the companion script endpoint in `Program.cs`:
On ASP.NET Core 9 or later, enable static assets and associate them with the endpoints that render views in `Program.cs`:

```csharp
app.MapHtmxToolkitScript();
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();
```

Load HTMX first, then the toolkit script in the layout:
For MVC, apply `.WithStaticAssets()` to the controller endpoint builder instead; a hybrid Razor Pages and MVC
application applies it to each endpoint set. On ASP.NET Core 6–8, enable static files:

```csharp
app.UseStaticFiles();
```

The NuGet package includes the toolkit script as a static web asset. Load it after HTMX in the layout:

```html
<script src="/path/to/htmx.min.js"></script>
<script src="@Html.HtmxToolkitScriptPath()"></script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js" asp-append-version="true"></script>
```

The default script URL contains a content hash, so the script can be cached indefinitely. When the script changes, its URL changes automatically.

You can now generate an HTMX URL from ASP.NET Core route information:

```html
Expand Down Expand Up @@ -444,20 +451,15 @@ builder.Services.AddHtmxToolkit(options =>
});
```

Instead of mapping an endpoint, the companion script can be embedded directly:
Use the readable script during development with:

```html
<script>
@Html.HtmxToolkitScript()
</script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.js" asp-append-version="true"></script>
```

Pass `debug: true` to `HtmxToolkitScript` or `HtmxToolkitScriptPath` to use the readable script during development.
A custom endpoint path is also supported:
Load HTMX before the toolkit; if deferring execution, apply `defer` to both scripts.

```csharp
app.MapHtmxToolkitScript("/assets/htmx-toolkit.js");
```
See [Antiforgery and Toolkit script](docs/articles/antiforgery.md) for static asset and caching details.

## Compatibility Notes

Expand Down Expand Up @@ -506,9 +508,7 @@ Idiomorph library before the first morph swap:

<script src="https://unpkg.com/htmx.org@2"></script>
<script src="https://unpkg.com/idiomorph@0.7.4"></script>
<script>
@Html.HtmxToolkitScript()
</script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js" asp-append-version="true"></script>
</body>
```

Expand Down
14 changes: 0 additions & 14 deletions docs/api-overwrites/tag-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,3 @@ example:
- |-
[!code-razor[](../snippets/tag-helpers/ConfigTagHelper.cshtml)]
---

---
uid: Ramstack.HtmxToolkit.HtmlHelperExtensions.HtmxToolkitScriptPath(Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper,System.Boolean)
example:
- |-
[!code-razor[](../snippets/tag-helpers/ToolkitScript.cshtml)]
---

---
uid: Ramstack.HtmxToolkit.Hosting.EndpointRouteBuilderExtensions.MapHtmxToolkitScript(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)
example:
- |-
[!code-csharp[](../snippets/tag-helpers/MapToolkitScriptEndpoint.cs)]
---
85 changes: 59 additions & 26 deletions docs/articles/antiforgery.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,45 @@ Otherwise, it is added to the request parameters under the configured form-field

## Configure the layout

Map the script endpoint in `Program.cs`:
Enable static files in `Program.cs`:

```csharp
using Ramstack.HtmxToolkit.Hosting;

var app = builder.Build();

app.MapHtmxToolkitScript();
app.UseStaticFiles();
app.MapRazorPages();
```

On ASP.NET Core 9 or later, use `MapStaticAssets()` and associate the asset collection with the page endpoints
instead to enable build-time compression and fingerprinted URLs:

```csharp
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();
```

For MVC, apply `.WithStaticAssets()` to the controller endpoint builder, for example:

```csharp
app.MapStaticAssets();
app.MapDefaultControllerRoute().WithStaticAssets();
```

A hybrid Razor Pages and MVC application applies it to each endpoint set that renders views:

```csharp
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();
app.MapControllers().WithStaticAssets();
```

`MapControllers()` covers attribute-routed controllers; with conventional or area routing, apply
`.WithStaticAssets()` to each `MapControllerRoute` or `MapAreaControllerRoute` call.

> [!NOTE]
> ASP.NET Core reads the asset collection from the current endpoint's metadata. An endpoint without it still
> renders a working URL: the resolver falls back to a `?v=...` version instead of a fingerprinted URL.

Render configuration metadata in `<head>`, then load HTMX before the Toolkit script:

```html
Expand All @@ -37,7 +65,7 @@ Render configuration metadata in `<head>`, then load HTMX before the Toolkit scr
@RenderBody()

<script src="~/js/htmx.min.js"></script>
<script src="@Html.HtmxToolkitScriptPath()"></script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js" asp-append-version="true"></script>
</body>
```

Expand Down Expand Up @@ -79,42 +107,47 @@ No token input is required in this form because the layout metadata and Toolkit
When a boosted navigation returns a new full document, the Toolkit script reads antiforgery metadata from that response
and updates the token used for later requests. Ensure the returned document contains `<htmx-config />`.

## Script endpoint and caching
## Static web assets and caching

The default endpoint path contains a content hash:
The NuGet package includes both script variants as ASP.NET Core static web assets:

```text
/htmxtoolkit/{content-hash}
/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js
/_content/Ramstack.HtmxToolkit/htmx-toolkit.js
```

It returns the minified script with `Cache-Control: public,max-age=31536000`. A new embedded script receives a new default URL.
Reference the minified file from the layout with an app-relative path:

Pass a custom path when routing conventions require one:

```csharp
app.MapHtmxToolkitScript("/assets/htmx-toolkit.js");
```html
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js"
asp-append-version="true"></script>
```

When using a stable custom path, account for cache invalidation in deployment or proxy configuration.
This form requires `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers` in `_ViewImports.cshtml`.
ASP.NET Core resolves the `~` path and applies content-based versioning:

Request the readable script while diagnosing browser behavior:
- On ASP.NET Core 9 or later, when the current endpoint's asset collection contains the script, the framework
selects the fingerprinted URL, such as `htmx-toolkit.min.{fingerprint}.js`. In production, `MapStaticAssets()`
serves fingerprinted assets with long-lived, immutable caching and supports precompressed Gzip and Brotli
representations.
- Otherwise, `asp-append-version="true"` appends a `?v=...` version computed and cached from the file content.
This includes ASP.NET Core 6–8 and applications using `UseStaticFiles()`.

```html
<script src="@Html.HtmxToolkitScriptPath(debug: true)"></script>
```
Both forms account for the application's path base. When the file changes, its versioned URL changes. Cache
headers are managed by the application's static asset or static file configuration; adding `?v=...` does not
itself set a cache lifetime.

The debug URL adds `?debug` and the endpoint returns the unminified asset.
Static web assets work with both project references and NuGet packages. On publish, ASP.NET Core copies
the scripts into the application's `wwwroot/_content/Ramstack.HtmxToolkit` directory.

## Inline the script

Applications that cannot map the endpoint can render the embedded asset inside a script element:
Request the readable script while diagnosing browser behavior:

```html
<script>@Html.HtmxToolkitScript()</script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.js"
asp-append-version="true"></script>
```

Inlining removes a request but changes the content security policy and repeats the script in every full document.
Prefer the cacheable endpoint for most applications.
If you use `defer`, apply it to both HTMX and the Toolkit script so their execution order is preserved.

## Disable automatic antiforgery

Expand All @@ -135,6 +168,6 @@ The Toolkit script can still be used for morph compatibility after antiforgery m
- Antiforgery protects cookie-authenticated state-changing requests; it does not replace authentication or authorization.
- A custom `hx-header-*` value is client-controlled and must not be trusted as proof of identity.
- Cross-origin permissions still require correct ASP.NET Core CORS and credential configuration.
- An inline Toolkit script may require a CSP nonce or hash. The endpoint form works naturally with a policy that allows scripts from the application's origin.
- The Toolkit static web asset works with a CSP policy that allows scripts from the application's origin.

If a protected request returns 400, see [Troubleshooting](troubleshooting.md#post-returns-http-400).
30 changes: 23 additions & 7 deletions docs/articles/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This example assumes HTMX 2.x, which is the toolkit default.

## 2. Register HtmxToolkit

Register Razor Pages and the toolkit in `Program.cs`, then map the companion script endpoint:
Register Razor Pages and the toolkit in `Program.cs`, and enable static files:

```csharp
using Ramstack.HtmxToolkit.Hosting;
Expand All @@ -27,7 +27,6 @@ builder.Services.AddHtmxToolkit();
var app = builder.Build();

app.UseStaticFiles();
app.MapHtmxToolkitScript();
app.MapRazorPages();

app.Run();
Expand All @@ -36,17 +35,32 @@ app.Run();
No configuration delegate is required for HTMX 2.x. To use another major version,
see [Choose an HTMX version](choosing-version.md).

The setup above works on ASP.NET Core 6 or later. On ASP.NET Core 9 or later, use the following instead of
`UseStaticFiles()` and `MapRazorPages()` to enable build-time compression and fingerprinted asset URLs:

```csharp
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();
```

> [!NOTE]
> In a hybrid Razor Pages and MVC application, call `.WithStaticAssets()` on every endpoint set that renders
> views, for example `app.MapControllers().WithStaticAssets()` as well.

## 3. Enable the Razor helpers

Add the namespace and Tag Helpers to `Pages/_ViewImports.cshtml`:

```html
@using Ramstack.HtmxToolkit
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Ramstack.HtmxToolkit
```

The `@using` directive makes `Html.HtmxToolkitScriptPath()` and toolkit types available.
The `@addTagHelper` directive enables attributes such as `hx-page`, `hx-route-*`, and the `<htmx-config />` element.
The `@using` directive makes toolkit types available.
The first `@addTagHelper` directive enables the standard ASP.NET Core Tag Helpers used by the layout snippet
(`~` path resolution and `asp-append-version`). The second enables attributes such as `hx-page` and `hx-route-*`,
as well as the `<htmx-config />` element.

## 4. Configure the layout

Expand All @@ -64,13 +78,15 @@ Render the configuration metadata in `<head>`. Load HTMX first and the Toolkit s
@RenderBody()

<script src="~/js/htmx.min.js"></script>
<script src="@Html.HtmxToolkitScriptPath()"></script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js" asp-append-version="true"></script>
</body>
</html>
```

The default Toolkit script URL includes a content hash and is served with a one-year cache lifetime.
When the embedded script changes, its default URL changes too.
The Toolkit script is a static web asset supplied by the NuGet package. ASP.NET Core resolves the `~` path, and
`asp-append-version="true"` makes the URL content-based: on ASP.NET Core 9 or later the framework selects a
fingerprinted URL when available, otherwise it appends a `?v=...` version. Both forms account for the application's
path base. Cache headers are managed by the application's static asset or static file configuration.

> [!IMPORTANT]
> `<htmx-config />` and the Toolkit script work together to add ASP.NET Core antiforgery data to unsafe HTMX requests. Omitting either one disables that automatic behavior.
Expand Down
2 changes: 1 addition & 1 deletion docs/articles/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The package targets .NET 6 and can be used by applications running on .NET 6 or
| Razor | Tag Helpers for routes, values, headers, and per-request options |
| Configuration | Version-specific HTMX configuration rendered by `<htmx-config />` |
| Security | Automatic ASP.NET Core antiforgery headers for unsafe HTMX requests |
| Assets | A small companion script with a cacheable endpoint or inline rendering |
| Assets | A companion static web asset referenced from the layout with automatic content-based URL versioning |

HtmxToolkit does not include the HTMX library and does not replace HTMX attributes such as `hx-target`, `hx-trigger`,
or `hx-swap`. Add a supported HTMX release to the application separately.
Expand Down
28 changes: 23 additions & 5 deletions docs/articles/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,41 @@ Common causes are missing or invalid antiforgery data.

1. Inspect the document for `<meta name="htmx-config">` and its `data-antiforgery-*` attributes.
2. Confirm HTMX loads before the Toolkit script.
3. Confirm the Toolkit script endpoint returns JavaScript rather than 404 or HTML.
3. Confirm the Toolkit script URL returns JavaScript rather than 404 or HTML.
4. Inspect the request for the antiforgery header or form value configured by ASP.NET Core.
5. After boosted navigation, confirm the returned full document also includes `<htmx-config />`.

See [Antiforgery and Toolkit script](antiforgery.md).

## Toolkit script returns 404

Map the endpoint before the application finishes endpoint registration:
Enable static files and reference the script from the layout:

```csharp
app.MapHtmxToolkitScript();
app.UseStaticFiles();
app.MapRazorPages();
```

Use `@Html.HtmxToolkitScriptPath()` instead of copying the default hash URL. If a custom path is passed
to `MapHtmxToolkitScript`, ensure the helper is rendered after that mapping is configured during application startup.
For ASP.NET Core 9 or later with `MapStaticAssets()`, configure the page endpoints with the asset collection:

```csharp
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();
```

```html
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js"
asp-append-version="true"></script>
```

The resolved URL points to `/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js` or its fingerprinted variant
under the application's path base. Ensure `_ViewImports.cshtml` registers the standard MVC Tag Helpers
(`@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`) and that deployment includes the published
static assets and manifests.

When running from build output in an environment other than `Development`, enable discovery of static web assets
with `builder.WebHost.UseStaticWebAssets()` before building the app. This extra call is unnecessary when running
from published output.

## Response has no HX headers

Expand Down
6 changes: 0 additions & 6 deletions docs/snippets/tag-helpers/MapToolkitScriptEndpoint.cs

This file was deleted.

3 changes: 0 additions & 3 deletions docs/snippets/tag-helpers/ToolkitScript.cshtml

This file was deleted.

6 changes: 3 additions & 3 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ function trim() {
}

export default {
input: "src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js",
input: "src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js",
treeshake: "smallest",
output: [{
file: "src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js",
file: "src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js",
}, {
file: "src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js",
file: "src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js",
plugins: [terser({
output: {
comments: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0"
integrity="sha384-BvJpBiO8Kh31EqtJe5DRIeWrHWnCGkwytKs9NKFi86Hhw96dEqdEMzZDeK9iEGTc"
crossorigin="anonymous"></script>
<script src="@Html.HtmxToolkitScriptPath()"></script>
<script src="~/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js" asp-append-version="true"></script>
<script src="~/js/demo.js"></script>
</body>
</html>
Loading
Loading