HTML rendering support for the Website service (#5)
This PR contains the work done to replace the use of static _HTML_ files with type-safe HTML rendered server-side via **Elementary** through **Hummingbird**. To provide further details about the work done: * Added the **Elementary** dependencies. * Added the `IndexPage` and `ErrorPage` pages, ported from the old HTML boilerplate; removed the static files. * Added the `RootController` controller serving GET / using the `IndexPage` page, wired into the router. * Reworked the `NotFoundMiddleware` middleare to render `ErrorPage` page directly; non-notFound errors still propagate. * the `FileMiddleware` middleware no longer searches for any static `index.html` file. * Simplified the `StaticFile` enumeration, dropped unused constants and now-unnecessary throws. Reviewed-on: rock-n-code/loud-amsterdam#5 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import Foundation
|
||||
|
||||
/// A static file shipped with the website service.
|
||||
///
|
||||
/// Each case identifies a file stored under the static files root (the `Resources/Static`
|
||||
@@ -7,16 +5,12 @@ import Foundation
|
||||
enum StaticFile: CaseIterable, Sendable {
|
||||
/// The `js/app.js` script.
|
||||
case appJS
|
||||
/// The `404.html` error page.
|
||||
case errorHTML
|
||||
/// The `favicon.ico` icon.
|
||||
case faviconICO
|
||||
/// The `icon.png` icon.
|
||||
case iconPNG
|
||||
/// The `icon.svg` icon.
|
||||
case iconSVG
|
||||
/// The `index.html` landing page.
|
||||
case indexHTML
|
||||
/// The `robots.txt` crawler directives.
|
||||
case robotsTXT
|
||||
/// The `site.webmanifest` web application manifest.
|
||||
@@ -32,8 +26,6 @@ extension StaticFile {
|
||||
enum Extension: String, Sendable {
|
||||
/// A Cascading Style Sheets file.
|
||||
case css
|
||||
/// A HyperText Markup Language file.
|
||||
case html
|
||||
/// A JavaScript file.
|
||||
case js
|
||||
/// A Portable Network Graphics image.
|
||||
@@ -59,7 +51,6 @@ extension StaticFile {
|
||||
var contentType: String {
|
||||
switch fileExtension {
|
||||
case .css: "text/css"
|
||||
case .html: "text/html"
|
||||
case .js: "text/javascript"
|
||||
case .png: "image/png"
|
||||
case .ico: "image/vnd.microsoft.icon"
|
||||
@@ -74,8 +65,6 @@ extension StaticFile {
|
||||
switch self {
|
||||
case .styleCSS: .css
|
||||
case .appJS: .js
|
||||
case .errorHTML,
|
||||
.indexHTML: .html
|
||||
case .faviconICO: .ico
|
||||
case .iconPNG: .png
|
||||
case .iconSVG: .svg
|
||||
@@ -88,11 +77,9 @@ extension StaticFile {
|
||||
var fileName: String {
|
||||
switch self {
|
||||
case .appJS: "app"
|
||||
case .errorHTML: "404"
|
||||
case .faviconICO: "favicon"
|
||||
case .iconPNG,
|
||||
.iconSVG: "icon"
|
||||
case .indexHTML: "index"
|
||||
case .robotsTXT: "robots"
|
||||
case .siteWebmanifest: "site"
|
||||
case .styleCSS: "style"
|
||||
@@ -103,18 +90,10 @@ extension StaticFile {
|
||||
///
|
||||
/// This also matches the URL path the file is served at by `FileMiddleware`.
|
||||
var relativePath: String {
|
||||
let file = String(
|
||||
format: Constant.Format.file,
|
||||
fileName,
|
||||
fileExtension.rawValue
|
||||
)
|
||||
let file = "\(fileName).\(fileExtension.rawValue)"
|
||||
|
||||
return subdirectory
|
||||
.map { .init(
|
||||
format: Constant.Format.path,
|
||||
$0,
|
||||
file
|
||||
)} ?? file
|
||||
.map { "\($0)/\(file)" } ?? file
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
@@ -129,12 +108,8 @@ extension StaticFile {
|
||||
guard !basePath.isEmpty else {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
return .init(
|
||||
format: Constant.Format.path,
|
||||
basePath,
|
||||
relativePath
|
||||
)
|
||||
|
||||
return "\(basePath)/\(relativePath)"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -155,12 +130,3 @@ private extension StaticFile {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private enum Constant {
|
||||
enum Format {
|
||||
static let file = "%@.%@"
|
||||
static let path = "%@/%@"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import Elementary
|
||||
|
||||
/// The HTML page rendered for a not-found response.
|
||||
struct ErrorPage: HTMLDocument, Sendable {
|
||||
|
||||
// MARK: Document
|
||||
|
||||
/// The page's content.
|
||||
var body: some HTML {
|
||||
h1 { "Page Not Found" }
|
||||
p { "Sorry, but the page you were trying to view does not exist." }
|
||||
}
|
||||
|
||||
/// The metadata and inline styles placed in the document head.
|
||||
var head: some HTML {
|
||||
meta(.charset(.utf8))
|
||||
meta(
|
||||
.name(.viewport),
|
||||
.content("width=device-width, initial-scale=1")
|
||||
)
|
||||
style { Self.styles }
|
||||
}
|
||||
|
||||
/// The document language.
|
||||
var lang: String { "en" }
|
||||
|
||||
/// The document title.
|
||||
var title: String { "Page Not Found" }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension ErrorPage {
|
||||
/// The inline CSS applied to the page.
|
||||
static let styles = """
|
||||
* {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
color: #888;
|
||||
display: table;
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #555;
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 280px) {
|
||||
body, p {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 0.3em;
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import Elementary
|
||||
|
||||
/// The website's landing page.
|
||||
struct IndexPage: HTMLDocument, Sendable {
|
||||
|
||||
// MARK: Document
|
||||
|
||||
/// The page's content.
|
||||
var body: some HTML {
|
||||
p { "Hello world! This is HTML5 Boilerplate." }
|
||||
script(.src("js/app.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("css/style.css")
|
||||
)
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href("/favicon.ico"),
|
||||
.custom(
|
||||
name: "sizes",
|
||||
value: "any"
|
||||
)
|
||||
)
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href("/icon.svg"),
|
||||
.custom(
|
||||
name: "type",
|
||||
value: "image/svg+xml"
|
||||
)
|
||||
)
|
||||
link(
|
||||
.rel("apple-touch-icon"),
|
||||
.href("icon.png")
|
||||
)
|
||||
link(
|
||||
.rel("manifest"),
|
||||
.href("site.webmanifest")
|
||||
)
|
||||
meta(
|
||||
.name("theme-color"),
|
||||
.content("#fafafa")
|
||||
)
|
||||
}
|
||||
|
||||
/// The document language.
|
||||
var lang: String {
|
||||
"en"
|
||||
}
|
||||
|
||||
/// The document title.
|
||||
var title: String { "" }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user