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
|
||||
/// 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 {
|
||||
|
||||
// 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.
|
||||
var canonicalURL: String? { get }
|
||||
|
||||
/// The page's markup, rendered before the ``scripts``.
|
||||
/// The page's markup, rendered as the document body.
|
||||
@HTMLBuilder
|
||||
var content: Content { get }
|
||||
|
||||
@@ -38,7 +38,10 @@ public protocol Page: HTMLDocument, Sendable {
|
||||
@HTMLBuilder
|
||||
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 }
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// The page ``content`` followed by its ``scripts``.
|
||||
/// The page ``content``; the ``scripts`` load deferred from the ``head``.
|
||||
@HTMLBuilder
|
||||
var body: some HTML {
|
||||
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
|
||||
/// ``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,
|
||||
/// 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.
|
||||
|
||||
@@ -21,7 +21,7 @@ struct PageTests {
|
||||
#expect(html.contains(#"name="viewport""#))
|
||||
#expect(html.contains(#"<meta name="stub" content="marker">"#))
|
||||
#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"))
|
||||
}
|
||||
|
||||
@@ -38,13 +38,17 @@ struct PageTests {
|
||||
}
|
||||
|
||||
@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 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 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
|
||||
|
||||
Reference in New Issue
Block a user