diff --git a/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedPlugin.swift new file mode 100644 index 0000000..8fd8881 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedPlugin.swift @@ -0,0 +1,39 @@ +// +// JSONFeedPlugin.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-10. +// + +import Foundation + +final class JSONFeedPlugin: Plugin { + let postRepo: PostRepo + let jsonFeedWriter: JSONFeedWriter + + init( + postRepo: PostRepo = PostRepo(), + jsonFeedWriter: JSONFeedWriter = JSONFeedWriter() + ) { + self.postRepo = postRepo + self.jsonFeedWriter = jsonFeedWriter + } + + // MARK: - Plugin methods + + func setUp(site: Site, sourceURL: URL) throws { + guard postRepo.postDataExists(at: sourceURL) else { + return + } + + try postRepo.readPosts(sourceURL: sourceURL, makePath: jsonFeedWriter.urlPathForPost) + } + + func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws { + guard !postRepo.isEmpty else { + return + } + + try jsonFeedWriter.writeFeed(postRepo.postsForFeed, site: site, to: targetURL, with: templateRenderer) + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedWriter.swift b/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedWriter.swift new file mode 100644 index 0000000..40835ca --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Feeds/JSONFeedWriter.swift @@ -0,0 +1,98 @@ +// +// JSONFeedWriter.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-10. +// + +import Foundation + +private struct Feed: Codable { + let version = "https://jsonfeed.org/version/1" + let title: String + let home_page_url: String + let feed_url: String + let author: FeedAuthor + let icon: String? + let favicon: String? + let items: [FeedItem] +} + +private struct FeedAuthor: Codable { + let name: String + let avatar: String? + let url: String? +} + +private struct FeedItem: Codable { + let title: String + let date_published: Date + let id: String + let url: String + let external_url: String? + let author: FeedAuthor + let content_html: String + let tags: [String] +} + +final class JSONFeedWriter { + let fileManager: FileManager + let feedPath: String + let postsPath: String + + var baseURL: URL! + + init(fileManager: FileManager = .default, feedPath: String = "feed.json", postsPath: String = "posts") { + self.fileManager = fileManager + self.feedPath = feedPath + self.postsPath = postsPath + } + + #warning("These urlPath methods were copied from PostsPlugin and should possibly be moved somewhere else") + + func urlPath(year: Int) -> String { + "/\(postsPath)/\(year)" + } + + func urlPath(year: Int, month: Month) -> String { + urlPath(year: year).appending("/\(month.padded)") + } + + func urlPathForPost(date: Date, slug: String) -> String { + urlPath(year: date.year, month: Month(date.month)).appending("/\(slug)") + } + + func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + let items: [FeedItem] = try posts.map { post in + let url = site.url.appendingPathComponent(post.path) + return FeedItem( + title: post.isLink ? "→ \(post.title)" : post.title, + date_published: post.date, + id: url.absoluteString, + url: url.absoluteString, + external_url: post.link?.absoluteString, + author: FeedAuthor(name: post.author, avatar: nil, url: nil), + content_html: try templateRenderer.renderTemplate(name: "feed-post.html", context: [ + "post": post, + ]), + tags: post.tags + ) + } + let avatar = site.avatarPath.map(site.url.appendingPathComponent) + let feed: Feed = Feed( + title: site.title, + home_page_url: site.url.absoluteString, + feed_url: site.url.appendingPathComponent(feedPath).absoluteString, + author: FeedAuthor(name: site.author, avatar: avatar?.absoluteString, url: site.url.absoluteString), + icon: site.iconPath.map(site.url.appendingPathComponent)?.absoluteString, + favicon: site.faviconPath.map(site.url.appendingPathComponent)?.absoluteString, + items: items + ) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes] + let feedJSON = try encoder.encode(feed) + let feedURL = targetURL.appendingPathComponent(feedPath) + try feedJSON.write(to: feedURL, options: [.atomic]) + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/HumanSite.swift b/SiteGenerator/Sources/SiteGenerator/Generator/HumanSite.swift index bf7e308..f53306c 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/HumanSite.swift +++ b/SiteGenerator/Sources/SiteGenerator/Generator/HumanSite.swift @@ -17,6 +17,9 @@ struct HumanSite: Codable { let template: String? let styles: [String]? let scripts: [String]? + let avatar: String? + let icon: String? + let favicon: String? } extension Site { @@ -29,7 +32,10 @@ extension Site { url: humanSite.url, template: humanSite.template ?? "page", styles: humanSite.styles ?? [], - scripts: humanSite.scripts ?? [] + scripts: humanSite.scripts ?? [], + avatarPath: humanSite.avatar, + iconPath: humanSite.icon, + faviconPath: humanSite.favicon ) } } diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Site.swift b/SiteGenerator/Sources/SiteGenerator/Generator/Site.swift index a28568d..307fdd1 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Site.swift +++ b/SiteGenerator/Sources/SiteGenerator/Generator/Site.swift @@ -16,6 +16,11 @@ public struct Site { public let template: String public let styles: [String] public let scripts: [String] + + // Used for JSON feed + public let avatarPath: String? + public let iconPath: String? + public let faviconPath: String? } extension Site { diff --git a/SiteGenerator/Sources/SiteGenerator/main.swift b/SiteGenerator/Sources/SiteGenerator/main.swift index 840a3f4..9dc5d61 100644 --- a/SiteGenerator/Sources/SiteGenerator/main.swift +++ b/SiteGenerator/Sources/SiteGenerator/main.swift @@ -13,7 +13,7 @@ func main(sourcePath: String, targetPath: String) throws { let targetURL = URL(fileURLWithPath: targetPath) let generator = try Generator( sourceURL: sourceURL, - plugins: [ProjectsPlugin(), PostsPlugin(), RSSFeedPlugin()], + plugins: [ProjectsPlugin(), PostsPlugin(), RSSFeedPlugin(), JSONFeedPlugin()], renderers: [LessRenderer(), MarkdownRenderer()] ) try generator.generate(targetURL: targetURL) diff --git a/site.json b/site.json index d414198..514caf6 100644 --- a/site.json +++ b/site.json @@ -8,5 +8,8 @@ "/css/normalize.css", "/css/style.css" ], - "scripts": [] + "scripts": [], + "icon": "images/apple-touch-icon-300.png", + "favicon": "images/apple-touch-icon-80.png", + "avatar": "images/me.jpg", }