mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Add OpenGraph metadata tags and fix canonical URLs
This commit is contained in:
parent
a656a8859d
commit
c18a946dae
26 changed files with 219 additions and 95 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 292 KiB |
|
|
@ -9,7 +9,6 @@ import Foundation
|
|||
|
||||
struct JSONFeed {
|
||||
let path: String
|
||||
let avatarPath: String?
|
||||
let iconPath: String?
|
||||
let faviconPath: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct Post {
|
|||
let scripts: [Script]
|
||||
let styles: [Stylesheet]
|
||||
let body: String
|
||||
let excerpt: String
|
||||
let path: String
|
||||
|
||||
var isLink: Bool {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -9,18 +9,25 @@ import Foundation
|
|||
import Plot
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,35 +8,51 @@
|
|||
import Foundation
|
||||
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 {
|
||||
static func site(body: Node<HTML.BodyContext>, context: TemplateContext) -> HTML {
|
||||
HTML(
|
||||
static func site<Context: TemplateContext>(body: Node<HTML.BodyContext>, context: Context) -> 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),
|
||||
.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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")!
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue