Render a JSON feed

This commit is contained in:
Sami Samhuri 2019-12-10 22:29:32 -08:00
parent 1d0ffd52a2
commit dd96d95fc4
6 changed files with 154 additions and 3 deletions

View file

@ -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)
}
}

View file

@ -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])
}
}

View file

@ -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
)
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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",
}