Add OpenGraph metadata tags and fix canonical URLs

This commit is contained in:
Sami Samhuri 2025-01-03 14:05:04 -08:00
parent a656a8859d
commit c18a946dae
No known key found for this signature in database
26 changed files with 219 additions and 95 deletions

View file

@ -1,5 +1,6 @@
--- ---
Title: About me 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. 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View file

@ -9,7 +9,6 @@ import Foundation
struct JSONFeed { struct JSONFeed {
let path: String let path: String
let avatarPath: String?
let iconPath: String? let iconPath: String?
let faviconPath: String? let faviconPath: String?
} }

View file

@ -39,7 +39,7 @@ private extension JSONFeedWriter {
func buildFeed(site: Site, posts: [Post], renderer: JSONFeedRendering) throws -> Feed { func buildFeed(site: Site, posts: [Post], renderer: JSONFeedRendering) throws -> Feed {
let author = FeedAuthor( let author = FeedAuthor(
name: site.author, name: site.author,
avatar: jsonFeed.avatarPath.map(site.url.appendingPathComponent)?.absoluteString, avatar: site.imageURL?.absoluteString,
url: site.url.absoluteString url: site.url.absoluteString
) )
return Feed( return Feed(

View file

@ -18,6 +18,7 @@ struct Post {
let scripts: [Script] let scripts: [Script]
let styles: [Stylesheet] let styles: [Stylesheet]
let body: String let body: String
let excerpt: String
let path: String let path: String
var isLink: Bool { var isLink: Bool {

View file

@ -11,6 +11,31 @@ import Ink
struct RawPost { struct RawPost {
let slug: String let slug: String
let markdown: 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 { final class PostRepo {
@ -72,6 +97,7 @@ private extension PostRepo {
scripts: metadata.scripts, scripts: metadata.scripts,
styles: metadata.styles, styles: metadata.styles,
body: result.html, body: result.html,
excerpt: rawPost.excerpt,
path: path path: path
) )
} }

View file

@ -22,16 +22,18 @@ final class PostWriter {
extension PostWriter { extension PostWriter {
func writePosts(_ posts: [Post], for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { func writePosts(_ posts: [Post], for site: Site, to targetURL: URL, with renderer: PostsRendering) throws {
for post in posts { for post in posts {
let postHTML = try renderer.renderPost(post, site: site) let path = [
let postURL = targetURL outputPath,
.appendingPathComponent(outputPath) postPath(date: post.date, slug: post.slug),
.appendingPathComponent(filePath(date: post.date, slug: post.slug)) ].joined(separator: "/")
try fileWriter.write(string: postHTML, to: postURL) 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 { private func postPath(date: Date, slug: String) -> String {
"/\(date.year)/\(Month(date).padded)/\(slug)/index.html" "\(date.year)/\(Month(date).padded)/\(slug)"
} }
} }
@ -39,7 +41,7 @@ extension PostWriter {
extension PostWriter { extension PostWriter {
func writeRecentPosts(_ recentPosts: [Post], for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { 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") let fileURL = targetURL.appendingPathComponent("index.html")
try fileWriter.write(string: recentPostsHTML, to: fileURL) try fileWriter.write(string: recentPostsHTML, to: fileURL)
} }
@ -49,7 +51,7 @@ extension PostWriter {
extension PostWriter { extension PostWriter {
func writeArchive(posts: PostsByYear, for site: Site, to targetURL: URL, with renderer: PostsRendering) throws { 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") let archiveURL = targetURL.appendingPathComponent(outputPath).appendingPathComponent("index.html")
try fileWriter.write(string: archiveHTML, to: archiveURL) 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 { func writeYearIndexes(posts: PostsByYear, for site: Site, to targetURL: URL, with renderer: PostsRendering) throws {
for yearPosts in posts.byYear.values { for yearPosts in posts.byYear.values {
let yearDir = targetURL.appendingPathComponent(yearPosts.path) 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") let yearURL = yearDir.appendingPathComponent("index.html")
try fileWriter.write(string: yearHTML, to: yearURL) try fileWriter.write(string: yearHTML, to: yearURL)
} }
@ -75,7 +77,7 @@ extension PostWriter {
for yearPosts in posts.byYear.values { for yearPosts in posts.byYear.values {
for monthPosts in yearPosts.byMonth.values { for monthPosts in yearPosts.byMonth.values {
let monthDir = targetURL.appendingPathComponent(monthPosts.path) 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") let monthURL = monthDir.appendingPathComponent("index.html")
try fileWriter.write(string: monthHTML, to: monthURL) try fileWriter.write(string: monthHTML, to: monthURL)
} }

View file

@ -26,14 +26,12 @@ extension PostsPlugin {
func jsonFeed( func jsonFeed(
path: String? = nil, path: String? = nil,
avatarPath: String? = nil,
iconPath: String? = nil, iconPath: String? = nil,
faviconPath: String? = nil faviconPath: String? = nil
) -> Self { ) -> Self {
precondition(jsonFeed == nil, "JSON feed is already defined") precondition(jsonFeed == nil, "JSON feed is already defined")
jsonFeed = JSONFeed( jsonFeed = JSONFeed(
path: path ?? "feed.json", path: path ?? "feed.json",
avatarPath: avatarPath,
iconPath: iconPath, iconPath: iconPath,
faviconPath: faviconPath faviconPath: faviconPath
) )

View file

@ -8,13 +8,13 @@
import Foundation import Foundation
protocol PostsRendering { 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
} }

View file

@ -10,8 +10,13 @@ import Plot
extension PageRenderer: JSONFeedRendering { extension PageRenderer: JSONFeedRendering {
func renderJSONFeedPost(_ post: Post, site: Site) throws -> String { 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 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. // Turn relative URLs into absolute ones.
return Node.feedPost(post, url: url, styles: context.styles) return Node.feedPost(post, url: url, styles: context.styles)
.render(indentedBy: .spaces(2)) .render(indentedBy: .spaces(2))

View file

@ -9,29 +9,62 @@ import Foundation
import Plot import Plot
extension PageRenderer: PostsRendering { extension PageRenderer: PostsRendering {
func renderArchive(postsByYear: PostsByYear, site: Site) throws -> String { func renderArchive(postsByYear: PostsByYear, site: Site, path: String) throws -> String {
let context = SiteContext(site: site, subtitle: "Archive") let context = SiteContext(
site: site,
canonicalURL: site.url.appending(path: path),
subtitle: "Archive",
description: "Archive of all posts"
)
return render(.archive(postsByYear), context: context) return render(.archive(postsByYear), context: context)
} }
func renderYearPosts(_ yearPosts: YearPosts, site: Site) throws -> String { func renderYearPosts(_ yearPosts: YearPosts, site: Site, path: String) throws -> String {
let context = SiteContext(site: site, subtitle: yearPosts.title) 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) 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 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) return render(.monthPosts(posts), context: context)
} }
func renderPost(_ post: Post, site: Site) throws -> String { func renderPost(_ post: Post, site: Site, path: String) throws -> String {
let context = SiteContext(site: site, subtitle: post.title, templateAssets: post.templateAssets) 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) return render(.post(post, articleClass: "container"), context: context)
} }
func renderRecentPosts(_ posts: [Post], site: Site) throws -> String { func renderRecentPosts(_ posts: [Post], site: Site, path: String) throws -> String {
let context = SiteContext(site: site, subtitle: nil, templateAssets: posts.templateAssets) 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) return render(.recentPosts(posts), context: context)
} }
} }

View file

@ -54,12 +54,13 @@ final class ProjectsPlugin: Plugin {
let projectsDir = targetURL.appendingPathComponent(outputPath) let projectsDir = targetURL.appendingPathComponent(outputPath)
let projectsURL = projectsDir.appendingPathComponent("index.html") 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) try fileWriter.write(string: projectsHTML, to: projectsURL)
for project in projects { for project in projects {
let projectURL = projectsDir.appendingPathComponent("\(project.title)/index.html") 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) try fileWriter.write(string: projectHTML, to: projectURL)
} }
} }

View file

@ -8,7 +8,7 @@
import Foundation import Foundation
protocol ProjectsRenderer { 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
} }

View file

@ -9,14 +9,25 @@ import Foundation
import Plot import Plot
extension PageRenderer: ProjectsRenderer { extension PageRenderer: ProjectsRenderer {
func renderProjects(_ projects: [Project], site: Site) throws -> String { func renderProjects(_ projects: [Project], site: Site, path: String) throws -> String {
let context = SiteContext(site: site, subtitle: "Projects", templateAssets: .empty()) let context = SiteContext(
site: site,
canonicalURL: site.url.appending(path: path),
subtitle: "Projects",
templateAssets: .empty()
)
return render(.projects(projects), context: context) 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 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) return render(.project(projectContext), context: context)
} }
} }

View file

@ -10,13 +10,16 @@ import Foundation
struct ProjectContext: TemplateContext { struct ProjectContext: TemplateContext {
let site: Site let site: Site
let title: String let title: String
let canonicalURL: URL
let description: String let description: String
let pageType = "website"
let githubURL: URL let githubURL: URL
let templateAssets: TemplateAssets let templateAssets: TemplateAssets
init(project: Project, site: Site, templateAssets: TemplateAssets) { init(project: Project, site: Site, templateAssets: TemplateAssets) {
self.site = site self.site = site
self.title = project.title self.title = project.title
self.canonicalURL = site.url.appending(components: "projects", project.title)
self.description = project.description self.description = project.description
self.githubURL = URL(string: "https://github.com/samsonjs/\(title)")! self.githubURL = URL(string: "https://github.com/samsonjs/\(title)")!
self.templateAssets = templateAssets self.templateAssets = templateAssets

View file

@ -18,26 +18,31 @@ final class MarkdownRenderer: Renderer {
self.fileWriter = fileWriter self.fileWriter = fileWriter
} }
func canRenderFile(named filename: String, withExtension ext: String) -> Bool { func canRenderFile(named filename: String, withExtension ext: String?) -> Bool {
ext == "md" ext == "md"
} }
/// Parse Markdown and render it as HTML, running it through a Stencil template. /// Parse Markdown and render it as HTML, running it through a Stencil template.
func render(site: Site, fileURL: URL, targetDir: URL) throws { 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 metadata = try markdownMetadata(from: fileURL)
let pageHTML = try pageRenderer.renderPage(site: site, bodyHTML: bodyHTML, metadata: metadata)
let mdFilename = fileURL.lastPathComponent let mdFilename = fileURL.lastPathComponent
let showExtension = mdFilename == "index.md" || metadata["Show extension"]?.lowercased() == "yes" let showExtension = mdFilename == "index.md" || metadata["Show extension"]?.lowercased() == "yes"
let htmlPath: String let htmlPath: String = if showExtension {
if showExtension { mdFilename.replacingOccurrences(of: ".md", with: ".html")
htmlPath = mdFilename.replacingOccurrences(of: ".md", with: ".html")
} }
else { 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) let htmlURL = targetDir.appendingPathComponent(htmlPath)
try fileWriter.write(string: pageHTML, to: htmlURL) try fileWriter.write(string: pageHTML, to: htmlURL)
} }

View file

@ -8,5 +8,5 @@
import Foundation import Foundation
protocol PageRendering { 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
} }

View file

@ -8,7 +8,7 @@
import Foundation import Foundation
protocol Renderer { 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 func render(site: Site, fileURL: URL, targetDir: URL) throws
} }

View file

@ -12,6 +12,7 @@ extension Site {
private let title: String private let title: String
private let description: String private let description: String
private let author: String private let author: String
private let imageURL: URL?
private let email: String private let email: String
private let url: URL private let url: URL
@ -25,12 +26,20 @@ extension Site {
title: String, title: String,
description: String, description: String,
author: String, author: String,
imagePath: String?,
email: String, email: String,
url: URL url: URL
) { ) {
self.title = title self.title = title
self.description = description self.description = description
self.author = author 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.email = email
self.url = url self.url = url
} }
@ -61,6 +70,7 @@ extension Site {
email: email, email: email,
title: title, title: title,
description: description, description: description,
imageURL: imageURL,
url: url, url: url,
scripts: scripts, scripts: scripts,
styles: styles, styles: styles,

View file

@ -12,6 +12,7 @@ struct Site {
let email: String let email: String
let title: String let title: String
let description: String let description: String
let imageURL: URL?
let url: URL let url: URL
let scripts: [Script] let scripts: [Script]
let styles: [Stylesheet] let styles: [Stylesheet]

View file

@ -70,7 +70,7 @@ final class SiteGenerator {
try fileManager.removeItem(at: targetURL) 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 { for renderer in site.renderers {
if renderer.canRenderFile(named: filename, withExtension: ext) { if renderer.canRenderFile(named: filename, withExtension: ext) {
try renderer.render(site: site, fileURL: sourceURL, targetDir: targetDir) try renderer.render(site: site, fileURL: sourceURL, targetDir: targetDir)

View file

@ -9,18 +9,25 @@ import Foundation
import Plot import Plot
final class PageRenderer { final class PageRenderer {
func render(_ body: Node<HTML.BodyContext>, context: TemplateContext) -> String { func render<Context: TemplateContext>(_ body: Node<HTML.BodyContext>, context: Context) -> String {
Template.site(body: body, context: context).render(indentedBy: .spaces(2)) Template.site(body: body, context: context).render(indentedBy: .spaces(2))
} }
} }
extension PageRenderer: PageRendering { 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 pageTitle = metadata["Title"]
let pageType = metadata["Page type"]
let scripts = metadata.commaSeparatedList(key: "Scripts").map(Script.init(ref:)) let scripts = metadata.commaSeparatedList(key: "Scripts").map(Script.init(ref:))
let styles = metadata.commaSeparatedList(key: "Styles").map(Stylesheet.init(ref:)) let styles = metadata.commaSeparatedList(key: "Styles").map(Stylesheet.init(ref:))
let assets = TemplateAssets(scripts: scripts, styles: styles) 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) return render(.page(title: pageTitle ?? "", bodyHTML: bodyHTML), context: context)
} }
} }

View file

@ -9,12 +9,26 @@ import Foundation
struct SiteContext: TemplateContext { struct SiteContext: TemplateContext {
let site: Site let site: Site
let canonicalURL: URL
let subtitle: String? let subtitle: String?
let description: String
let pageType: String
let templateAssets: TemplateAssets 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.site = site
self.canonicalURL = canonicalURL
self.subtitle = subtitle self.subtitle = subtitle
self.description = description ?? site.description
self.pageType = pageType ?? "website"
self.templateAssets = templateAssets self.templateAssets = templateAssets
} }
@ -26,15 +40,3 @@ struct SiteContext: TemplateContext {
return "\(site.title): \(subtitle)" return "\(site.title): \(subtitle)"
} }
} }
extension SiteContext {
var dictionary: [String: Any] {
[
"site": site,
"title": site.title,
"styles": site.styles,
"scripts": site.scripts,
"currentYear": Date().year,
]
}
}

View file

@ -8,35 +8,51 @@
import Foundation import Foundation
import Plot import Plot
private extension Node where Context == HTML.DocumentContext {
/// Add a `<head>` 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<HTML.HeadContext>]) -> Node {
.element(named: "head", nodes: nodes)
}
}
enum Template { enum Template {
static func site(body: Node<HTML.BodyContext>, context: TemplateContext) -> HTML { static func site<Context: TemplateContext>(body: Node<HTML.BodyContext>, context: Context) -> HTML {
HTML( // Broken up to fix a build error because Swift can't type-check the varargs version.
let headNodes: [Node<HTML.HeadContext>] = [
.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), .lang(.english),
.comment("meow"), .comment("meow"),
.head( .head(headNodes),
.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))
})
),
.body( .body(
.header(.class("primary"), .header(.class("primary"),
.div(.class("title"), .div(.class("title"),

View file

@ -12,6 +12,9 @@ protocol TemplateContext {
var site: Site { get } var site: Site { get }
var title: String { get } var title: String { get }
var canonicalURL: URL { get }
var description: String { get }
var pageType: String { get }
var templateAssets: TemplateAssets { get } var templateAssets: TemplateAssets { get }
// These all have default implementations // These all have default implementations

View file

@ -45,7 +45,6 @@ public extension samhuri {
let postsPlugin = PostsPlugin.Builder(renderer: renderer) let postsPlugin = PostsPlugin.Builder(renderer: renderer)
.path("posts") .path("posts")
.jsonFeed( .jsonFeed(
avatarPath: "images/me.jpg",
iconPath: "images/apple-touch-icon-300.png", iconPath: "images/apple-touch-icon-300.png",
faviconPath: "images/apple-touch-icon-80.png" faviconPath: "images/apple-touch-icon-80.png"
) )
@ -54,8 +53,9 @@ public extension samhuri {
return Site.Builder( return Site.Builder(
title: "samhuri.net", 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", author: "Sami Samhuri",
imagePath: "images/me.jpg",
email: "sami@samhuri.net", email: "sami@samhuri.net",
url: siteURLOverride ?? URL(string: "https://samhuri.net")! url: siteURLOverride ?? URL(string: "https://samhuri.net")!
) )