Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

The SEO kit — schema.org structured data for a page's head

head { StructuredData = new SeoProduct { Name = p.Name, Offers = new SeoOffer { Price = p.Price, Currency = "SEK" } }; }

`Osysharp.Seo` ships schema.org's shapes as classes: a product with its offer, rating and brand, an article, a breadcrumb trail, the organization behind a site, and the site itself. Each writes its own JSON-LD, with `@context` and `@type` filled in, and each is a `JsonLd` block, so a page's `head { StructuredData = … }` takes any of them, or several in a list.

stable1 example compiled by CIuiseokithead

Source on GitHub: Osysharp.Seo — github.com/osysharp/seo ↗

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 · Damaged

Description#

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 { } block StructuredData belongs to.
  • [Serve] — /sitemap.xml and /robots.txt, the other half of what a crawler reads.
  • JsonSerializer — JsonSerializer.Serialize, which every block writes itself with.

Related

head

A routed page's document head — its title, description, canonical address, share image, structured data, whether it may…

[Serve]

Gives a function an address on the app's own origin. A GET at that address answers with the function's return value as…

JsonSerializer

Turn a value into a JSON string and a JSON string into a typed object — the C#-faithful System.Text.Json spelling…