DotSee.SchemaMapper
17.0.2
dotnet add package DotSee.SchemaMapper --version 17.0.2
NuGet\Install-Package DotSee.SchemaMapper -Version 17.0.2
<PackageReference Include="DotSee.SchemaMapper" Version="17.0.2" />
<PackageVersion Include="DotSee.SchemaMapper" Version="17.0.2" />
<PackageReference Include="DotSee.SchemaMapper" />
paket add DotSee.SchemaMapper --version 17.0.2
#r "nuget: DotSee.SchemaMapper, 17.0.2"
#:package DotSee.SchemaMapper@17.0.2
#addin nuget:?package=DotSee.SchemaMapper&version=17.0.2
#tool nuget:?package=DotSee.SchemaMapper&version=17.0.2
DotSee Schema Mapper
A backoffice dashboard for Umbraco CMS that lets developers configure schema.org JSON-LD structured data output per content type — without writing code.
What it does
- Map any Umbraco content type (or block element type, from Block List, Block Grid or Single Block) to one or more schema.org types (
Article,Event,Product,Organization, ...) through a visual dashboard, no C# required. - Supports nested objects, arrays of blocks, lists of child pages (e.g. a blog index) or of the pages that pick a page (e.g. an author's articles), cross-entity
@idreferences, site-wideWebSite/Organizationschemas, and an automaticBreadcrumbListfor every page built from the content tree. - Ships a Preview panel that generates real JSON-LD for a live page directly in the backoffice, including unsaved changes (nothing is saved by previewing).
- Adds a Structured data tab to every page (its published JSON-LD and a check against Google's required and recommended properties, read-only, for anyone who can open the page) and a Schema tab to document types (their mappings, with links into the dashboard). Nothing is added to your document types.
Requirements
- Umbraco CMS 17 (17.2.0 or later)
- .NET 10
The package depends on Schema.NET.Pending, which declares the same classes in the same Schema.NET namespace as the plain Schema.NET package. If your site references Schema.NET directly, use Schema.NET.Pending instead (it has every class Schema.NET has), or code that uses those classes won't compile: the type names would be ambiguous.
Installation
dotnet add package DotSee.SchemaMapper
The Schema Mapper appears automatically in the Settings tree, under Advanced → Schema Mapper, after installing and restarting your Umbraco site (on sites where a DotSee package adds the DotSee section, it appears there instead). No configuration is required — the plugin creates its own database table on first run, and by default the dashboard offers every document type (element types are never offered; one without a template is offered for use where its pages are picked).
To restrict which document types the dashboard offers as mapping targets (and which pages actually generate output), add an optional allow-list to appsettings.json:
"DotSee": {
"AllowedSchemaDocTypes": ["blogPost", "newsArticle"]
}
Omitting this section (or leaving it as an empty array) keeps the default: every document type. A configured list is used as-is, so it can also include document types without a template (for example ones rendered by a custom content finder).
The same DotSee section also takes SchemaOutputCacheMinutes, how long each page's output is cached (see Caching).
Sites with the earlier built-in Schema Mapper
DotSee packages released before 30 September 2026 bundled a Schema Mapper of their own. It doesn't conflict with this package, but it is a separate tool with its own mappings, so remove it:
- Update the package that bundled it to a release without it.
- Delete
App_Plugins/Dashboard-Schema-Mapperfrom the site. Updating doesn't remove it, because that package copies its App_Plugins only once. Until you do, Settings shows a second, older Schema Mapper Dashboard. - Remove
th_StructuredDatafrom your layout if it calls it, so pages don't output its JSON-LD next to this package's. - Drop the
SchemaMappingstable once you no longer need its mappings. This package uses its owndotseeSchemaMappingstable and never reads the old one.
Rendering the output on your site
Register the tag helper in Views/_ViewImports.cshtml:
@addTagHelper *, DotSee.SchemaMapper
Then add it to the <head> of your layout:
<ds:schema-jsonld />
It renders <script type="application/ld+json">…</script> for the current page, or nothing when there's no output for it. Any other attributes are passed through to the <script> tag, e.g. a CSP nonce: <ds:schema-jsonld nonce="@nonce" />. To render a different content item than the current page, pass it with content="@someContent".
Rendering it yourself
If you need full control, call ISchemaGeneratorService.GenerateSchemaJsonAsync directly:
@inject DotSee.SchemaMapper.Services.Generator.ISchemaGeneratorService SchemaGenerator
@{
var schemaJson = await SchemaGenerator.GenerateSchemaJsonAsync(Model);
}
@if (!string.IsNullOrEmpty(schemaJson))
{
<script type="application/ld+json">@Html.Raw(schemaJson)</script>
}
Use @Html.Raw — a plain @schemaJson HTML-encodes the quotes (") and produces invalid JSON-LD. This is safe: the generated JSON already escapes <, > and &, so content can't break out of the <script> tag. GenerateSchemaJsonAsync never throws — it returns null when there's nothing to render for that page.
Caching
Each page's output is cached for 10 minutes, per culture and site address, so crawlers requesting a page again and again don't rebuild it every time. Publishing, unpublishing, deleting or moving content, saving media, changing document types, data types, domains, languages or public access, and saving a mapping all clear the cache straight away, on every server of a load-balanced site. Saving a draft doesn't, since visitors don't see it. Previews (the dashboard's and the backoffice's) are never cached.
To change how long output is kept, or to turn the cache off with 0:
"DotSee": {
"SchemaOutputCacheMinutes": 10
}
The minutes are only a safety net for changes the clearing doesn't cover; the ones above take effect at once. Like the other settings, a change takes effect after a site restart.
When generating a page's output fails (e.g. the database is out of reach), the page gets no JSON-LD, and that is kept for one minute only, so a lasting error is logged once a minute per page rather than on every view. Output missing a part that failed (one mapping, or site code that threw) is kept for one minute too. The page itself always renders.
The cache is kept per site address, since the output's URLs are built from it. On a site with domains (Culture and Hostnames), only the host names they list are cached; a request for any other name gets its output built uncached. On a site without domains, every host name the server answers gets its own copy, so set ASP.NET Core's AllowedHosts on a live site.
Output limits
A list of child pages, or of pages that pick a page, outputs at most 50 pages unless its mapping sets Max items or No limit; without an Order those are the first 50 in the content tree, usually the oldest. Once a page's output holds 500 objects its lists take no more items, and picked pages are followed at most three pages deep. The dashboard's Preview says when any of these left something out, and the site's log notes it once.
How property values are output
| Property value | JSON-LD output |
|---|---|
| Text, numbers, booleans, tags | as-is |
| Content / media pickers, Multi URL Picker | absolute URL (a list for multiple items; query strings and anchors on picked links are kept) |
| Date pickers | ISO 8601 (2026-09-29T10:00:00+02:00, 2026-09-29; a fraction of a second only when there is one) |
| Rich text | plain text, markup stripped |
| Block List / Block Grid, elements, other complex objects | left out — a warning is logged once per type. Map one of their properties instead, via a path (blocks.elementAlias.property), or use an Object with its values read from the blocks |
Block List, Block Grid (including blocks in areas) and Single Block content is read alike.
What a mapping can't express can be added in code, into the same output (cached and previewed): a graph contributor, a value resolver for a custom property editor, or a computed value offered in the dashboard. See Completing a mapping in code below.
The dashboard and its API require access to the Settings section, or to the DotSee section on sites that have it.
Completing a mapping in code
When a page needs something the dashboard can't express, keep the mapping and add the missing part in code, rather than writing the page's whole structured data by hand. There are three small hooks. They run inside the generator, so what they produce is cached, shown in the Preview and the page's Structured data tab, and checked against Google's guidance like the rest.
| You want… | Use | Visible in the dashboard? |
|---|---|---|
| A kind of property (a property editor) to always convert a certain way, e.g. a map picker into GeoCoordinates | Value resolver (ISchemaValueResolver) |
No: it applies wherever a mapping reads such a property |
| A value that isn't a property, built from one or several, or from outside Umbraco, that editors can choose on specific lines, e.g. an embed URL from a video ID | Computed value (ISchemaComputedValue) |
Yes: listed with the built-in values (Page Name, Page URL…) |
| To add, complete or remove whole entities, e.g. an Offer with a live price on the mapped Product | Graph contributor (ISchemaGraphContributor) |
No: it works on the page's finished output |
All three are registered in a composer in the site's code, one line each. They are created per request, so their constructors can take any service: Umbraco's services, a commerce API, a cache.
using DotSee.SchemaMapper.Extensibility;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
public class SchemaComposer : IComposer
{
public void Compose(IUmbracoBuilder builder) => builder.Services
.AddSchemaValueResolver<MapPointResolver>()
.AddSchemaComputedValue<VimeoEmbedUrl>()
.AddSchemaGraphContributor<ProductOfferContributor>();
}
Value resolvers
The mapper converts Umbraco's own editors itself: text, pickers (to URLs), dates (ISO 8601), rich text (plain text), tags. A value from any other editor is left out, with a note in the Preview (“location” on “Hall” holds a MapPoint value, which can't be output…) and a log entry. A value resolver teaches the mapper how to convert one: wherever a mapping reads such a property (a Value line, or a value read through blocks), the resolver's result is output instead. Nothing changes in the dashboard; the property is mapped as usual.
public class MapPointResolver : ISchemaValueResolver
{
// Several resolvers can match a property: the highest priority is asked (default 0).
public int Priority => 0;
// Which properties this resolver converts: here, every property using the map editor.
public bool CanResolve(IPublishedPropertyType propertyType) =>
propertyType.EditorAlias == "Our.Umbraco.OpenStreetMap";
public object? Resolve(SchemaValueContext context) =>
context.Value is MapPoint point
? new JsonObject { ["@type"] = "GeoCoordinates", ["latitude"] = point.Lat, ["longitude"] = point.Lng }
: null;
}
Mapping geo → Location (the map property) then gives a GeoCoordinates object.
- What
Resolvereturns: a string, number, boolean or a list of them, which then goes through the usual conversion; aJsonObject(or a dictionary), output exactly as it is, so a resolver can return a whole object (GeoCoordinates, an Offer); ornullto leave the property out. Anything else (another object) is left out too, and the Preview says so: MapPointResolver returned a MapPoint value on “Hall”, which can't be output…. - What the context holds:
Value(the property's value as Umbraco converts it, in the culture being rendered),Property(the property itself),Element(the page, block or picked page being read),Page(the page being rendered) andCulture. - It can replace a built-in conversion too: a resolver always comes before the built-in conversion, so one matching
Umbraco.RichTextcan, for example, keep the links in FAQ answers instead of outputting plain text.Priorityonly decides between resolvers that match the same property. - Not for Values from: the pickers and blocks an Object reads its items from are read by the mapper itself; a resolver converts property values, not sources.
Computed values
A computed value is a value your code builds that isn't a property: from a property in another format (minutes as an ISO 8601 duration PT1H20M), from several properties ("title – site name"), or from outside Umbraco. It appears in every Value line's list next to Page Name, Page URL and Create Date, under its DisplayName; editors choose it on the lines that need it, and the mapping stores it as $code: plus its Name.
public class VimeoEmbedUrl : ISchemaComputedValue
{
public string Name => "vimeoEmbedUrl"; // stored as "$code:vimeoEmbedUrl"
public string DisplayName => "Vimeo embed URL"; // shown in the dashboard
public object? Compute(SchemaValueContext context) =>
context.Element.GetProperty("vimeoId")?.GetValue() is string id
? $"https://player.vimeo.com/video/{id}"
: null;
}
- What it is computed for: whatever the line reads from — the page, each block of a list, or a picked or child page. Use
context.Elementfor that, andcontext.Pagefor the page being rendered.PropertyandValueare empty for computed values. - What it returns: the same as a value resolver (a value, a
JsonObject, ornullto leave the property out). - Keep
Namestable: mappings store it. A mapping that uses a computed value no longer registered (removed, or missing on another environment) leaves the property out, and the Preview says No computed value named … is registered.
Graph contributors
A contributor works on the page's finished entities, after every mapping:
context.Nodesis the list of the page's entities (JsonObjects, in output order): change them in place, add, insert or remove;context.FindById("#product")finds an entity of this page, nested ones included (or pass a full @id);context.IdFor(page, "#offer")builds an @id the way the mappings do (the page's URL plus the fragment);context.Add(node)/context.Remove(node);context.Warn("…")adds a note to the Preview;context.Page,PageUrl,SiteUrl(what$siteUrlgives),Culture, andIsPreview(true in the dashboard's Preview).
// The price comes from the shop, not from the page: complete the mapped Product with an Offer.
public class ProductOfferContributor(IPriceService prices) : ISchemaGraphContributor
{
public int Order => 0; // contributors run in ascending order
public async Task ContributeAsync(SchemaGraphContext context)
{
if (context.FindById("#product") is not { } product) return;
var price = await prices.GetPriceAsync(context.Page.Key);
if (price is null) return;
product["offers"] = new JsonObject
{
["@type"] = "Offer",
["price"] = price.Amount,
["priceCurrency"] = price.Currency,
["availability"] = "https://schema.org/InStock",
["url"] = context.PageUrl,
};
}
}
Contributors run before references to missing entities are left out, so an entity a contributor adds can be the target of a mapped Reference. They also run on pages without any mapping, so a contributor can give a page its whole output.
When code fails, and caching
- A failure never breaks the page. A resolver, computed value or contributor that throws is logged and noted in the Preview; that value (or that contributor's changes) is left out, and the page still gets the rest of its structured data. The same holds for a mapping that fails (e.g. a property editor whose value can't be read): only that mapping's entity is left out.
- Caching. A page's output is cached for
DotSee:SchemaOutputCacheMinutes(10 by default). Publishing content, saving media or mappings clears it at once, but a change outside Umbraco (a new price in the shop) only shows once the cached copy expires. Lower the setting if that matters, or set it to0. Output built while a mapping, resolver, computed value or contributor threw is kept for one minute only, then built again, so a passing error (a timed-out call to the shop) doesn't leave that part out for the whole cache time. (A$code:name that no computed value has isn't an error of that kind: it is cached as usual.)
Documentation
Shipped alongside this README, and shown in the dashboard's Guide tab:
GUIDE.md: how to use the Schema Mapper, for developers and SEO specialists, with recipes for common pages.REFERENCE.md: the exact rules behind every option, the ID formulas, what is left out of the output and why, and the stored mapping JSON.
Reporting issues
Found a bug or have feedback? Email support@dot-see.com. Please include the package version, your Umbraco version, and, if possible, the mapping and the JSON-LD output (or error) you got.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Schema.NET.Pending (>= 13.0.0)
- Umbraco.Cms.Web.Common (>= 17.2.0 && < 18.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.
| Version | Downloads | Last Updated |
|---|---|---|
| 17.0.2 | 44 | 10/21/2026 |