@using DeepDrftPublic.Client.Common @inject SeoOptions Seo @inject SeoEnvironment SeoEnv @inject NavigationManager Nav @* The single reusable SEO head surface (Phase 22). Presentational and parameter-fed — owns no fetch and no business logic (C4); it reads the injected SeoOptions for defaults and NavigationManager for the current path. Renders (the sole title source — pages drop their bare ) plus a block carrying the full standard/OG/Twitter/JSON-LD surface (§3), projected into the in App.razor so it is present in the prerendered HTML a crawler sees (C2/AC1). Identical output across the InteractiveAuto double render (AC6): every value comes from the parameter Model (built from the page's bridged PersistentComponentState) and config — never a browser API — so the prerender and WASM passes render byte-identical tags. Partial data (C6/AC4): a missing value falls back to config or omits its tag; og:image always resolves (the default guarantees presence) so there is never a content="" attribute or a broken node. *@ @_fullTitle @* Standard / search *@ @* Open Graph *@ @if (_hasCover) { } @* Music-vertical OG (release pages only) *@ @if (!string.IsNullOrWhiteSpace(Model.Artist)) { } @if (Model.ReleaseDate is not null) { } @if (_isoDuration is not null) { } @* Twitter Card. No twitter:site / twitter:creator — no X account exists (OQ3). *@ @* JSON-LD structured data *@ @if (!string.IsNullOrEmpty(Model.JsonLd)) { } @code { /// The page's resolved SEO input, built via a factory. [Parameter, EditorRequired] public required SeoModel Model { get; set; } private string _fullTitle = string.Empty; private string _description = string.Empty; private string _canonical = string.Empty; private string _robots = string.Empty; private string _ogType = "website"; private string _image = string.Empty; private bool _hasCover; private string? _isoDuration; protected override void OnParametersSet() { _fullTitle = $"{Model.Title} · {Seo.TitleSuffix}"; _description = string.IsNullOrWhiteSpace(Model.Description) ? Seo.DefaultDescription : Model.Description; // Default robots is environment-gated (non-production → noindex,nofollow) so beta/staging is never // crawled; an explicit per-page Robots still wins (e.g. the 404's / soft-404's noindex,follow). _robots = string.IsNullOrWhiteSpace(Model.Robots) ? SeoEnv.DefaultRobots : Model.Robots; _ogType = OgTypeString(Model.OgType); // Canonical: BaseUrl + the model's path, defaulting to the current relative path. The origin is // always config (no browser API) so prerender and WASM agree (§5). var path = Model.CanonicalPath ?? RelativePath(); _canonical = SeoUrls.Absolute(Seo, path); _hasCover = !string.IsNullOrWhiteSpace(Model.ImagePath); _image = SeoUrls.CoverOrDefault(Seo, Model.ImagePath); _isoDuration = SeoUrls.IsoDuration(Model.DurationSeconds); } private string RelativePath() { var path = Nav.ToBaseRelativePath(Nav.Uri); var query = path.IndexOf('?'); if (query >= 0) path = path[..query]; return "/" + path; } private static string OgTypeString(SeoOgType type) => type switch { SeoOgType.MusicAlbum => "music.album", SeoOgType.MusicSong => "music.song", _ => "website", }; }