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;
}