import Elementary import Foundation /// A page of a website: an HTML document with the shared scaffolding assembled around the page's content. /// /// A conforming page supplies its locale, its title, the stylesheets and scripts it needs, its head metadata, and its content; the protocol assembles the /// rest of the document around them: the viewport declaration, the summary, canonical, and social card tags, the structured data script, the analytics /// tracker script, and the metadata followed by the stylesheet links and the deferred script tags in the head, and the content as the body. public protocol Page: HTMLDocument, Sendable { // MARK: Associated types /// The type of the page's markup. associatedtype Content: HTML /// The type of the page's head metadata markup. associatedtype Metadata: HTML // MARK: Properties /// The analytics tracker embedded as a deferred script in the document head, or `nil` (the default) to omit it. var analytics: Analytics? { get } /// The version token appended to the page's asset URLs, or `nil` to leave them unversioned. var assetVersion: String? { get } /// The canonical URL the page is served at, rendered as a `link rel="canonical"` tag in the document head, or `nil` (the default) to omit the tag. var canonicalURL: String? { get } /// The page's markup, rendered as the document body. @HTMLBuilder var content: Content { get } /// The locale the page content is localized to. var locale: Locale { get } /// The markup placed in the document head before the ``stylesheets`` links: icon and manifest links, extra meta tags, and the like. @HTMLBuilder var metadata: Metadata { get } /// The scripts loaded from the document head, in order. /// /// Rendered as `defer`red tags: the downloads start while the head is parsed, and the scripts still execute in order only after the /// document is fully parsed — the same semantics end-of-body tags would give, minus the late download start. var scripts: [any Asset] { get } /// The card controlling the page's link previews, rendered as Open Graph and Twitter meta tags in the document head, or `nil` (the default) /// to omit them. var socialCard: SocialCard? { get } /// The page's structured data, rendered as a JSON-LD script in the document head, or `nil` (the default) to omit it. var structuredData: StructuredData? { get } /// The stylesheets linked in the document head, in order. var stylesheets: [any Asset] { get } /// The page's summary, rendered as a `meta name="description"` tag in the document head, or `nil` (the default) to omit the tag. var summary: String? { get } } // MARK: - Implementations public extension Page { // MARK: Computed /// The canonical URL is omitted unless the page provides one. var canonicalURL: String? { nil } /// The page ``content``; the ``scripts`` load deferred from the ``head``. @HTMLBuilder var body: some HTML { content } /// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links and the deferred /// ``scripts`` tags, placed in the document head. /// /// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `` before this markup, /// and HTML5 allows only one. /// /// The structured data is an inert data block — browsers never execute it, so a site's `Content-Security-Policy` does not apply to it — /// that search engines read for the organization's name, logo, and profiles. The analytics tracker, by contrast, is an executable script the /// policy must allow, and it is `defer`red so it never delays the page render; each behavior flag renders its `data-` attribute only when enabled. @HTMLBuilder var head: some HTML { meta( .name(.viewport), .content("width=device-width, initial-scale=1") ) if let summary { meta( .name(.description), .content(summary) ) } if let canonicalURL { link( .rel("canonical"), .href(canonicalURL) ) } if let socialCard { for tag in socialCard.tags { meta( .custom( name: tag.attribute.rawValue, value: tag.name.rawValue ), .content(tag.content) ) } } if let structuredData { script(.custom( name: "type", value: "application/ld+json" )) { HTMLRaw(structuredData.payload) } } if let analytics { script( .defer, .src(analytics.scriptURL) ) {} .attributes(contentsOf: analytics.attributes.map { .custom( name: $0.name, value: $0.value ) }) } metadata for file in stylesheets { link( .rel(.stylesheet), .href(file.urlPath( for: .css, version: assetVersion )) ) } for file in scripts { script( .defer, .src(file.urlPath( for: .js, version: assetVersion )) ) {} } } /// The analytics tracker is omitted unless the page provides one. var analytics: Analytics? { nil } /// The social card is omitted unless the page provides one. var socialCard: SocialCard? { nil } /// The structured data is omitted unless the page provides one. var structuredData: StructuredData? { nil } /// The summary is omitted unless the page provides one. var summary: String? { nil } }