Summary#
Search engines read structured data to show more than a link: a product's price and whether it is in stock, where
the page sits in the site, the organization's logo. That data is JSON-LD in schema.org's vocabulary, and its shapes are
easy to get subtly wrong. For example, offers needs its own @type, availability is a URL, and a breadcrumb's steps
are numbered from 1.
Osysharp.Seo writes those shapes for you. You fill in fields and hand the object to the page's
head { StructuredData = … }.
// app.osy
use Osysharp.Seo@0;Signature#
JsonLd // core — what `StructuredData` holds. JsonLd.Raw(json) · JsonLd.Of([a, b]) · .ToJson()
SeoProduct : JsonLd // Name · Description · Image · Sku · Gtin · Brand · Url · Offers · Rating
SeoOffer // Price · Currency · Availability · Condition · Url · PriceValidUntil
SeoRating // Value · Count · Best
SeoBrand // Name
SeoArticle : JsonLd // Type ("Article", "TechArticle", "NewsArticle") · Headline · Description · Language ·
// Published · Modified · Image · Url · Publisher
SeoBreadcrumbList : JsonLd // Steps — each a SeoBreadcrumb { Name, Url }
SeoOrganization : JsonLd // Name · Url · Logo · SameAs
SeoWebSite : JsonLd // Name · Url
enum SeoAvailability // InStock · OutOfStock · PreOrder · BackOrder · LimitedAvailability · SoldOut · Discontinued
enum SeoCondition // New · Used · Refurbished · DamagedDescription#
A member left null is absent#
Every block writes itself with JsonSerializer.Serialize(this) under WhenWritingNull, so a member you do not set
does not appear in the JSON. schema.org reads a missing property as "not stated", which is the truth. The same property
written as null would claim something.
Several blocks are one array#
A product page usually says two things: the product, and where it sits. StructuredData = [product, breadcrumbs];
writes them as one JSON-LD array, which is how JSON-LD carries more than one item. JsonLd.Of([...]) is the same
thing when you build the list somewhere else.
What the kit does not model#
JsonLd.Raw(json) is a block of JSON you wrote yourself, for any schema.org type the kit does not ship: a recipe,
an event, an FAQ. It is checked when the page is served, and text that is not JSON is left out of the head rather than
written into it. You can also derive from JsonLd and override ToJson(), which is exactly what the kit's classes
do.
Safe whatever the data says#
A product's name can contain anything, including </script>. The head writes the JSON into its script element with
every < escaped, so no row value can end that element early. Nothing in the kit, or in a class of your own, needs
to escape anything beyond what JSON needs.
Examples#
A product page's head#
app SeoExample {
use Osysharp.Seo;
model "model/**/*.osy";
}
using Osysharp.Seo;
entity Kettle {
[Required, MaxLength(120)] string Name;
[Required, MaxLength(120)] string Slug;
decimal Price;
int Stock;
security { allow read when IsAnonymous || IsAuthenticated; }
}
[Route("/k/{slug}")]
[AllowAnonymous]
[Title("Kettle")]
component KettlePage(string slug) {
var kettle = Kettle.Where(k => k.Slug == slug).FirstOrDefault();
head {
Title = kettle == null ? "Not found" : kettle.Name;
StructuredData = kettle == null ? null : [
new SeoProduct {
Name = kettle.Name,
Url = "https://kettles.example.com/k/" + kettle.Slug,
Offers = new SeoOffer {
Price = kettle.Price, Currency = "SEK",
Availability = kettle.Stock > 0 ? SeoAvailability.InStock : SeoAvailability.OutOfStock,
},
},
new SeoBreadcrumbList { Steps = [
new SeoBreadcrumb { Name = "Kettles", Url = "https://kettles.example.com/k" },
new SeoBreadcrumb { Name = kettle.Name },
] },
];
}
render { Text(slug); }
}The product block that page writes, for a kettle in stock:
{"@context":"https://schema.org","@type":"Product","name":"Blue Kettle","url":"https://kettles.example.com/k/blue",
"offers":{"@type":"Offer","price":349.50,"priceCurrency":"SEK","availability":"https://schema.org/InStock"}}See also#
- head — the
head { }blockStructuredDatabelongs to. - [Serve] —
/sitemap.xmland/robots.txt, the other half of what a crawler reads. - JsonSerializer —
JsonSerializer.Serialize, which every block writes itself with.