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