Simplify images, tag overview

This commit is contained in:
Christoph Hagen
2025-01-04 08:44:26 +01:00
parent 4d4275e072
commit 22e7d9a05a
49 changed files with 603 additions and 509 deletions

View File

@ -26,7 +26,7 @@ final class GenerationResults: ObservableObject {
var requiredFiles: Set<FileResource> = []
@Published
var imagesToGenerate: Set<ImageGenerationJob> = []
var imagesToGenerate: Set<ImageVersion> = []
@Published
var invalidCommands: Set<String> = []
@ -38,7 +38,7 @@ final class GenerationResults: ObservableObject {
var unsavedOutputFiles: Set<String> = []
@Published
var failedImages: Set<ImageGenerationJob> = []
var failedImages: Set<ImageVersion> = []
@Published
var emptyPages: Set<LocalizedPageId> = []
@ -151,11 +151,11 @@ final class GenerationResults: ObservableObject {
update { self.requiredFiles.formUnion(files) }
}
func generate(_ image: ImageGenerationJob) {
func generate(_ image: ImageVersion) {
update { self.imagesToGenerate.insert(image) }
}
func generate<S>(_ images: S) where S: Sequence, S.Element == ImageGenerationJob {
func generate<S>(_ images: S) where S: Sequence, S.Element == ImageVersion {
update { self.imagesToGenerate.formUnion(images) }
}
@ -167,7 +167,7 @@ final class GenerationResults: ObservableObject {
update { self.warnings.insert(warning) }
}
func failed(image: ImageGenerationJob) {
func failed(image: ImageVersion) {
update { self.failedImages.insert(image) }
}

View File

@ -15,20 +15,33 @@ final class ImageGenerator {
self.storage = storage
self.settings = settings
self.generatedImages = storage.loadListOfGeneratedImages() ?? [:]
print("ImageGenerator: Loaded list of \(totalImageCount) already generated images")
}
private var outputFolder: String {
settings.paths.imagesOutputFolderPath
}
private var totalImageCount: Int {
generatedImages.values.reduce(0) { $0 + $1.count }
}
@discardableResult
func save() -> Bool {
guard storage.save(listOfGeneratedImages: generatedImages) else {
print("Failed to save list of generated images")
print("ImageGenerator: Failed to save list of generated images")
return false
}
print("ImageGenerator: Saved list of \(totalImageCount) images")
return true
}
private var avifCommands: Set<String> = []
func printAvifCommands() {
avifCommands.sorted().forEach { print($0) }
}
/**
Remove all versions of an image, so that they will be recreated on the next run.
@ -44,26 +57,27 @@ final class ImageGenerator {
print("Image generator: \(generatedImages.count)/\(images.count) images (\(versionCount) versions)")
}
private func needsToGenerate(version: String, for image: String) -> Bool {
if exists(version) {
private func hasPreviouslyGenerated(_ version: ImageVersion) -> Bool {
guard let versions = generatedImages[version.image.id] else {
return false
}
guard let versions = generatedImages[image] else {
return true
}
guard versions.contains(version) else {
return true
}
return !exists(version)
return versions.contains(version.versionId)
}
private func hasNowGenerated(version: String, for image: String) {
guard var versions = generatedImages[image] else {
generatedImages[image] = [version]
return
private func needsToGenerate(_ version: ImageVersion) -> Bool {
if hasPreviouslyGenerated(version) {
return false
}
versions.insert(version)
generatedImages[image] = versions
if exists(version) {
// Mark as already generated
hasNowGenerated(version)
return false
}
return true
}
private func hasNowGenerated(_ version: ImageVersion) {
generatedImages[version.image.id, default: []].insert(version.versionId)
}
private func removeVersions(for image: String) {
@ -72,53 +86,59 @@ final class ImageGenerator {
// MARK: Files
private func exists(_ image: String) -> Bool {
storage.hasFileInOutputFolder(relativePath(for: image))
private func exists(_ version: ImageVersion) -> Bool {
storage.hasFileInOutputFolder(version.outputPath)
}
private func relativePath(for image: String) -> String {
outputFolder + "/" + image
}
private func write(imageData data: Data, version: String) -> Bool {
return storage.write(data, to: relativePath(for: version))
private func write(imageData data: Data, of version: ImageVersion) -> Bool {
return storage.write(data, to: version.outputPath)
}
// MARK: Image operations
func generate(job: ImageGenerationJob) -> Bool {
guard needsToGenerate(version: job.version, for: job.image) else {
func generate(version: ImageVersion) -> Bool {
guard needsToGenerate(version) else {
return true
}
guard let data = storage.fileData(for: job.image) else {
print("Failed to load image \(job.image)")
guard let data = version.image.dataContent() else {
print("ImageGenerator: Failed to load data for image \(version.image.id)")
return false
}
guard let originalImage = NSImage(data: data) else {
print("Failed to load image")
print("ImageGenerator: Failed to load image \(version.image.id)")
return false
}
let representation = create(image: originalImage, width: CGFloat(job.maximumWidth), height: CGFloat(job.maximumHeight))
let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight))
guard let data = create(image: representation, type: job.type, quality: job.quality) else {
print("Failed to get data for type \(job.type)")
guard let data = create(image: representation, type: version.type, quality: version.quality) else {
print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)")
return false
}
if job.type == .avif {
let input = job.version.fileNameAndExtension.fileName + "." + job.image.fileExtension!
print("avifenc -q 70 \(input) \(job.version)")
hasNowGenerated(version: job.version, for: job.image)
if version.type == .avif {
// AVIF conversion is very slow, so we save bash commands
// for the conversion instead
let baseVersion = ImageVersion(
image: version.image,
type: version.image.type,
maximumWidth: version.maximumWidth,
maximumHeight: version.maximumHeight)
let originalImagePath = storage.outputPath(to: baseVersion.outputPath)!.path()
let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
let quality = Int(version.quality * 100)
avifCommands.insert("avifenc -q \(quality) '\(originalImagePath)' '\(generatedImagePath)'")
// hasNowGenerated(version)
return true
}
guard write(imageData: data, version: job.version) else {
guard write(imageData: data, of: version) else {
return false
}
hasNowGenerated(version: job.version, for: job.image)
hasNowGenerated(version)
return true
}

View File

@ -1,73 +0,0 @@
import Foundation
struct ImageGenerationJob {
let image: String
let type: FileType
let maximumWidth: Int
let maximumHeight: Int
let quality: CGFloat
init(image: String, type: FileType, maximumWidth: CGFloat, maximumHeight: CGFloat, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = Int(maximumWidth)
self.maximumHeight = Int(maximumHeight)
self.quality = quality
}
init(image: String, type: FileType, maximumWidth: Int, maximumHeight: Int, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = maximumWidth
self.maximumHeight = maximumHeight
self.quality = quality
}
var version: String {
let fileName = image.fileNameAndExtension.fileName
let prefix = "\(fileName)@\(maximumWidth)x\(maximumHeight)"
return "\(prefix).\(type.fileExtension)"
}
static func imageSet(for image: String, maxWidth: Int, maxHeight: Int, quality: CGFloat = 0.7) -> [ImageGenerationJob] {
let type = FileType(fileExtension: image.fileExtension)
let width2x = maxWidth * 2
let height2x = maxHeight * 2
return [
.init(image: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: type, maximumWidth: width2x, maximumHeight: height2x, quality: quality)
]
}
}
extension ImageGenerationJob: Equatable {
static func == (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
lhs.version == rhs.version
}
}
extension ImageGenerationJob: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(version)
}
}
extension ImageGenerationJob: Comparable {
static func < (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
lhs.version < rhs.version
}
}

View File

@ -0,0 +1,52 @@
import Foundation
struct ImageSet {
let image: FileResource
let maxWidth: Int
let maxHeight: Int
let quality: CGFloat
let description: String
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7) {
self.image = image
self.maxWidth = maxWidth
self.maxHeight = maxHeight
self.description = description
self.quality = quality
}
var jobs: [ImageVersion] {
let type = image.type
let width2x = maxWidth * 2
let height2x = maxHeight * 2
return [
.init(image: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: type, maximumWidth: width2x, maximumHeight: height2x, quality: quality)
]
}
var content: String {
let fileExtension = image.type.fileExtension.map { "." + $0 } ?? ""
let prefix1x = "/\(image.outputImageFolder)/\(maxWidth)x\(maxHeight)"
let prefix2x = "/\(image.outputImageFolder)/\(maxWidth*2)x\(maxHeight*2)"
var result = "<picture>"
result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>"
result += "<source type='image/webp' srcset='\(prefix1x).webm 1x, \(prefix1x).webm 2x'/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'/>"
result += "</picture>"
return result
}
}

View File

@ -0,0 +1,78 @@
import Foundation
/**
A version of an image with a specific size and possible different image type.
*/
struct ImageVersion {
/// The name of the image file to convert
let image: FileResource
let type: FileType
let maximumWidth: Int
let maximumHeight: Int
let quality: CGFloat
init(image: FileResource, type: FileType, maximumWidth: CGFloat, maximumHeight: CGFloat, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = Int(maximumWidth)
self.maximumHeight = Int(maximumHeight)
self.quality = quality
}
init(image: FileResource, type: FileType, maximumWidth: Int, maximumHeight: Int, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = maximumWidth
self.maximumHeight = maximumHeight
self.quality = quality
}
/// A unique id of the version for this image (not unique across images)
var versionId: String {
"\(maximumWidth)-\(maximumHeight)-\(type.fileExtension!)"
}
/// The path of the generated image version in the output folder (without leading slash)
var outputPath: String {
image.outputPath(width: maximumWidth, height: maximumHeight, type: type)
}
}
extension ImageVersion: Identifiable {
var id: String {
image.id + "-" + versionId
}
}
extension ImageVersion: Equatable {
static func == (lhs: ImageVersion, rhs: ImageVersion) -> Bool {
lhs.image.id == rhs.image.id &&
lhs.maximumWidth == rhs.maximumWidth &&
lhs.maximumHeight == rhs.maximumHeight &&
lhs.type == rhs.type
}
}
extension ImageVersion: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(image.id)
hasher.combine(maximumWidth)
hasher.combine(maximumHeight)
hasher.combine(type)
}
}
extension ImageVersion: Comparable {
static func < (lhs: ImageVersion, rhs: ImageVersion) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -39,8 +39,6 @@ extension KnownHeaderElement: Comparable {
static func < (lhs: KnownHeaderElement, rhs: KnownHeaderElement) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
extension KnownHeaderElement: CustomStringConvertible {

View File

@ -50,12 +50,19 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
results.missing(file: song.cover, containedIn: file)
continue
}
guard image.type.isImage else {
results.warning("Cover '\(song.cover)' in file \(fileId) is not an image file")
continue
}
guard let audioFile = content.file(song.file) else {
results.missing(file: song.cover, containedIn: file)
continue
}
#warning("Check if file is audio")
guard audioFile.type.isAudio else {
results.warning("Song '\(song.file)' in file \(fileId) is not an audio file")
continue
}
let coverUrl = image.absoluteUrl
let playlistItem = AudioPlayer.PlaylistItem(

View File

@ -106,7 +106,8 @@ struct PageHtmlProcessor: CommandProcessor {
results.warning("Failed to find <source> elements in inline HTML: \(error)")
return
}
checkSourceSetAttributes(sources: sources)
checkSourceAttributes(sources: sources)
}
private func checkSourceSetAttributes(sources: [Element]) {

View File

@ -29,7 +29,8 @@ final class FeedPageGenerator {
showTitle: Bool,
pageNumber: Int,
totalPages: Int,
languageButtonUrl: String) -> String {
languageButtonUrl: String,
linkPrefix: String) -> String {
var headers = content.defaultPageHeaders
var footer = ""
if posts.contains(where: { $0.images.count > 1 }) {
@ -63,7 +64,10 @@ final class FeedPageGenerator {
content += FeedEntry(data: post).content
}
if totalPages > 1 {
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
content += PostFeedPageNavigation(
linkPrefix: linkPrefix,
currentPage: pageNumber,
numberOfPages: totalPages).content
}
}
return page.content

View File

@ -6,12 +6,11 @@ final class PageGenerator {
self.content = content
}
private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> {
private func makeHeaders(requiredItems: Set<KnownHeaderElement>, results: PageGenerationResults) -> Set<HeaderElement> {
var result = content.defaultPageHeaders
for item in requiredItems {
guard let header = item.header(content: content) else {
print("Missing header \(item)")
#warning("Add warning on missing file assignment")
results.warning("Header \(item) not configured in settings")
continue
}
result.insert(header)
@ -20,6 +19,7 @@ final class PageGenerator {
}
private func makeEmptyPageContent(in language: ContentLanguage) -> String {
#warning("Configure empty page text in settings")
switch language {
case .english:
return ContentBox(
@ -56,7 +56,7 @@ final class PageGenerator {
url: tag.absoluteUrl(in: language))
}
let headers = makeHeaders(requiredItems: results.requiredHeaders)
let headers = makeHeaders(requiredItems: results.requiredHeaders, results: results)
results.require(files: headers.compactMap { $0.file })
let iconUrl = content.settings.navigation.localized(in: language).rootUrl

View File

@ -0,0 +1,74 @@
final class TagOverviewGenerator {
let content: Content
let language: ContentLanguage
let results: PageGenerationResults
init(content: Content, language: ContentLanguage, results: PageGenerationResults) {
self.content = content
self.language = language
self.results = results
}
func generatePage(tags: [Tag], overview: TagOverviewPage) {
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
let languageUrl = overview.absoluteUrl(in: language.next)
let languageButton = NavigationBar.Link(
text: language.next.rawValue,
url: languageUrl)
let localized = overview.localized(in: language)
let pageHeader = PageHeader(
language: language,
title: localized.linkPreviewTitle ?? localized.title,
description: localized.linkPreviewDescription,
iconUrl: iconUrl,
languageButton: languageButton,
links: content.navigationBar(in: language),
headers: content.defaultPageHeaders,
icons: [])
let page = GenericPage(
header: pageHeader,
additionalFooter: "") { content in
content += "<h1>\(localized.title)</h1>"
for tag in tags {
let localized = tag.localized(in: self.language)
let url = tag.absoluteUrl(in: self.language)
let title = localized.name
let description = localized.description ?? ""
let image = self.makePageImage(item: localized)
content += RelatedPageLink(
title: title,
description: description,
url: url,
image: image)
.content
}
// if totalPages > 1 {
// content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
// }
}
let fileContent = page.content
let url = overview.absoluteUrl(in: language) + ".html"
guard content.storage.write(fileContent, to: url) else {
results.unsavedOutput(url, source: .tagOverview)
return
}
}
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
item.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize
let imageSet = image.imageSet(width: size, height: size, language: language)
results.require(imageSet: imageSet)
return imageSet
}
}
}

View File

@ -160,20 +160,11 @@ final class PageContentParser {
guard !image.type.isSvg else {
return SvgImage(imagePath: path, altText: altText).content
}
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
results.require(imageSet: thumbnail)
let thumbnail = FeedEntryData.Image(
rawImagePath: path,
width: thumbnailWidth,
height: thumbnailWidth,
altText: altText)
results.requireImageSet(for: image, size: thumbnailWidth)
let largeImage = FeedEntryData.Image(
rawImagePath: path,
width: largeImageWidth,
height: largeImageWidth,
altText: altText)
results.requireImageSet(for: image, size: largeImageWidth)
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
results.require(imageSet: largeImage)
return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
@ -221,18 +212,18 @@ final class PageContentParser {
results.invalid(command: .video, markdown)
return nil
}
if case let .poster(imageId) = option {
switch option {
case .poster(let imageId):
if let image = content.image(imageId) {
results.used(file: image)
let width = 2*thumbnailWidth
let fullLink = WebsiteImage.imagePath(source: image.absoluteUrl, width: width, height: width)
return .poster(image: fullLink)
let version = image.imageVersion(width: width, height: width, type: .jpg)
results.require(image: version)
return .poster(image: version.outputPath)
} else {
results.missing(file: imageId, source: "Video command poster")
return nil // Image file not present, so skip the option
}
}
if case let .src(videoId) = option {
case .src(let videoId):
if let video = content.video(videoId) {
results.used(file: video)
let link = video.absoluteUrl
@ -241,8 +232,9 @@ final class PageContentParser {
results.missing(file: videoId, source: "Video command source")
return nil // Video file not present, so skip the option
}
default:
return option
}
return option
}
/**
@ -270,17 +262,7 @@ final class PageContentParser {
let url = page.absoluteUrl(in: language)
let title = localized.linkPreviewTitle ?? localized.title
let description = localized.linkPreviewDescription ?? ""
let image = localized.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize
results.used(file: image)
results.requireImageSet(for: image, size: size)
return RelatedPageLink.Image(
url: image.absoluteUrl,
description: image.localized(in: language),
size: size)
}
let image = makePageImage(item: localized)
return RelatedPageLink(
title: title,
@ -309,16 +291,7 @@ final class PageContentParser {
let url = tag.absoluteUrl(in: language)
let title = localized.name
let description = localized.description ?? ""
let image = localized.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize
results.requireImageSet(for: image, size: size)
return RelatedPageLink.Image(
url: image.absoluteUrl,
description: image.localized(in: language),
size: size)
}
let image = makePageImage(item: localized)
return RelatedPageLink(
title: title,
@ -328,6 +301,15 @@ final class PageContentParser {
.content
}
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
item.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize
let imageSet = image.imageSet(width: size, height: size, language: language)
results.require(imageSet: imageSet)
return imageSet
}
}
/**
Format: `![model](<file>)`
*/

View File

@ -64,7 +64,7 @@ final class PageGenerationResults: ObservableObject {
private(set) var requiredFiles: Set<FileResource>
/// The image versions required for this page
private(set) var imagesToGenerate: Set<ImageGenerationJob>
private(set) var imagesToGenerate: Set<ImageVersion>
private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
@ -127,10 +127,16 @@ final class PageGenerationResults: ObservableObject {
delegate.missing(file: file)
}
func requireImageSet(for image: FileResource, size: Int) {
let jobs = ImageGenerationJob.imageSet(for: image.id, maxWidth: size, maxHeight: size)
func require(image: ImageVersion) {
imagesToGenerate.insert(image)
used(file: image.image)
delegate.generate(image)
}
func require(imageSet: ImageSet) {
let jobs = imageSet.jobs
imagesToGenerate.formUnion(jobs)
used(file: image)
used(file: imageSet.image)
delegate.generate(jobs)
}

View File

@ -11,6 +11,10 @@ struct FeedGeneratorSource: PostListPageGeneratorSource {
false
}
var postsPerPage: Int {
content.settings.posts.postsPerPage
}
var pageTitle: String {
content.settings.localized(in: language).title
}

View File

@ -16,12 +16,8 @@ final class PostListPageGenerator {
source.content.settings.posts.contentWidth
}
private var postsPerPage: Int {
source.content.settings.posts.postsPerPage
}
private func pageUrl(in language: ContentLanguage, pageNumber: Int) -> String {
"\(source.pageUrlPrefix(for: language))/\(pageNumber).html"
"\(source.pageUrlPrefix(for: language))/\(pageNumber)"
}
func createPages(for posts: [Post]) {
@ -29,6 +25,7 @@ final class PostListPageGenerator {
guard totalCount > 0 else {
return
}
let postsPerPage = source.postsPerPage
let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up
for pageIndex in 1...numberOfPages {
@ -43,10 +40,11 @@ final class PostListPageGenerator {
let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language)
#warning("Add post link text to settings or to each post")
let linkUrl = post.linkedPage.map {
FeedEntryData.Link(
url: $0.absoluteUrl(in: language),
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
text: language == .english ? "View" : "Anzeigen")
}
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
@ -54,7 +52,10 @@ final class PostListPageGenerator {
url: tag.absoluteUrl(in: language))
}
let images = localized.images.map(createFeedImage)
let images = localized.images.map { image in
image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language)
}
images.forEach(source.results.require)
return FeedEntryData(
entryId: post.id,
@ -68,7 +69,7 @@ final class PostListPageGenerator {
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
let languageButtonUrl = pageUrl(in: language.next, pageNumber: pageIndex)
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)
let fileContent = feedPageGenerator.generatePage(
language: language,
@ -78,23 +79,15 @@ final class PostListPageGenerator {
showTitle: source.showTitle,
pageNumber: pageIndex,
totalPages: pageCount,
languageButtonUrl: languageButtonUrl)
let filePath = pageUrl(in: language, pageNumber: pageIndex)
languageButtonUrl: languageButtonUrl,
linkPrefix: "/" + source.pageUrlPrefix(for: language) + "/")
let filePath = pageUrl(in: language, pageNumber: pageIndex) + ".html"
guard save(fileContent, to: filePath) else {
source.results.unsavedOutput(filePath, source: .feed)
return
}
}
private func createFeedImage(for image: FileResource) -> FeedEntryData.Image {
source.results.requireImageSet(for: image, size: mainContentMaximumWidth)
return .init(
rawImagePath: image.absoluteUrl,
width: mainContentMaximumWidth,
height: mainContentMaximumWidth,
altText: image.localized(in: language))
}
private func save(_ content: String, to relativePath: String) -> Bool {
source.content.storage.write(content, to: relativePath)
}

View File

@ -14,4 +14,6 @@ protocol PostListPageGeneratorSource {
var pageDescription: String { get }
func pageUrlPrefix(for language: ContentLanguage) -> String
var postsPerPage: Int { get }
}

View File

@ -13,6 +13,10 @@ struct TagPageGeneratorSource: PostListPageGeneratorSource {
true
}
var postsPerPage: Int {
content.settings.posts.postsPerPage
}
var pageTitle: String {
tag.localized(in: language).name
}

View File

@ -96,7 +96,7 @@ enum VideoOption {
case .height(let height): return "height='\(height)'"
case .width(let width): return "width='\(width)'"
case .preload(let option): return "preload='\(option)'"
case .poster(let image): return "poster='\(image)'"
case .poster(let image): return "poster='/\(image)'"
case .src(let url): return "src='\(url)'"
}
}