Implemented the scripts' deferral on the Page protocol in the Infrastructure package.
This commit is contained in:
@@ -5,7 +5,7 @@ import Foundation
|
|||||||
///
|
///
|
||||||
/// A conforming page supplies its locale, its title, the stylesheets and scripts it needs, its head metadata, and its content; the protocol assembles the
|
/// 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
|
/// 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 in the head, and the content followed by the script tags in the body.
|
/// 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 {
|
public protocol Page: HTMLDocument, Sendable {
|
||||||
|
|
||||||
// MARK: Associated types
|
// MARK: Associated types
|
||||||
@@ -27,7 +27,7 @@ public protocol Page: HTMLDocument, Sendable {
|
|||||||
/// 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.
|
/// 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 }
|
var canonicalURL: String? { get }
|
||||||
|
|
||||||
/// The page's markup, rendered before the ``scripts``.
|
/// The page's markup, rendered as the document body.
|
||||||
@HTMLBuilder
|
@HTMLBuilder
|
||||||
var content: Content { get }
|
var content: Content { get }
|
||||||
|
|
||||||
@@ -38,7 +38,10 @@ public protocol Page: HTMLDocument, Sendable {
|
|||||||
@HTMLBuilder
|
@HTMLBuilder
|
||||||
var metadata: Metadata { get }
|
var metadata: Metadata { get }
|
||||||
|
|
||||||
/// The scripts loaded at the end of the document body, in order.
|
/// 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 }
|
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)
|
/// The card controlling the page's link previews, rendered as Open Graph and Twitter meta tags in the document head, or `nil` (the default)
|
||||||
@@ -67,21 +70,15 @@ public extension Page {
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The page ``content`` followed by its ``scripts``.
|
/// The page ``content``; the ``scripts`` load deferred from the ``head``.
|
||||||
@HTMLBuilder
|
@HTMLBuilder
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
content
|
content
|
||||||
|
|
||||||
for file in scripts {
|
|
||||||
script(.src(file.urlPath(
|
|
||||||
for: .js,
|
|
||||||
version: assetVersion
|
|
||||||
))) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the
|
/// 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, placed in the document head.
|
/// ``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 `<meta charset="UTF-8">` before this markup,
|
/// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `<meta charset="UTF-8">` before this markup,
|
||||||
/// and HTML5 allows only one.
|
/// and HTML5 allows only one.
|
||||||
@@ -155,6 +152,16 @@ public extension Page {
|
|||||||
))
|
))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for file in scripts {
|
||||||
|
script(
|
||||||
|
.defer,
|
||||||
|
.src(file.urlPath(
|
||||||
|
for: .js,
|
||||||
|
version: assetVersion
|
||||||
|
))
|
||||||
|
) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The analytics tracker is omitted unless the page provides one.
|
/// The analytics tracker is omitted unless the page provides one.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct PageTests {
|
|||||||
#expect(html.contains(#"name="viewport""#))
|
#expect(html.contains(#"name="viewport""#))
|
||||||
#expect(html.contains(#"<meta name="stub" content="marker">"#))
|
#expect(html.contains(#"<meta name="stub" content="marker">"#))
|
||||||
#expect(html.contains(#"<link rel="stylesheet" href="/css/stub.css">"#))
|
#expect(html.contains(#"<link rel="stylesheet" href="/css/stub.css">"#))
|
||||||
#expect(html.contains(#"<script src="/js/stub.js"></script>"#))
|
#expect(html.contains(#"<script defer src="/js/stub.js"></script>"#))
|
||||||
#expect(html.contains("Stub content"))
|
#expect(html.contains("Stub content"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,13 +38,17 @@ struct PageTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func `renders the scripts after the content`() throws {
|
func `renders the scripts deferred in the head, after the stylesheets`() throws {
|
||||||
let html = StubPage().render()
|
let html = StubPage().render()
|
||||||
|
|
||||||
let content = try #require(html.range(of: "Stub content"))
|
let stylesheet = try #require(html.range(of: "/css/stub.css"))
|
||||||
let script = try #require(html.range(of: "/js/stub.js"))
|
let script = try #require(html.range(of: "/js/stub.js"))
|
||||||
|
let content = try #require(html.range(of: "Stub content"))
|
||||||
|
|
||||||
#expect(content.lowerBound < script.lowerBound)
|
// Deferred head scripts start downloading during head parsing but still execute, in order, only after the
|
||||||
|
// document is parsed — the semantics end-of-body tags gave, minus the late download start.
|
||||||
|
#expect(stylesheet.lowerBound < script.lowerBound)
|
||||||
|
#expect(script.lowerBound < content.lowerBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user