Chatixy.Umbraco 1.0.1

dotnet add package Chatixy.Umbraco --version 1.0.1
                    
NuGet\Install-Package Chatixy.Umbraco -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Chatixy.Umbraco" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Chatixy.Umbraco" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Chatixy.Umbraco" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Chatixy.Umbraco --version 1.0.1
                    
#r "nuget: Chatixy.Umbraco, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Chatixy.Umbraco@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Chatixy.Umbraco&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Chatixy.Umbraco&version=1.0.1
                    
Install as a Cake Tool

Chatixy AI Support Agent - Umbraco package

A NuGet package (Chatixy.Umbraco) that adds the Chatixy AI support agent to every front-end page of an Umbraco site. Add the package, put your widget key in appsettings.json, redeploy - that is the whole install. No Program.cs edit, no template change, no RenderController subclass.

Targets net8.0, which maps to:

Umbraco TFM Works with this package
13 (LTS) net8.0 yes
14 net8.0 yes
15 / 16 net9.0+ yes - a net9.0 app consumes a net8.0 library

The NuGet dependency is Umbraco.Cms.Web.Website [13.0.0,17.0.0). Do not "modernise" the TFM to net9.0: that would silently drop Umbraco 13 LTS, which is where most production sites are.

How it works

Same job as every Chatixy integration: bind a widget_key to the site and inject

<script src="https://chatixy.com/source/<64-hex-key>.js?platform=umbraco" async></script>

Mechanism: IComposer → UmbracoPipelineFilter → response-rewriting middleware

  • src/Chatixy.Umbraco/ChatixyComposer.cs - an IComposer. Umbraco auto-discovers composers in every referenced assembly at boot, which is what makes dotnet add package the complete installation. In Compose() it binds the options and configures UmbracoPipelineOptions with an UmbracoPipelineFilter. Umbraco's own docs are explicit that this is the supported route for a package: "this doesn't require any changes in your Program.cs file. It also allows packages to enable CORS and configure their own policies."
  • Stage: PostPipeline. It runs after Umbraco's own middleware but still inside the middleware phase - i.e. before UseEndpoints - so it wraps endpoint execution and can buffer the rendered HTML. A filter on Endpoints could not (endpoints are terminal), and PrePipeline would buffer static files and back-office traffic too.
  • src/Chatixy.Umbraco/ChatixyInjectionMiddleware.cs - swaps Response.Body for a MemoryStream, runs the rest of the pipeline, then splices the loader in before the final </body>.
  • src/Chatixy.Umbraco.Core/ChatixyKey.cs - the hardened helper (key sanitisation, host pinning, URL building), identical in contract to the WordPress/Drupal/Moodle/Craft/… helpers.
  • src/Chatixy.Umbraco.Core/ChatixyLoader.cs - the HTML splice, as a pure string → string function so the interesting properties are unit-testable with no HTTP pipeline.

Response-rewriting caveats (read these)

Rewriting response bodies generically is how you break a site. Every gate below is a hard pass-through, not a best effort:

  1. Content type. Only text/html is touched. JSON, XML sitemaps, RSS, PDFs, images, CSS/JS bundles - anything else is copied through byte-for-byte. The header is parsed with MediaTypeHeaderValue, so text/html; charset=utf-8 matches and application/xhtml+xml (deliberately not rewritten) does not.
  2. Back office. Every request under the Umbraco back-office path is skipped before any buffering happens - the back office is a SPA we have no business injecting a support agent into, and its /umbraco/api/… and management-API endpoints must not be buffered. The path is read from GlobalSettings.UmbracoPath, so a renamed back office is still skipped, with the literal /umbraco also always skipped as a fallback.
  3. Compression. A response carrying a Content-Encoding other than identity is passed through untouched - appending plain text to a gzip/brotli body would produce an unreadable page. In the normal ASP.NET Core layout this guard never fires, because UseResponseCompression() is registered in Program.cs before UseUmbraco() and therefore sits outside this middleware: it compresses the HTML we already injected into. Reverse-proxy compression (IIS dynamic compression, gzip on in nginx, Cloudflare) happens after Kestrel entirely and is likewise unaffected. UNVERIFIED: that ordering claim is reasoned from ASP.NET Core middleware semantics, not observed on a live Umbraco site. If a host does register compression downstream of Umbraco, the guard degrades to "widget silently absent" - the safe direction, never a corrupt page.
  4. No </body> → unchanged. Fragments and partial views are returned byte-identically. The splice also uses the last </body> (case-insensitive) so an escaped example earlier in the document is not mistaken for the real closing tag.
  5. No double injection. If the document already contains /source/<key>.js - because the snippet was pasted into a template by hand, or a second copy of the middleware ran - nothing is added. Running the splice twice yields exactly one tag.
  6. Cheap when idle. With no key configured (or Enabled: false), the middleware returns before it ever swaps the response body, so an installed but unconfigured package costs one string check per request.

The loader origin is pinned

ChatixyKey.SanitizeHost() ends in the same choke point every Chatixy integration uses:

return IsAllowedHost(origin) ? origin : DefaultHost;

The origin has to be https on chatixy.com or a subdomain of it; anything else becomes https://chatixy.com. The host feeds a first-party <script src> on every page, so a host an attacker managed to store would be site-wide stored XSS. The regex is anchored at both ends and only grows the host to the left of a literal dot, so evilchatixy.com (no dot boundary) and chatixy.com.evil.example (canonical domain as a prefix) are both rejected; parsing goes through System.Uri, which is what defeats the https://chatixy.com@evil.example userinfo trick (Host == "evil.example").

There is no host setting - not in ChatixyOptions, not anywhere. We do not sell self-hosted Chatixy, so a settable host would only ever be attack surface.

Install

dotnet add package Chatixy.Umbraco

Then add the Chatixy section to appsettings.json (exact section name):

{
  "Chatixy": {
    "Enabled": true,
    "WidgetKey": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  }
}
  • WidgetKey accepts whatever the Chatixy dashboard hands you: the bare 64-char hex key, <key>.js, or the whole <script> snippet. Anything without a 64-hex run means "not configured" and injects nothing.
  • Enabled defaults to true, so setting only WidgetKey is enough.
  • Being plain IConfiguration, the section can equally come from an environment variable (Chatixy__WidgetKey), user secrets, or Azure App Configuration.
  • Binding uses IOptionsMonitor, so an appsettings.json edit is picked up without an app restart.

The real blocker: this is a developer install

Be honest about the distribution story. Unlike the WordPress or Shopify integrations, a non-technical site owner cannot install this. It requires someone who can add a NuGet package to the solution, edit appsettings.json, and redeploy the site. Umbraco has no runtime package installer for NuGet packages - that is a property of the platform, not something this package can fix. For agency-built Umbraco sites that is fine (the agency does it once); for a self-serve funnel it is a hard gate.

Tests

ChatixyKey and ChatixyLoader live in src/Chatixy.Umbraco.Core/, which has zero Umbraco and zero ASP.NET Core dependencies, purely so the test project can reference them without restoring the whole Umbraco.Cms dependency graph. The same .cs files are <Compile Include>'d by Chatixy.Umbraco.csproj, so the shipped package contains that exact source compiled into Chatixy.Umbraco.dll - there is no copy to drift and no second assembly in the nupkg. Chatixy.Umbraco.Core.csproj is IsPackable=false and is never published.

.NET is not installed on the Mac, so run the suite in a container from the repo root:

docker run --rm -v "$PWD:/repo" -w /repo/integrations/umbraco \
  mcr.microsoft.com/dotnet/sdk:8.0 dotnet test
# → Passed!  - Failed: 0, Passed: 80, Skipped: 0, Total: 80

dotnet test on the solution also restores Umbraco.Cms.Web.Website, which is a large graph and needs working nuget.org access. To run only the helper/splice tests (seconds, tiny graph), name the test project:

docker run --rm -v "$PWD:/repo" -w /repo/integrations/umbraco \
  mcr.microsoft.com/dotnet/sdk:8.0 \
  dotnet test tests/Chatixy.Umbraco.Tests/Chatixy.Umbraco.Tests.csproj

Coverage (80 tests):

  • Key extraction from all three paste shapes (bare key, <key>.js, whole <script> snippet), plus case-folding/trimming and rejection of 63-hex, 65-hex, non-hex, empty and null input.
  • EmbedSrc with and without ?platform=, including URL-encoding of the platform and trailing-slash stripping; VerifyUrl.
  • The full host matrix - https://chatixy.com and subdomains pass; plain http, evilchatixy.com, chatixy.com.evil.example, https://chatixy.com@evil.example, ftp://, javascript:, protocol-relative and garbage all fall back to DefaultHost. Plus a property-style test that SanitizeHost output is always allow-listed, whatever goes in.
  • The HTML splice - injects exactly once immediately before </body>; no </body> leaves the document unchanged; an invalid key injects nothing; running it twice does not double-inject; a page that already carries the loader is left alone; the last </body> is used, not an escaped earlier one; case-insensitive </BODY>; JSON left alone.

Mutation check

Replacing the final line of SanitizeHost() with an unconditional return origin; turns 11 of the 80 red (the http/lookalike/userinfo/scheme rows of both SanitizeHost theories plus the property test); restoring the pin returns the suite to 80/80 green. Both numbers were observed, and no mutant was left in the tree.

Not yet installed on a real Umbraco site

Verified here:

  • Chatixy.Umbraco.csproj restores against real Umbraco.Cms.Web.Website from nuget.org and compiles clean (dotnet build -c Release, 0 warnings, 0 errors) - so IComposer, IUmbracoBuilder, UmbracoPipelineOptions, UmbracoPipelineFilter, GlobalSettings.UmbracoPath and the middleware's ASP.NET Core surface are all real APIs with the signatures used here.
  • The 80 helper/splice tests pass, and the host pin is mutation-checked.

Not verified, and worth checking first on a live install:

  • That the composer is actually discovered and the middleware actually runs. Composer auto-discovery and UmbracoPipelineFilter are documented Umbraco conventions, but nothing here has been booted.
  • That PostPipeline really sits outside endpoint execution on the branch you deploy, so buffering catches the rendered page. If it does not, the symptom is "no widget", not a broken site.
  • The compression ordering assumption in caveat 3 above.
  • That GlobalSettings.UmbracoPath is the right source for a renamed back office on your Umbraco major (the literal /umbraco skip is unconditional either way, so the default install is covered regardless).
  • Behaviour under Umbraco preview, and on any endpoint that streams a text/html response - buffering means the full body is held in memory before the first byte reaches the client.
  • That Response.Body swapping (rather than replacing IHttpResponseBodyFeature) is sufficient for every renderer on the site. It is the simple, widely-used form; a component that writes through IHttpResponseBodyFeature.Writer directly would bypass it, and the symptom would again be "no widget".

Listing (owner-gated)

The Umbraco Marketplace is one of the few genuinely open and automated stores: it auto-indexes NuGet, so there is no submission form to fill in and nothing to upload.

Per Umbraco's Marketplace docs:

  1. The package must carry the NuGet tag umbraco-marketplace. This is load-bearing, not decoration - without it the package is never listed, no matter what else is set. It is already in Chatixy.Umbraco.csproj's <PackageTags>.
  2. The package must depend on an Umbraco.Cms.* package (or UmbracoCms.* for Umbraco 8, or Umbraco.Commerce.*). The Marketplace derives the supported version range from that dependency's version spec - which is why Umbraco.Cms.Web.Website is referenced as [13.0.0,17.0.0) rather than pinned.
  3. New packages are scanned every 24 hours at 0400 UTC; metadata is refreshed every 2 hours; download counts hourly. So a fresh publish shows up within a day, and later metadata edits within a couple of hours.
  4. Fees / approval: the documentation describes no fee and no approval or review step. Note the phrasing - the docs are silent on fees, which is not the same as a documented statement that listing is free. Do not promise "free listing" externally on the strength of that silence.

umbraco-marketplace.json goes on the PROJECT URL, not in the package

Verified: the Marketplace looks for umbraco-marketplace.json at the root of whatever the NuGet project URL points at - not inside the .nupkg.

  • Project URL https://chatixy.com → fetched from https://chatixy.com/umbraco-marketplace.json.
  • If the project URL is a GitHub repo → the root of that repo's default branch.
  • Several packages sharing one domain → suffix the file with the package id, e.g. umbraco-marketplace-chatixy.umbraco.json.

umbraco-marketplace.json in this folder is therefore a source of truth to be published, not a build artifact. Before the first release, either point <PackageProjectUrl> at the public GitHub repo and put this file at its root, or serve it from https://chatixy.com/umbraco-marketplace.json. Right now <PackageProjectUrl> is https://chatixy.com, so the file must be served from chatixy.com or the listing gets NuGet metadata only.

As of 2026-08-13 it is not served: https://chatixy.com/umbraco-marketplace.json answers 200 with content-type: text/html, which is the SPA fallback handing back index.html, not the file. Check the content type, never the status code - a 200 proves nothing here. Serving it is a website change plus a deploy, so it is owner-gated and it is not required for the listing: the umbraco-marketplace tag and the Umbraco.Cms.* dependency are what get the package indexed, and this file only expands an entry that already exists.

The schema is PascalCase, and it is closed

The file is validated against umbraco-marketplace-schema.json, which declares "additionalProperties": false and names every property in PascalCase. Until 2026-08-13 this file was written entirely in camelCase (packageType, category, authorDetails, description, pricingModel, openSource, tags), so every field in it was invalid or ignored - the listing would have fallen back to bare NuGet metadata with no sign that anything had gone wrong. Category is also a closed enum that never contained the "Backoffice Extensions" value this file used; it is now "Artificial Intelligence". There is no supportUrl, sourceCodeUrl, bugsUrl, pricingModel or openSource property at all - the equivalents are IssueTrackerUrl and LicenseTypes, and the source repository is read from the NuGet package's RepositoryUrl.

tests/Chatixy.Umbraco.Tests/PackageMetadataTests.cs now guards all of that offline, along with the umbraco-marketplace tag, the explicit <Version>, and the icon/README pack paths.

The old sourceCodeUrl/bugsUrl pointed at github.com/chatixy/chatixy-umbraco, which has never existed (nor has the chatixy GitHub org). Every URL in the file is now a live chatixy.com page.

Publishing (owner-gated)

cd integrations/umbraco
dotnet pack src/Chatixy.Umbraco/Chatixy.Umbraco.csproj -c Release -o dist -p:Version=1.0.0
dotnet nuget push dist/Chatixy.Umbraco.1.0.0.nupkg \
  --source https://api.nuget.org/v3/index.json --api-key "$NUGET_API_KEY"

Needs a nuget.org account owning the Chatixy.* prefix - that is the only gate, and it is an owner action.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 117 8/19/2026
1.0.0 102 8/19/2026