using System.Text.Json; using System.Text.Json.Serialization; namespace DeepDrftPublic.Client.Common; /// /// Typed schema.org JSON-LD nodes (Phase 22, OQ5 — the typed-builder option). Each record mirrors one /// schema.org type; renders a node to the <script type="application/ld+json"> /// body. Keeping the shape in C# (not hand-written JSON in pages) is what makes the medium→type mapping /// live in one place (DRY, §4.3) and the output unit-testable (AC5) rather than a manual validator pass. /// /// /// All nodes share so the @context/@type pair serialises first and /// once. Null properties are omitted (the serializer ignores nulls) so partial data never emits an empty /// or broken node (C6/AC4). /// /// public static class SeoJsonLd { private static readonly JsonSerializerOptions Options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // schema.org keys are PascalCase ("@type", "byArtist", "datePublished"); JsonPropertyName drives // each. Encoder relaxed so the JSON sits inline in HTML without over-escaping apostrophes etc. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = false, }; /// Renders a node to its compact JSON-LD script body. The host component wraps it in the script tag. public static string Serialize(TNode node) where TNode : JsonLdNode => JsonSerializer.Serialize(node, node.GetType(), Options); } /// Base for every schema.org node: emits @context and @type first. public abstract record JsonLdNode { [JsonPropertyName("@context")] [JsonPropertyOrder(-2)] public string Context => "https://schema.org"; [JsonPropertyName("@type")] [JsonPropertyOrder(-1)] public abstract string Type { get; } } /// The Deep DRFT collective entity — the home/about node. public sealed record MusicGroupNode : JsonLdNode { public override string Type => "MusicGroup"; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("url")] public string? Url { get; init; } [JsonPropertyName("genre")] public string? Genre { get; init; } [JsonPropertyName("description")] public string? Description { get; init; } [JsonPropertyName("logo")] public string? Logo { get; init; } [JsonPropertyName("sameAs")] public IReadOnlyList? SameAs { get; init; } } /// A studio cut or a live session release. AlbumProductionType distinguishes them. public sealed record MusicAlbumNode : JsonLdNode { public override string Type => "MusicAlbum"; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("byArtist")] public ArtistRef? ByArtist { get; init; } /// schema.org MusicAlbumProductionType URI, e.g. StudioAlbum or LiveAlbum. [JsonPropertyName("albumProductionType")] public string? AlbumProductionType { get; init; } [JsonPropertyName("datePublished")] public string? DatePublished { get; init; } [JsonPropertyName("genre")] public string? Genre { get; init; } [JsonPropertyName("image")] public string? Image { get; init; } [JsonPropertyName("url")] public string? Url { get; init; } /// Ordered list of the album's recordings (cut track list, in TrackNumber order). [JsonPropertyName("track")] public IReadOnlyList? Track { get; init; } } /// A single recording — a mix release, or one track inside an album's track list. public sealed record MusicRecordingNode : JsonLdNode { public override string Type => "MusicRecording"; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("byArtist")] public ArtistRef? ByArtist { get; init; } /// ISO-8601 duration (e.g. PT1H2M3S) from DurationSeconds. [JsonPropertyName("duration")] public string? Duration { get; init; } [JsonPropertyName("genre")] public string? Genre { get; init; } [JsonPropertyName("image")] public string? Image { get; init; } [JsonPropertyName("url")] public string? Url { get; init; } } /// A browse/index surface listing releases (cuts/sessions/mixes/archive). public sealed record CollectionPageNode : JsonLdNode { public override string Type => "CollectionPage"; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("description")] public string? Description { get; init; } [JsonPropertyName("url")] public string? Url { get; init; } } /// A nested byArtist reference — the collective as a MusicGroup, by name. public sealed record ArtistRef { [JsonPropertyName("@type")] public string Type => "MusicGroup"; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; }