diff --git a/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift b/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift index 92ff75e..4458f0e 100644 --- a/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift +++ b/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift @@ -1,7 +1,6 @@ import FluentKit import HummingbirdFluent import Logging -import NIOCore import Testing @testable import Persistence diff --git a/Packages/Web/Tests/Cases/Public/Extensions/RouterMethods+RouteCollectionsTests.swift b/Packages/Web/Tests/Cases/Public/Extensions/RouterMethods+RouteCollectionsTests.swift index 1808834..2f6edc5 100644 --- a/Packages/Web/Tests/Cases/Public/Extensions/RouterMethods+RouteCollectionsTests.swift +++ b/Packages/Web/Tests/Cases/Public/Extensions/RouterMethods+RouteCollectionsTests.swift @@ -1,5 +1,6 @@ import Hummingbird import HummingbirdTesting +import NIOCore import Testing @testable import Web diff --git a/Services/Website/Sources/App/App.swift b/Services/Website/Sources/App/App.swift index ee99ab9..679beff 100644 --- a/Services/Website/Sources/App/App.swift +++ b/Services/Website/Sources/App/App.swift @@ -1,6 +1,5 @@ import Configuration import Hummingbird -import Logging /// The entry point of the website executable. /// diff --git a/Services/Website/Sources/App/Extensions/App+Build.swift b/Services/Website/Sources/App/Extensions/App+Build.swift index 2bb0d52..1096740 100644 --- a/Services/Website/Sources/App/Extensions/App+Build.swift +++ b/Services/Website/Sources/App/Extensions/App+Build.swift @@ -3,6 +3,7 @@ import Hummingbird import HummingbirdCompression import Logging import Persistence +import Web import WebsiteLibrary /// Builds the website application. diff --git a/Services/Website/Sources/Library/Internal/Pages/ErrorPage.swift b/Services/Website/Sources/Library/Internal/Pages/ErrorPage.swift index 79a1f53..3f332fb 100644 --- a/Services/Website/Sources/Library/Internal/Pages/ErrorPage.swift +++ b/Services/Website/Sources/Library/Internal/Pages/ErrorPage.swift @@ -3,12 +3,12 @@ import Foundation import Localization /// The HTML page rendered for a not-found response, with its text localized to a given locale. -struct ErrorPage: HTMLDocument, Sendable { +struct ErrorPage { // MARK: Properties /// The locale the page content is localized to. - private let locale: Locale + let locale: Locale /// Resolves the page's text from the bundled String Catalog for the page's ``locale``. private let localize: Localize @@ -23,45 +23,31 @@ struct ErrorPage: HTMLDocument, Sendable { self.locale = locale self.localize = .init(bundle: .module) } +} - // MARK: Document +// MARK: Page - /// The page's content: a localized heading and explanatory message, followed by the error and shared scripts. - var body: some HTML { +extension ErrorPage: Page { + + // MARK: Properties + + var content: some HTML { h1 { localize("error.heading", locale: locale) } p { localize("error.message", locale: locale) } - script(.src(StaticFile.error.urlPath(for: .js))) {} - script(.src(StaticFile.shared.urlPath(for: .js))) {} } - /// The metadata and stylesheet links placed in the document head. - var head: some HTML { - meta(.charset(.utf8)) - meta( - .name(.viewport), - .content("width=device-width, initial-scale=1") - ) - link( - .rel(.stylesheet), - .href(StaticFile.shared.urlPath(for: .css)) - ) - link( - .rel(.stylesheet), - .href(StaticFile.error.urlPath(for: .css)) - ) + var scripts: [StaticFile] { + [.error, .shared] } - /// The document language, derived from the page's locale and falling back to the default language. - var lang: String { - locale.language.languageCode?.identifier - ?? LanguageList(bundle: .module).default + var stylesheets: [StaticFile] { + [.shared, .error] } - /// The localized document title. var title: String { localize("error.title", locale: locale) } diff --git a/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift b/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift index 6a9b6e2..aabef0b 100644 --- a/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift +++ b/Services/Website/Sources/Library/Internal/Pages/IndexPage.swift @@ -3,12 +3,12 @@ import Foundation import Localization /// The website's landing page, with its text localized to a given locale. -struct IndexPage: HTMLDocument, Sendable { +struct IndexPage { // MARK: Properties /// The locale the page content is localized to. - private let locale: Locale + let locale: Locale /// Resolves the page's text from the bundled String Catalog for the page's ``locale``. private let localize: Localize @@ -24,77 +24,28 @@ struct IndexPage: HTMLDocument, Sendable { self.localize = .init(bundle: .module) } - // MARK: Document +} - /// The page's content: a localized greeting followed by the shared and index scripts. - var body: some HTML { +// MARK: - Page + +extension IndexPage: Page { + + // MARK: Properties + + var content: some HTML { p { localize("index.greeting", locale: locale) } - script(.src(StaticFile.index.urlPath(for: .js))) {} - script(.src(StaticFile.shared.urlPath(for: .js))) {} } - /// The metadata, stylesheet, icon, and manifest links placed in the document head. - var head: some HTML { - meta(.charset(.utf8)) - meta( - .name(.viewport), - .content("width=device-width, initial-scale=1") - ) - link( - .rel(.stylesheet), - .href(StaticFile.shared.urlPath(for: .css)) - ) - link( - .rel(.stylesheet), - .href(StaticFile.index.urlPath(for: .css)) - ) - link( - .rel(.icon), - .href(StaticFile.favicon.urlPath(for: .ico)), - .custom( - name: "sizes", - value: "any" - ) - ) - link( - .rel(.icon), - .href(StaticFile.icon.urlPath(for: .svg)), - .custom( - name: "type", - value: "image/svg+xml" - ) - ) - link( - .rel("apple-touch-icon"), - .href(StaticFile.appleTouchIcon.urlPath(for: .png)) - ) - link( - .rel("manifest"), - .href(StaticFile.site.urlPath(for: .webmanifest)) - ) - meta( - .name("theme-color"), - .content("#0c0710"), - .custom( - name: "media", - value: "(prefers-color-scheme: dark)" - ) - ) - meta( - .name("theme-color"), - .content("#fafafa") - ) + var scripts: [StaticFile] { + [.index, .shared] } - /// The document language, derived from the page's locale and falling back to the default language. - var lang: String { - locale.language.languageCode?.identifier - ?? LanguageList(bundle: .module).default + var stylesheets: [StaticFile] { + [.shared, .index] } - /// The localized document title. var title: String { localize("index.title", locale: locale) } diff --git a/Services/Website/Sources/Library/Internal/Protocols/Page.swift b/Services/Website/Sources/Library/Internal/Protocols/Page.swift new file mode 100644 index 0000000..e05eb45 --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Protocols/Page.swift @@ -0,0 +1,107 @@ +import Elementary +import Foundation +import Localization + +/// A page of the website: an HTML document with the shared scaffolding assembled around the page's content. +/// +/// A conforming page supplies its locale, its localized title, the stylesheets and scripts it needs, and its content; the protocol assembles the rest of the +/// document around them: the metadata, stylesheet, icon, and manifest links in the head, the content followed by the script tags in the body, and the +/// document language derived from the locale. +protocol Page: HTMLDocument, Sendable { + + // MARK: Associated types + + /// The type of the page's markup. + associatedtype Content: HTML + + // MARK: Properties + + /// The page's markup, rendered before the ``scripts``. + @HTMLBuilder + var content: Content { get } + + /// The locale the page content is localized to. + var locale: Locale { get } + + /// The scripts loaded at the end of the document body, in order. + var scripts: [StaticFile] { get } + + /// The stylesheets linked in the document head, in order. + var stylesheets: [StaticFile] { get } + +} + +// MARK: - Implementations + +extension Page { + + // MARK: Computed + + /// The page ``content`` followed by its ``scripts``. + @HTMLBuilder + var body: some HTML { + content + for file in scripts { + script(.src(file.urlPath(for: .js))) {} + } + } + + /// The metadata, ``stylesheets``, icon, and manifest links placed in the document head. + @HTMLBuilder + var head: some HTML { + meta(.charset(.utf8)) + meta( + .name(.viewport), + .content("width=device-width, initial-scale=1") + ) + for file in stylesheets { + link( + .rel(.stylesheet), + .href(file.urlPath(for: .css)) + ) + } + link( + .rel(.icon), + .href(StaticFile.favicon.urlPath(for: .ico)), + .custom( + name: "sizes", + value: "any" + ) + ) + link( + .rel(.icon), + .href(StaticFile.icon.urlPath(for: .svg)), + .custom( + name: "type", + value: "image/svg+xml" + ) + ) + link( + .rel("apple-touch-icon"), + .href(StaticFile.appleTouchIcon.urlPath(for: .png)) + ) + link( + .rel("manifest"), + .href(StaticFile.site.urlPath(for: .webmanifest)) + ) + meta( + .name("theme-color"), + .content("#fafafa") + ) + meta( + .name("theme-color"), + .content("#0c0710"), + .custom( + name: "media", + value: "(prefers-color-scheme: dark)" + ) + ) + } + + /// The document language, derived from the page's locale and falling back to the default language. + var lang: String { + locale.language.languageCode?.identifier + ?? LanguageList(bundle: .module).default + } + +} diff --git a/Services/Website/Sources/Library/Public/Controllers/RootController.swift b/Services/Website/Sources/Library/Public/Controllers/RootController.swift index de32221..7a0dff0 100644 --- a/Services/Website/Sources/Library/Public/Controllers/RootController.swift +++ b/Services/Website/Sources/Library/Public/Controllers/RootController.swift @@ -25,7 +25,9 @@ public struct RootController { /// Creates a root controller. public init() { - self.responses = .init { IndexPage(locale: $0) } + self.responses = .init { + IndexPage(locale: $0) + } } } diff --git a/Services/Website/Tests/App/AppTests.swift b/Services/Website/Tests/App/AppTests.swift index e665763..23fe521 100644 --- a/Services/Website/Tests/App/AppTests.swift +++ b/Services/Website/Tests/App/AppTests.swift @@ -2,7 +2,6 @@ import Configuration import Foundation import Hummingbird import HummingbirdTesting -import Logging import NIOCore import Testing diff --git a/Services/Website/Tests/Library/Cases/Internal/Enumerations/StaticFileTests.swift b/Services/Website/Tests/Library/Cases/Internal/Enumerations/StaticFileTests.swift index 24bde11..2ca9b20 100644 --- a/Services/Website/Tests/Library/Cases/Internal/Enumerations/StaticFileTests.swift +++ b/Services/Website/Tests/Library/Cases/Internal/Enumerations/StaticFileTests.swift @@ -1,4 +1,3 @@ -import Foundation import Testing @testable import WebsiteLibrary