Chatixy.Umbraco
1.0.1
dotnet add package Chatixy.Umbraco --version 1.0.1
NuGet\Install-Package Chatixy.Umbraco -Version 1.0.1
<PackageReference Include="Chatixy.Umbraco" Version="1.0.1" />
<PackageVersion Include="Chatixy.Umbraco" Version="1.0.1" />
<PackageReference Include="Chatixy.Umbraco" />
paket add Chatixy.Umbraco --version 1.0.1
#r "nuget: Chatixy.Umbraco, 1.0.1"
#:package Chatixy.Umbraco@1.0.1
#addin nuget:?package=Chatixy.Umbraco&version=1.0.1
#tool nuget:?package=Chatixy.Umbraco&version=1.0.1
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- anIComposer. Umbraco auto-discovers composers in every referenced assembly at boot, which is what makesdotnet add packagethe complete installation. InCompose()it binds the options and configuresUmbracoPipelineOptionswith anUmbracoPipelineFilter. 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. beforeUseEndpoints- so it wraps endpoint execution and can buffer the rendered HTML. A filter onEndpointscould not (endpoints are terminal), andPrePipelinewould buffer static files and back-office traffic too. src/Chatixy.Umbraco/ChatixyInjectionMiddleware.cs- swapsResponse.Bodyfor aMemoryStream, 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 purestring → stringfunction 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:
- Content type. Only
text/htmlis touched. JSON, XML sitemaps, RSS, PDFs, images, CSS/JS bundles - anything else is copied through byte-for-byte. The header is parsed withMediaTypeHeaderValue, sotext/html; charset=utf-8matches andapplication/xhtml+xml(deliberately not rewritten) does not. - 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 fromGlobalSettings.UmbracoPath, so a renamed back office is still skipped, with the literal/umbracoalso always skipped as a fallback. - Compression. A response carrying a
Content-Encodingother thanidentityis 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, becauseUseResponseCompression()is registered inProgram.csbeforeUseUmbraco()and therefore sits outside this middleware: it compresses the HTML we already injected into. Reverse-proxy compression (IIS dynamic compression,gzip onin 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. - 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. - 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. - 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"
}
}
WidgetKeyaccepts 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.Enableddefaults totrue, so setting onlyWidgetKeyis 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 anappsettings.jsonedit 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 teston the solution also restoresUmbraco.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. EmbedSrcwith and without?platform=, including URL-encoding of the platform and trailing-slash stripping;VerifyUrl.- The full host matrix -
https://chatixy.comand subdomains pass; plainhttp,evilchatixy.com,chatixy.com.evil.example,https://chatixy.com@evil.example,ftp://,javascript:, protocol-relative and garbage all fall back toDefaultHost. Plus a property-style test thatSanitizeHostoutput 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.csprojrestores against realUmbraco.Cms.Web.Websitefrom nuget.org and compiles clean (dotnet build -c Release, 0 warnings, 0 errors) - soIComposer,IUmbracoBuilder,UmbracoPipelineOptions,UmbracoPipelineFilter,GlobalSettings.UmbracoPathand 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
UmbracoPipelineFilterare documented Umbraco conventions, but nothing here has been booted. - That
PostPipelinereally 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.UmbracoPathis the right source for a renamed back office on your Umbraco major (the literal/umbracoskip is unconditional either way, so the default install is covered regardless). - Behaviour under Umbraco preview, and on any endpoint that streams a
text/htmlresponse - buffering means the full body is held in memory before the first byte reaches the client. - That
Response.Bodyswapping (rather than replacingIHttpResponseBodyFeature) is sufficient for every renderer on the site. It is the simple, widely-used form; a component that writes throughIHttpResponseBodyFeature.Writerdirectly 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:
- 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 inChatixy.Umbraco.csproj's<PackageTags>. - The package must depend on an
Umbraco.Cms.*package (orUmbracoCms.*for Umbraco 8, orUmbraco.Commerce.*). The Marketplace derives the supported version range from that dependency's version spec - which is whyUmbraco.Cms.Web.Websiteis referenced as[13.0.0,17.0.0)rather than pinned. - 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.
- 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 fromhttps://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 | Versions 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. |
-
net8.0
- Umbraco.Cms.Web.Website (>= 13.0.0 && < 17.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.