import Elementary import Foundation import Infrastructure import Localization /// The site-wide defaults shared by every page of the website. extension Page { // MARK: Constants /// The fonts preloaded on every page: one per face the stylesheets actually render. /// /// Listed rather than derived from every WOFF2 in ``StaticFile/all``: a subset gated by a `unicode-range` no page reaches would add a /// download that never otherwise happens. Empty until the site ships fonts of its own. static var preloadedFonts: [StaticFile] { [] } /// The territories paired with the site's languages, in Open Graph's `language_TERRITORY` form. /// /// English ships as `en_US`, Open Graph's conventional default. A site serving a language in a territory of its own repoints or extends the /// map (`en_NL`, `nl_NL`, …); a language with no entry stays a bare code, which scrapers also accept. static var ogLocales: [String: String] { ["en": "en_US"] } // MARK: Computed /// The document language, derived from the page's locale and falling back to the default language. var lang: String { locale.language.languageCode?.identifier ?? LanguageList().default } /// The locale of the page's social card, in Open Graph's `language_TERRITORY` form where ``ogLocales`` pairs the page's language with a /// territory; a language it does not name stays a bare code, which scrapers also accept. /// /// Keyed off ``lang``, so the card's locale and the document's `lang` attribute never describe the document differently. Nothing reads it /// until a page supplies a `socialCard` — like ``preloadedFonts``, it is a hook a generated site fills in. var ogLocale: String { Self.ogLocales[lang] ?? lang } /// The site-wide head metadata; a page that adds tags of its own composes ``siteMetadata`` rather than replacing it. var metadata: some HTML { siteMetadata } /// The `hreflang` alternates tying the page's language editions together, or nothing without an origin — the annotations require absolute URLs. /// /// Nothing is emitted for a single-language site either: a set naming one edition tells a search engine nothing it cannot already see. /// `x-default` points at the catalog's default language, whose bare URL negotiates the language and so is the right landing for everyone /// unmatched — it stays the catalog's default even when `languages` names a subset. /// - Parameters: /// - origin: the site's public origin, or `nil` to emit nothing. /// - path: the page's bare (default-language) path. /// - languages: the languages the page is published in; every catalog language by default. A page translated into only some of them /// narrows the set, so it never advertises an edition that does not exist. /// - Returns: one `alternate` link per language, followed by the `x-default` link. @HTMLBuilder func languageAlternates( origin: String?, path: String, languages: [Language] = Language.all ) -> some HTML { if let origin, languages.count > 1 { ForEach(languages) { language in link( .rel("alternate"), .custom( name: "hreflang", value: language.identifier ), .href(language.url( origin: origin, path: path )) ) } link( .rel("alternate"), .custom( name: "hreflang", value: "x-default" ), .href(Language.default.url( origin: origin, path: path )) ) } } /// The ``preloadedFonts`` links, then the icon, manifest, and theme colour metadata shared by every page of the website. /// /// A preload URL must match the stylesheet's `@font-face` source exactly — unversioned, with `crossorigin` — or the browser fetches the /// font twice. @HTMLBuilder var siteMetadata: some HTML { for file in Self.preloadedFonts { link( .rel("preload"), .href(file.urlPath(for: .woff2)), .as(.font), .custom( name: "type", value: AssetExtension.woff2.contentType ), .crossorigin(.anonymous) ) } link( .rel(.icon), .href(StaticFile.favicon.urlPath( for: .ico, version: assetVersion )), .custom( name: "sizes", value: "any" ) ) link( .rel(.icon), .href(StaticFile.icon.urlPath( for: .svg, version: assetVersion )), .custom( name: "type", value: "image/svg+xml" ) ) link( .rel("apple-touch-icon"), .href(StaticFile.appleTouchIcon.urlPath( for: .png, version: assetVersion )) ) link( .rel("manifest"), .href(StaticFile.site.urlPath( for: .webmanifest, version: assetVersion )) ) meta( .name("theme-color"), .content("#fafafa") ) meta( .name("theme-color"), .content("#0c0710"), .custom( name: "media", value: "(prefers-color-scheme: dark)" ) ) } }