diff --git a/public/about.md b/public/about.md index 1b10f97..20495d9 100644 --- a/public/about.md +++ b/public/about.md @@ -1,5 +1,6 @@ --- Title: About me +Page type: profile --- I'm Sami Samhuri, a software developer and general technology geek. Sometimes I write my thoughts and post my projects here. I moved to [Victoria, BC][vic] in 2003 to study computer science at the [University of Victoria][uvic], and then dropped out a couple of years later. diff --git a/public/images/me.jpg b/public/images/me.jpg index ddd4c02..db89643 100644 Binary files a/public/images/me.jpg and b/public/images/me.jpg differ diff --git a/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeed.swift b/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeed.swift index 4e0569d..4f04169 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeed.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeed.swift @@ -9,7 +9,6 @@ import Foundation struct JSONFeed { let path: String - let avatarPath: String? let iconPath: String? let faviconPath: String? } diff --git a/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeedWriter.swift b/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeedWriter.swift index 0a2b8cb..93b58aa 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeedWriter.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/JSONFeed/JSONFeedWriter.swift @@ -39,7 +39,7 @@ private extension JSONFeedWriter { func buildFeed(site: Site, posts: [Post], renderer: JSONFeedRendering) throws -> Feed { let author = FeedAuthor( name: site.author, - avatar: jsonFeed.avatarPath.map(site.url.appendingPathComponent)?.absoluteString, + avatar: site.imageURL?.absoluteString, url: site.url.absoluteString ) return Feed( diff --git a/samhuri.net/Sources/samhuri.net/Posts/Model/Post.swift b/samhuri.net/Sources/samhuri.net/Posts/Model/Post.swift index c8316af..271b042 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/Model/Post.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/Model/Post.swift @@ -18,6 +18,7 @@ struct Post { let scripts: [Script] let styles: [Stylesheet] let body: String + let excerpt: String let path: String var isLink: Bool { diff --git a/samhuri.net/Sources/samhuri.net/Posts/PostRepo.swift b/samhuri.net/Sources/samhuri.net/Posts/PostRepo.swift index 5466e24..94ad21c 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/PostRepo.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/PostRepo.swift @@ -11,6 +11,31 @@ import Ink struct RawPost { let slug: String let markdown: String + + private static let StripMetadataRegex = try! Regex(#"---\n.*?---\n"#).dotMatchesNewlines() + + private static let TextifyParenthesesLinksRegex = try! Regex(#"\[([\w\s.-_]*)\]\([^)]+\)"#) + + private static let TextifyBracketLinksRegex = try! Regex(#"\[([\w\s.-_]*)\]\[[^\]]+\]"#) + + private static let StripImagesRegex = try! Regex(#"!\[[\w\s.-_]*\]\([^)]+\)"#) + + private static let WhitespaceRegex = try! Regex(#"\s+"#) + + private static let StripHTMLTagsRegex = try! Regex(#"<[^>]+>"#) + + var excerpt: String { + markdown + .replacing(Self.StripMetadataRegex, with: "") + .replacing(Self.StripImagesRegex, with: "") // must be before links for linked images + .replacing(Self.TextifyParenthesesLinksRegex) { match in match.output[1].substring ?? "" } + .replacing(Self.TextifyBracketLinksRegex) { match in match.output[1].substring ?? "" } + .replacing(Self.StripHTMLTagsRegex, with: "") + .replacing(Self.WhitespaceRegex, with: " ") + .trimmingPrefix(Self.WhitespaceRegex) + .prefix(300) + + "..." + } } final class PostRepo { @@ -72,6 +97,7 @@ private extension PostRepo { scripts: metadata.scripts, styles: metadata.styles, body: result.html, + excerpt: rawPost.excerpt, path: path ) } diff --git a/samhuri.net/Sources/samhuri.net/Posts/PostWriter.swift b/samhuri.net/Sources/samhuri.net/Posts/PostWriter.swift index c016d69..377fceb 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/PostWriter.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/PostWriter.swift @@ -22,16 +22,18 @@ final class PostWriter { extension PostWriter { func writePosts(_ posts: [Post], for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { for post in posts { - let postHTML = try renderer.renderPost(post, site: site) - let postURL = targetURL - .appendingPathComponent(outputPath) - .appendingPathComponent(filePath(date: post.date, slug: post.slug)) - try fileWriter.write(string: postHTML, to: postURL) + let path = [ + outputPath, + postPath(date: post.date, slug: post.slug), + ].joined(separator: "/") + let fileURL = targetURL.appending(path: path).appending(component: "index.html") + let postHTML = try renderer.renderPost(post, site: site, path: path) + try fileWriter.write(string: postHTML, to: fileURL) } } - private func filePath(date: Date, slug: String) -> String { - "/\(date.year)/\(Month(date).padded)/\(slug)/index.html" + private func postPath(date: Date, slug: String) -> String { + "\(date.year)/\(Month(date).padded)/\(slug)" } } @@ -39,7 +41,7 @@ extension PostWriter { extension PostWriter { func writeRecentPosts(_ recentPosts: [Post], for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { - let recentPostsHTML = try renderer.renderRecentPosts(recentPosts, site: site) + let recentPostsHTML = try renderer.renderRecentPosts(recentPosts, site: site, path: "/") let fileURL = targetURL.appendingPathComponent("index.html") try fileWriter.write(string: recentPostsHTML, to: fileURL) } @@ -49,7 +51,7 @@ extension PostWriter { extension PostWriter { func writeArchive(posts: PostsByYear, for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { - let archiveHTML = try renderer.renderArchive(postsByYear: posts, site: site) + let archiveHTML = try renderer.renderArchive(postsByYear: posts, site: site, path: outputPath) let archiveURL = targetURL.appendingPathComponent(outputPath).appendingPathComponent("index.html") try fileWriter.write(string: archiveHTML, to: archiveURL) } @@ -61,7 +63,7 @@ extension PostWriter { func writeYearIndexes(posts: PostsByYear, for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { for yearPosts in posts.byYear.values { let yearDir = targetURL.appendingPathComponent(yearPosts.path) - let yearHTML = try renderer.renderYearPosts(yearPosts, site: site) + let yearHTML = try renderer.renderYearPosts(yearPosts, site: site, path: yearPosts.path) let yearURL = yearDir.appendingPathComponent("index.html") try fileWriter.write(string: yearHTML, to: yearURL) } @@ -75,7 +77,7 @@ extension PostWriter { for yearPosts in posts.byYear.values { for monthPosts in yearPosts.byMonth.values { let monthDir = targetURL.appendingPathComponent(monthPosts.path) - let monthHTML = try renderer.renderMonthPosts(monthPosts, site: site) + let monthHTML = try renderer.renderMonthPosts(monthPosts, site: site, path: monthPosts.path) let monthURL = monthDir.appendingPathComponent("index.html") try fileWriter.write(string: monthHTML, to: monthURL) } diff --git a/samhuri.net/Sources/samhuri.net/Posts/PostsPlugin+Builder.swift b/samhuri.net/Sources/samhuri.net/Posts/PostsPlugin+Builder.swift index 8029bb9..2e4b1b4 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/PostsPlugin+Builder.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/PostsPlugin+Builder.swift @@ -26,14 +26,12 @@ extension PostsPlugin { func jsonFeed( path: String? = nil, - avatarPath: String? = nil, iconPath: String? = nil, faviconPath: String? = nil ) -> Self { precondition(jsonFeed == nil, "JSON feed is already defined") jsonFeed = JSONFeed( path: path ?? "feed.json", - avatarPath: avatarPath, iconPath: iconPath, faviconPath: faviconPath ) diff --git a/samhuri.net/Sources/samhuri.net/Posts/PostsRendering.swift b/samhuri.net/Sources/samhuri.net/Posts/PostsRendering.swift index 0914839..28f02ad 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/PostsRendering.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/PostsRendering.swift @@ -8,13 +8,13 @@ import Foundation protocol PostsRendering { - func renderArchive(postsByYear: PostsByYear, site: Site) throws -> String + func renderArchive(postsByYear: PostsByYear, site: Site, path: String) throws -> String - func renderYearPosts(_ yearPosts: YearPosts, site: Site) throws -> String + func renderYearPosts(_ yearPosts: YearPosts, site: Site, path: String) throws -> String - func renderMonthPosts(_ posts: MonthPosts, site: Site) throws -> String + func renderMonthPosts(_ posts: MonthPosts, site: Site, path: String) throws -> String - func renderPost(_ post: Post, site: Site) throws -> String + func renderPost(_ post: Post, site: Site, path: String) throws -> String - func renderRecentPosts(_ posts: [Post], site: Site) throws -> String + func renderRecentPosts(_ posts: [Post], site: Site, path: String) throws -> String } diff --git a/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+JSONFeed.swift b/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+JSONFeed.swift index dcbe5c9..149e7cd 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+JSONFeed.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+JSONFeed.swift @@ -10,8 +10,13 @@ import Plot extension PageRenderer: JSONFeedRendering { func renderJSONFeedPost(_ post: Post, site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: post.title, templateAssets: post.templateAssets) let url = site.url.appendingPathComponent(post.path) + let context = SiteContext( + site: site, + canonicalURL: url, + subtitle: post.title, + templateAssets: post.templateAssets + ) // Turn relative URLs into absolute ones. return Node.feedPost(post, url: url, styles: context.styles) .render(indentedBy: .spaces(2)) diff --git a/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+Posts.swift b/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+Posts.swift index 44418ff..3a1bd3b 100644 --- a/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+Posts.swift +++ b/samhuri.net/Sources/samhuri.net/Posts/Templates/PageRenderer+Posts.swift @@ -9,29 +9,62 @@ import Foundation import Plot extension PageRenderer: PostsRendering { - func renderArchive(postsByYear: PostsByYear, site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: "Archive") + func renderArchive(postsByYear: PostsByYear, site: Site, path: String) throws -> String { + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: "Archive", + description: "Archive of all posts" + ) return render(.archive(postsByYear), context: context) } - func renderYearPosts(_ yearPosts: YearPosts, site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: yearPosts.title) + func renderYearPosts(_ yearPosts: YearPosts, site: Site, path: String) throws -> String { + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: yearPosts.title, + description: "Archive of all posts from \(yearPosts.year)", + pageType: "article" + ) return render(.yearPosts(yearPosts), context: context) } - func renderMonthPosts(_ posts: MonthPosts, site: Site) throws -> String { + func renderMonthPosts(_ posts: MonthPosts, site: Site, path: String) throws -> String { + let subtitle = "\(posts.month.name) \(posts.year)" let assets = posts.posts.templateAssets - let context = SiteContext(site: site, subtitle: "\(posts.month.name) \(posts.year)", templateAssets: assets) + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: subtitle, + description: "Archive of all posts from \(subtitle)", + pageType: "article", + templateAssets: assets + ) return render(.monthPosts(posts), context: context) } - func renderPost(_ post: Post, site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: post.title, templateAssets: post.templateAssets) + func renderPost(_ post: Post, site: Site, path: String) throws -> String { + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: post.title, + description: post.excerpt, + pageType: "article", + templateAssets: post.templateAssets + ) return render(.post(post, articleClass: "container"), context: context) } - func renderRecentPosts(_ posts: [Post], site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: nil, templateAssets: posts.templateAssets) + func renderRecentPosts(_ posts: [Post], site: Site, path: String) throws -> String { + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: nil, + description: "Recent posts", + pageType: "article", + templateAssets: posts.templateAssets + ) return render(.recentPosts(posts), context: context) } } diff --git a/samhuri.net/Sources/samhuri.net/Projects/ProjectsPlugin.swift b/samhuri.net/Sources/samhuri.net/Projects/ProjectsPlugin.swift index 40a0bb5..c4df27a 100644 --- a/samhuri.net/Sources/samhuri.net/Projects/ProjectsPlugin.swift +++ b/samhuri.net/Sources/samhuri.net/Projects/ProjectsPlugin.swift @@ -54,12 +54,13 @@ final class ProjectsPlugin: Plugin { let projectsDir = targetURL.appendingPathComponent(outputPath) let projectsURL = projectsDir.appendingPathComponent("index.html") - let projectsHTML = try renderer.renderProjects(projects, site: site) + let projectsHTML = try renderer.renderProjects(projects, site: site, path: outputPath) try fileWriter.write(string: projectsHTML, to: projectsURL) for project in projects { let projectURL = projectsDir.appendingPathComponent("\(project.title)/index.html") - let projectHTML = try renderer.renderProject(project, site: site, assets: projectAssets) + let path = [outputPath, project.title].joined(separator: "/") + let projectHTML = try renderer.renderProject(project, site: site, path: path, assets: projectAssets) try fileWriter.write(string: projectHTML, to: projectURL) } } diff --git a/samhuri.net/Sources/samhuri.net/Projects/ProjectsRenderer.swift b/samhuri.net/Sources/samhuri.net/Projects/ProjectsRenderer.swift index 912c687..f06c793 100644 --- a/samhuri.net/Sources/samhuri.net/Projects/ProjectsRenderer.swift +++ b/samhuri.net/Sources/samhuri.net/Projects/ProjectsRenderer.swift @@ -8,7 +8,7 @@ import Foundation protocol ProjectsRenderer { - func renderProjects(_ projects: [Project], site: Site) throws -> String + func renderProjects(_ projects: [Project], site: Site, path: String) throws -> String - func renderProject(_ project: Project, site: Site, assets: TemplateAssets) throws -> String + func renderProject(_ project: Project, site: Site, path: String, assets: TemplateAssets) throws -> String } diff --git a/samhuri.net/Sources/samhuri.net/Projects/Templates/PageRenderer+Projects.swift b/samhuri.net/Sources/samhuri.net/Projects/Templates/PageRenderer+Projects.swift index e8a1b0d..485a109 100644 --- a/samhuri.net/Sources/samhuri.net/Projects/Templates/PageRenderer+Projects.swift +++ b/samhuri.net/Sources/samhuri.net/Projects/Templates/PageRenderer+Projects.swift @@ -9,14 +9,25 @@ import Foundation import Plot extension PageRenderer: ProjectsRenderer { - func renderProjects(_ projects: [Project], site: Site) throws -> String { - let context = SiteContext(site: site, subtitle: "Projects", templateAssets: .empty()) + func renderProjects(_ projects: [Project], site: Site, path: String) throws -> String { + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: "Projects", + templateAssets: .empty() + ) return render(.projects(projects), context: context) } - func renderProject(_ project: Project, site: Site, assets: TemplateAssets) throws -> String { + func renderProject(_ project: Project, site: Site, path: String, assets: TemplateAssets) throws -> String { let projectContext = ProjectContext(project: project, site: site, templateAssets: assets) - let context = SiteContext(site: site, subtitle: project.title, templateAssets: assets) + let context = SiteContext( + site: site, + canonicalURL: site.url.appending(path: path), + subtitle: project.title, + description: project.description, + templateAssets: assets + ) return render(.project(projectContext), context: context) } } diff --git a/samhuri.net/Sources/samhuri.net/Projects/Templates/ProjectContext.swift b/samhuri.net/Sources/samhuri.net/Projects/Templates/ProjectContext.swift index 1ac3cce..3e0fd88 100644 --- a/samhuri.net/Sources/samhuri.net/Projects/Templates/ProjectContext.swift +++ b/samhuri.net/Sources/samhuri.net/Projects/Templates/ProjectContext.swift @@ -10,13 +10,16 @@ import Foundation struct ProjectContext: TemplateContext { let site: Site let title: String + let canonicalURL: URL let description: String + let pageType = "website" let githubURL: URL let templateAssets: TemplateAssets init(project: Project, site: Site, templateAssets: TemplateAssets) { self.site = site self.title = project.title + self.canonicalURL = site.url.appending(components: "projects", project.title) self.description = project.description self.githubURL = URL(string: "https://github.com/samsonjs/\(title)")! self.templateAssets = templateAssets diff --git a/samhuri.net/Sources/samhuri.net/Site/MarkdownRenderer.swift b/samhuri.net/Sources/samhuri.net/Site/MarkdownRenderer.swift index 9912b66..2343ab3 100644 --- a/samhuri.net/Sources/samhuri.net/Site/MarkdownRenderer.swift +++ b/samhuri.net/Sources/samhuri.net/Site/MarkdownRenderer.swift @@ -18,26 +18,31 @@ final class MarkdownRenderer: Renderer { self.fileWriter = fileWriter } - func canRenderFile(named filename: String, withExtension ext: String) -> Bool { + func canRenderFile(named filename: String, withExtension ext: String?) -> Bool { ext == "md" } /// Parse Markdown and render it as HTML, running it through a Stencil template. func render(site: Site, fileURL: URL, targetDir: URL) throws { - let bodyMarkdown = try String(contentsOf: fileURL) - let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) let metadata = try markdownMetadata(from: fileURL) - let pageHTML = try pageRenderer.renderPage(site: site, bodyHTML: bodyHTML, metadata: metadata) - let mdFilename = fileURL.lastPathComponent let showExtension = mdFilename == "index.md" || metadata["Show extension"]?.lowercased() == "yes" - let htmlPath: String - if showExtension { - htmlPath = mdFilename.replacingOccurrences(of: ".md", with: ".html") + let htmlPath: String = if showExtension { + mdFilename.replacingOccurrences(of: ".md", with: ".html") } else { - htmlPath = mdFilename.replacingOccurrences(of: ".md", with: "/index.html") + mdFilename.replacingOccurrences(of: ".md", with: "/index.html") } + let bodyMarkdown = try String(contentsOf: fileURL) + let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) + let url = site.url.appending(path: htmlPath.replacingOccurrences(of: "/index.html", with: "")) + let pageHTML = try pageRenderer.renderPage( + site: site, + url: url, + bodyHTML: bodyHTML, + metadata: metadata + ) + let htmlURL = targetDir.appendingPathComponent(htmlPath) try fileWriter.write(string: pageHTML, to: htmlURL) } diff --git a/samhuri.net/Sources/samhuri.net/Site/PageRendering.swift b/samhuri.net/Sources/samhuri.net/Site/PageRendering.swift index 06162bd..e00adb3 100644 --- a/samhuri.net/Sources/samhuri.net/Site/PageRendering.swift +++ b/samhuri.net/Sources/samhuri.net/Site/PageRendering.swift @@ -8,5 +8,5 @@ import Foundation protocol PageRendering { - func renderPage(site: Site, bodyHTML: String, metadata: [String: String]) throws -> String + func renderPage(site: Site, url: URL, bodyHTML: String, metadata: [String: String]) throws -> String } diff --git a/samhuri.net/Sources/samhuri.net/Site/Renderer.swift b/samhuri.net/Sources/samhuri.net/Site/Renderer.swift index 541ca09..abbae38 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Renderer.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Renderer.swift @@ -8,7 +8,7 @@ import Foundation protocol Renderer { - func canRenderFile(named filename: String, withExtension ext: String) -> Bool + func canRenderFile(named filename: String, withExtension ext: String?) -> Bool func render(site: Site, fileURL: URL, targetDir: URL) throws } diff --git a/samhuri.net/Sources/samhuri.net/Site/Site+Builder.swift b/samhuri.net/Sources/samhuri.net/Site/Site+Builder.swift index 97e1fca..ae5f648 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Site+Builder.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Site+Builder.swift @@ -12,6 +12,7 @@ extension Site { private let title: String private let description: String private let author: String + private let imageURL: URL? private let email: String private let url: URL @@ -25,12 +26,20 @@ extension Site { title: String, description: String, author: String, + imagePath: String?, email: String, url: URL ) { self.title = title self.description = description self.author = author + self.imageURL = imagePath.flatMap { path in + var imageURL = url + for component in path.split(separator: "/") { + imageURL = imageURL.appending(component: component) + } + return imageURL + } self.email = email self.url = url } @@ -61,6 +70,7 @@ extension Site { email: email, title: title, description: description, + imageURL: imageURL, url: url, scripts: scripts, styles: styles, diff --git a/samhuri.net/Sources/samhuri.net/Site/Site.swift b/samhuri.net/Sources/samhuri.net/Site/Site.swift index 41722ab..3ef2f75 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Site.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Site.swift @@ -12,6 +12,7 @@ struct Site { let email: String let title: String let description: String + let imageURL: URL? let url: URL let scripts: [Script] let styles: [Stylesheet] diff --git a/samhuri.net/Sources/samhuri.net/Site/SiteGenerator.swift b/samhuri.net/Sources/samhuri.net/Site/SiteGenerator.swift index c38139a..5265c55 100644 --- a/samhuri.net/Sources/samhuri.net/Site/SiteGenerator.swift +++ b/samhuri.net/Sources/samhuri.net/Site/SiteGenerator.swift @@ -70,7 +70,7 @@ final class SiteGenerator { try fileManager.removeItem(at: targetURL) } - let ext = String(filename.split(separator: ".").last!) + let ext = filename.split(separator: ".").last.flatMap { String($0) } for renderer in site.renderers { if renderer.canRenderFile(named: filename, withExtension: ext) { try renderer.render(site: site, fileURL: sourceURL, targetDir: targetDir) diff --git a/samhuri.net/Sources/samhuri.net/Site/Templates/PageRenderer.swift b/samhuri.net/Sources/samhuri.net/Site/Templates/PageRenderer.swift index ebe0345..ffc653f 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Templates/PageRenderer.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Templates/PageRenderer.swift @@ -9,18 +9,25 @@ import Foundation import Plot final class PageRenderer { - func render(_ body: Node, context: TemplateContext) -> String { + func render(_ body: Node, context: Context) -> String { Template.site(body: body, context: context).render(indentedBy: .spaces(2)) } } extension PageRenderer: PageRendering { - func renderPage(site: Site, bodyHTML: String, metadata: [String: String]) throws -> String { + func renderPage(site: Site, url: URL, bodyHTML: String, metadata: [String: String]) throws -> String { let pageTitle = metadata["Title"] + let pageType = metadata["Page type"] let scripts = metadata.commaSeparatedList(key: "Scripts").map(Script.init(ref:)) let styles = metadata.commaSeparatedList(key: "Styles").map(Stylesheet.init(ref:)) let assets = TemplateAssets(scripts: scripts, styles: styles) - let context = SiteContext(site: site, subtitle: pageTitle, templateAssets: assets) + let context = SiteContext( + site: site, + canonicalURL: url, + subtitle: pageTitle, + pageType: pageType, + templateAssets: assets + ) return render(.page(title: pageTitle ?? "", bodyHTML: bodyHTML), context: context) } } diff --git a/samhuri.net/Sources/samhuri.net/Site/Templates/SiteContext.swift b/samhuri.net/Sources/samhuri.net/Site/Templates/SiteContext.swift index 0554472..080ed69 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Templates/SiteContext.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Templates/SiteContext.swift @@ -9,12 +9,26 @@ import Foundation struct SiteContext: TemplateContext { let site: Site + let canonicalURL: URL let subtitle: String? + let description: String + let pageType: String let templateAssets: TemplateAssets - init(site: Site, subtitle: String? = nil, templateAssets: TemplateAssets = .empty()) { + init( + site: Site, + canonicalURL: URL, + subtitle: String? = nil, + description: String? = nil, + pageType: String? = nil, + templateAssets: TemplateAssets = .empty() + ) { self.site = site + self.canonicalURL = canonicalURL self.subtitle = subtitle + self.description = description ?? site.description + self.pageType = pageType ?? "website" + self.templateAssets = templateAssets } @@ -26,15 +40,3 @@ struct SiteContext: TemplateContext { return "\(site.title): \(subtitle)" } } - -extension SiteContext { - var dictionary: [String: Any] { - [ - "site": site, - "title": site.title, - "styles": site.styles, - "scripts": site.scripts, - "currentYear": Date().year, - ] - } -} diff --git a/samhuri.net/Sources/samhuri.net/Site/Templates/SiteTemplate.swift b/samhuri.net/Sources/samhuri.net/Site/Templates/SiteTemplate.swift index 8688596..0629a8b 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Templates/SiteTemplate.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Templates/SiteTemplate.swift @@ -8,35 +8,51 @@ import Foundation import Plot +private extension Node where Context == HTML.DocumentContext { + /// Add a `` HTML element within the current context, which + /// contains non-visual elements, such as stylesheets and metadata. + /// - parameter nodes: The element's attributes and child elements. + static func head(_ nodes: [Node]) -> Node { + .element(named: "head", nodes: nodes) + } +} + enum Template { - static func site(body: Node, context: TemplateContext) -> HTML { - HTML( + static func site(body: Node, context: Context) -> HTML { + // Broken up to fix a build error because Swift can't type-check the varargs version. + let headNodes: [Node] = [ + .encoding(.utf8), + .title(context.title), + .description(context.description), + .siteName(context.site.title), + .url(context.canonicalURL), + .meta(.property("og:image"), .content(context.site.imageURL?.absoluteString ?? "")), + .meta(.property("og:type"), .content(context.pageType)), + .meta(.property("article:author"), .content(context.site.author)), + .meta(.name("twitter:card"), .content("summary")), + .rssFeedLink(context.url(for: "feed.xml"), title: context.site.title), + .jsonFeedLink(context.url(for: "feed.json"), title: context.site.title), + .meta(.name("fediverse:creator"), .content("@sjs@techhub.social")), + .link(.rel(.author), .type("text/plain"), .href(context.url(for: "humans.txt"))), + .link(.rel(.icon), .type("image/png"), .href(context.imageURL("favicon-32x32.png"))), + .link(.rel(.shortcutIcon), .href(context.imageURL("favicon.icon"))), + .appleTouchIcon(context.imageURL("apple-touch-icon.png")), + .safariPinnedTabIcon(context.imageURL("safari-pinned-tab.svg"), color: "#aa0000"), + .link(.attribute(named: "rel", value: "manifest"), .href(context.imageURL("manifest.json"))), + .meta(.name("msapplication-config"), .content(context.imageURL("browserconfig.xml").absoluteString)), + .meta(.name("theme-color"), .content("#121212")), // matches header + .meta(.name("viewport"), .content("width=device-width, initial-scale=1.0, viewport-fit=cover")), + .link(.rel(.dnsPrefetch), .href("https://use.typekit.net")), + .link(.rel(.dnsPrefetch), .href("https://netdna.bootstrapcdn.com")), + .link(.rel(.dnsPrefetch), .href("https://gist.github.com")), + .group(context.styles.map { url in + .link(.rel(.stylesheet), .type("text/css"), .href(url)) + }), + ] + return HTML( .lang(.english), .comment("meow"), - .head( - .encoding(.utf8), - .title(context.title), - .siteName(context.site.title), - .url(context.site.url), - .rssFeedLink(context.url(for: "feed.xml"), title: context.site.title), - .jsonFeedLink(context.url(for: "feed.json"), title: context.site.title), - .meta(.name("fediverse:creator"), .content("@sjs@techhub.social")), - .link(.rel(.author), .type("text/plain"), .href(context.url(for: "humans.txt"))), - .link(.rel(.icon), .type("image/png"), .href(context.imageURL("favicon-32x32.png"))), - .link(.rel(.shortcutIcon), .href(context.imageURL("favicon.icon"))), - .appleTouchIcon(context.imageURL("apple-touch-icon.png")), - .safariPinnedTabIcon(context.imageURL("safari-pinned-tab.svg"), color: "#aa0000"), - .link(.attribute(named: "rel", value: "manifest"), .href(context.imageURL("manifest.json"))), - .meta(.name("msapplication-config"), .content(context.imageURL("browserconfig.xml").absoluteString)), - .meta(.name("theme-color"), .content("#121212")), // matches header - .meta(.name("viewport"), .content("width=device-width, initial-scale=1.0, viewport-fit=cover")), - .link(.rel(.dnsPrefetch), .href("https://use.typekit.net")), - .link(.rel(.dnsPrefetch), .href("https://netdna.bootstrapcdn.com")), - .link(.rel(.dnsPrefetch), .href("https://gist.github.com")), - .group(context.styles.map { url in - .link(.rel(.stylesheet), .type("text/css"), .href(url)) - }) - ), + .head(headNodes), .body( .header(.class("primary"), .div(.class("title"), diff --git a/samhuri.net/Sources/samhuri.net/Site/Templates/TemplateContext.swift b/samhuri.net/Sources/samhuri.net/Site/Templates/TemplateContext.swift index 1e9fda3..4dbf2df 100644 --- a/samhuri.net/Sources/samhuri.net/Site/Templates/TemplateContext.swift +++ b/samhuri.net/Sources/samhuri.net/Site/Templates/TemplateContext.swift @@ -12,6 +12,9 @@ protocol TemplateContext { var site: Site { get } var title: String { get } + var canonicalURL: URL { get } + var description: String { get } + var pageType: String { get } var templateAssets: TemplateAssets { get } // These all have default implementations diff --git a/samhuri.net/Sources/samhuri.net/samhuri.net.swift b/samhuri.net/Sources/samhuri.net/samhuri.net.swift index c190ceb..088cbf6 100644 --- a/samhuri.net/Sources/samhuri.net/samhuri.net.swift +++ b/samhuri.net/Sources/samhuri.net/samhuri.net.swift @@ -45,7 +45,6 @@ public extension samhuri { let postsPlugin = PostsPlugin.Builder(renderer: renderer) .path("posts") .jsonFeed( - avatarPath: "images/me.jpg", iconPath: "images/apple-touch-icon-300.png", faviconPath: "images/apple-touch-icon-80.png" ) @@ -54,8 +53,9 @@ public extension samhuri { return Site.Builder( title: "samhuri.net", - description: "just some blog", + description: "Sami Samhuri's blog about programming, mainly about iOS and Ruby and Rails these days.", author: "Sami Samhuri", + imagePath: "images/me.jpg", email: "sami@samhuri.net", url: siteURLOverride ?? URL(string: "https://samhuri.net")! )