namespace DeepDrftPublic.Client.Common; /// /// Absolute-URL composition for SEO tags (Phase 22). Canonical / og:url / og:image origins /// all come from (config), never from a browser API — there is no /// window.location during server prerender and the request host is unreliable behind nginx /// (§5, OQ1). Shared by the SeoModel factories (which absolutise JSON-LD url/image) /// and SeoHead (which absolutises the meta/OG tags) so the rule lives in exactly one place. /// public static class SeoUrls { /// BaseUrl + a site-relative path. Both sides are trimmed so the join never doubles or drops the slash. public static string Absolute(SeoOptions options, string path) { var origin = options.BaseUrl.TrimEnd('/'); if (string.IsNullOrEmpty(path)) return origin; return $"{origin}/{path.TrimStart('/')}"; } /// /// Absolute URL of a release/track cover from its FileDatabase ImagePath, via the public image /// route (api/image/{escaped}). Returns the configured default share image when no cover exists /// (C6/AC4 — a default guarantees og:image presence). /// public static string CoverOrDefault(SeoOptions options, string? imagePath) { if (string.IsNullOrWhiteSpace(imagePath)) return Absolute(options, options.DefaultImageUrl); return Absolute(options, $"api/image/{Uri.EscapeDataString(imagePath)}"); } /// /// ISO-8601 duration (e.g. PT1H2M3S) from a seconds value, for JSON-LD duration and the /// music:duration OG tag. Null / non-finite / non-positive input yields null (omit the tag). /// public static string? IsoDuration(double? seconds) { if (seconds is null || double.IsNaN(seconds.Value) || double.IsInfinity(seconds.Value) || seconds.Value <= 0) return null; return System.Xml.XmlConvert.ToString(TimeSpan.FromSeconds(seconds.Value)); } }