Generate open graph meta tags
This commit is contained in:
parent
4b448f3415
commit
f6b868502d
@ -185,6 +185,8 @@
|
||||
E2FD1D282D2F2DAD00B48627 /* ErrorPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D272D2F2D9100B48627 /* ErrorPrinter.swift */; };
|
||||
E2FD1D2A2D35B74C00B48627 /* TextWithPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D292D35B74C00B48627 /* TextWithPopup.swift */; };
|
||||
E2FD1D2C2D35B76D00B48627 /* ListPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2B2D35B76D00B48627 /* ListPopup.swift */; };
|
||||
E2FD1D2E2D37180900B48627 /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2D2D37180600B48627 /* GeneralSettings.swift */; };
|
||||
E2FD1D302D37196C00B48627 /* GeneralSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2F2D37196500B48627 /* GeneralSettingsDetailView.swift */; };
|
||||
E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; };
|
||||
E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; };
|
||||
E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; };
|
||||
@ -416,6 +418,8 @@
|
||||
E2FD1D272D2F2D9100B48627 /* ErrorPrinter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPrinter.swift; sourceTree = "<group>"; };
|
||||
E2FD1D292D35B74C00B48627 /* TextWithPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithPopup.swift; sourceTree = "<group>"; };
|
||||
E2FD1D2B2D35B76D00B48627 /* ListPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPopup.swift; sourceTree = "<group>"; };
|
||||
E2FD1D2D2D37180600B48627 /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = "<group>"; };
|
||||
E2FD1D2F2D37196500B48627 /* GeneralSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsDetailView.swift; sourceTree = "<group>"; };
|
||||
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; };
|
||||
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; };
|
||||
E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; };
|
||||
@ -508,6 +512,7 @@
|
||||
E25DA53B2D0042EA00AEF16D /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2FD1D2D2D37180600B48627 /* GeneralSettings.swift */,
|
||||
E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */,
|
||||
E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */,
|
||||
E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */,
|
||||
@ -639,6 +644,7 @@
|
||||
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
|
||||
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */,
|
||||
E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */,
|
||||
E2FD1D2F2D37196500B48627 /* GeneralSettingsDetailView.swift */,
|
||||
E29D318C2D0B2E5E0051B7F4 /* Content */,
|
||||
E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */,
|
||||
E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */,
|
||||
@ -1130,6 +1136,7 @@
|
||||
E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */,
|
||||
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
|
||||
E2FE0F4F2D2BCD80002963B7 /* TagLinkCommand.swift in Sources */,
|
||||
E2FD1D302D37196C00B48627 /* GeneralSettingsDetailView.swift in Sources */,
|
||||
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
|
||||
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
||||
@ -1163,6 +1170,7 @@
|
||||
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
|
||||
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||
E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */,
|
||||
E2FD1D2E2D37180900B48627 /* GeneralSettings.swift in Sources */,
|
||||
E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */,
|
||||
E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */,
|
||||
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
|
||||
|
@ -36,6 +36,14 @@ enum HeaderElement {
|
||||
|
||||
case description(String)
|
||||
|
||||
case ogTitle(String)
|
||||
|
||||
case ogDescription(String)
|
||||
|
||||
case ogImage(String)
|
||||
|
||||
case ogUrl(String)
|
||||
|
||||
case charset
|
||||
|
||||
case viewport
|
||||
@ -44,30 +52,24 @@ enum HeaderElement {
|
||||
|
||||
var order: Int {
|
||||
switch self {
|
||||
case .charset:
|
||||
return 1
|
||||
case .robots:
|
||||
return 2
|
||||
case .viewport:
|
||||
return 3
|
||||
case .icon:
|
||||
return 10
|
||||
case .css(_, let order):
|
||||
return order
|
||||
case .js:
|
||||
return 20
|
||||
case .jsModule:
|
||||
return 30
|
||||
case .author:
|
||||
return 100
|
||||
case .title:
|
||||
return 101
|
||||
case .description:
|
||||
return 102
|
||||
case .charset: 1
|
||||
case .robots: 2
|
||||
case .viewport: 3
|
||||
case .icon: 10
|
||||
case .css(_, let order): order
|
||||
case .js: 20
|
||||
case .jsModule: 30
|
||||
case .author: 100
|
||||
case .title: 101
|
||||
case .description: 102
|
||||
case .ogTitle: 103
|
||||
case .ogDescription: 104
|
||||
case .ogImage: 105
|
||||
case .ogUrl: 106
|
||||
}
|
||||
}
|
||||
|
||||
var file: FileResource? {
|
||||
var requiredFile: FileResource? {
|
||||
switch self {
|
||||
case .icon(let file, _, _):
|
||||
return file
|
||||
@ -113,6 +115,14 @@ extension HeaderElement {
|
||||
return "<title>\(title)</title>"
|
||||
case .description(let description):
|
||||
return "<meta name='description' content=\"\(description)\">"
|
||||
case .ogTitle(let title):
|
||||
return "<meta property='og:title' content='\(title)'>"
|
||||
case .ogDescription(let description):
|
||||
return "<meta property='og:description' content='\(description)'>"
|
||||
case .ogImage(let image):
|
||||
return "<meta property='og:image' content='\(image)'>"
|
||||
case .ogUrl(let url):
|
||||
return "<meta property='og:url' content='\(url)'>"
|
||||
case .charset:
|
||||
return "<meta charset='utf-8' />"
|
||||
case .viewport:
|
||||
@ -141,6 +151,14 @@ extension HeaderElement: CustomStringConvertible {
|
||||
return "title"
|
||||
case .description:
|
||||
return "description"
|
||||
case .ogTitle:
|
||||
return "og:title"
|
||||
case .ogDescription:
|
||||
return "og:description"
|
||||
case .ogImage:
|
||||
return "og:image"
|
||||
case .ogUrl:
|
||||
return "og:url"
|
||||
case .charset:
|
||||
return "charset"
|
||||
case .viewport:
|
||||
|
@ -24,9 +24,11 @@ final class FeedPageGenerator {
|
||||
|
||||
func generatePage(language: ContentLanguage,
|
||||
posts: [FeedEntryData],
|
||||
title: String,
|
||||
title: String?,
|
||||
description: String?,
|
||||
showTitle: Bool,
|
||||
image: FileResource?,
|
||||
pageUrl: String,
|
||||
pageTitle: String?,
|
||||
pageNumber: Int,
|
||||
totalPages: Int,
|
||||
languageButtonUrl: String,
|
||||
@ -44,10 +46,14 @@ final class FeedPageGenerator {
|
||||
text: language.next.rawValue,
|
||||
url: languageButtonUrl)
|
||||
|
||||
let imageUrl = image?.linkPreviewImage(results: results)
|
||||
|
||||
let pageHeader = PageHeader(
|
||||
language: language,
|
||||
title: title,
|
||||
title: title ?? pageTitle,
|
||||
description: description,
|
||||
image: imageUrl,
|
||||
pageUrl: pageUrl,
|
||||
iconUrl: iconUrl,
|
||||
languageButton: languageButton,
|
||||
links: content.navigationBar(in: language),
|
||||
@ -57,8 +63,8 @@ final class FeedPageGenerator {
|
||||
let page = GenericPage(
|
||||
header: pageHeader,
|
||||
additionalFooter: footer) { content in
|
||||
if showTitle {
|
||||
content += "<h1 class='separated-headline'>\(title)</h1>"
|
||||
if let pageTitle {
|
||||
content += "<h1 class='separated-headline'>\(pageTitle)</h1>"
|
||||
}
|
||||
for post in posts {
|
||||
content += FeedEntry(data: post).content
|
||||
|
@ -25,7 +25,7 @@ final class PageGenerator {
|
||||
text: settings.emptyPageText).content
|
||||
}
|
||||
|
||||
func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? {
|
||||
func generate(page: Page, language: ContentLanguage, results: PageGenerationResults, pageUrl: String) -> String? {
|
||||
let contentGenerator = PageContentParser(
|
||||
content: content,
|
||||
language: language, results: results)
|
||||
@ -51,7 +51,7 @@ final class PageGenerator {
|
||||
}
|
||||
|
||||
let headers = makeHeaders(requiredItems: results.requiredHeaders, results: results)
|
||||
results.require(files: headers.compactMap { $0.file })
|
||||
results.require(files: headers.compactMap { $0.requiredFile })
|
||||
|
||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
||||
let languageUrl = page.absoluteUrl(in: language.next)
|
||||
@ -59,10 +59,14 @@ final class PageGenerator {
|
||||
text: language.next.rawValue,
|
||||
url: languageUrl)
|
||||
|
||||
let imageUrl = localized.linkPreview.image?.linkPreviewImage(results: results)
|
||||
|
||||
let pageHeader = PageHeader(
|
||||
language: language,
|
||||
title: localized.linkPreview.title ?? localized.title,
|
||||
description: localized.linkPreview.description,
|
||||
image: imageUrl,
|
||||
pageUrl: pageUrl,
|
||||
iconUrl: iconUrl,
|
||||
languageButton: languageButton,
|
||||
links: content.navigationBar(in: language),
|
||||
|
@ -34,22 +34,45 @@ private struct TagHeaderContent {
|
||||
|
||||
let description: String?
|
||||
|
||||
let image: String?
|
||||
|
||||
let iconUrl: String
|
||||
|
||||
let links: [NavigationBar.Link]
|
||||
|
||||
let headers: Set<HeaderElement>
|
||||
|
||||
let baseUrl: String
|
||||
let basePath: String
|
||||
|
||||
let websiteUrl: String
|
||||
|
||||
let localizedBaseUrl: String
|
||||
|
||||
private func url(pageNumber: Int) -> String {
|
||||
baseUrl + "/\(pageNumber)"
|
||||
/**
|
||||
The path (relative to the output folder) for the html file
|
||||
*/
|
||||
func filePath(pageNumber: Int) -> String {
|
||||
basePath + "/\(pageNumber).html"
|
||||
}
|
||||
|
||||
func fileUrl(pageNumber: Int) -> String {
|
||||
url(pageNumber: pageNumber) + ".html"
|
||||
/**
|
||||
The absolute url (without domain) for the language button of the page
|
||||
*/
|
||||
private func languageButtonUrl(page: Int) -> String {
|
||||
guard page > 1 else {
|
||||
return localizedBaseUrl
|
||||
}
|
||||
return localizedBaseUrl + "/\(page)"
|
||||
}
|
||||
|
||||
/**
|
||||
The full url (including domain) to the page.
|
||||
*/
|
||||
private func pageUrl(page: Int) -> String {
|
||||
guard page > 1 else {
|
||||
return websiteUrl + basePath
|
||||
}
|
||||
return websiteUrl + basePath + "/\(page)"
|
||||
}
|
||||
|
||||
func pageHeader(pageNumber: Int) -> PageHeader {
|
||||
@ -57,10 +80,12 @@ private struct TagHeaderContent {
|
||||
language: language,
|
||||
title: title,
|
||||
description: description,
|
||||
image: image,
|
||||
pageUrl: pageUrl(page: pageNumber),
|
||||
iconUrl: iconUrl,
|
||||
languageButton: .init(
|
||||
text: language.next.rawValue,
|
||||
url: localizedBaseUrl + "/\(pageNumber)"),
|
||||
url: languageButtonUrl(page: pageNumber)),
|
||||
links: links,
|
||||
headers: headers,
|
||||
icons: [])
|
||||
@ -83,14 +108,19 @@ final class TagOverviewGenerator {
|
||||
|
||||
func generatePages(tags: [Tag], overview: Tag) {
|
||||
let localized = overview.localized(in: language)
|
||||
|
||||
let imageUrl = localized.linkPreview.image?.linkPreviewImage(results: results)
|
||||
|
||||
let header = TagHeaderContent(
|
||||
language: language,
|
||||
title: localized.linkPreview.title ?? localized.title,
|
||||
description: localized.linkPreview.description,
|
||||
image: imageUrl,
|
||||
iconUrl: content.settings.navigation.localized(in: language).rootUrl,
|
||||
links: content.navigationBar(in: language),
|
||||
headers: content.postPageHeaders,
|
||||
baseUrl: overview.absoluteUrl(in: language),
|
||||
basePath: overview.absoluteUrl(in: language),
|
||||
websiteUrl: content.settings.general.url,
|
||||
localizedBaseUrl: overview.absoluteUrl(in: language.next))
|
||||
|
||||
// Sort tags by title
|
||||
@ -140,16 +170,16 @@ final class TagOverviewGenerator {
|
||||
}
|
||||
if totalPages > 1 {
|
||||
content += PostFeedPageNavigation(
|
||||
linkPrefix: header.baseUrl,
|
||||
linkPrefix: header.basePath,
|
||||
currentPage: pageNumber,
|
||||
numberOfPages: totalPages).content
|
||||
}
|
||||
}
|
||||
let fileContent = page.content
|
||||
let url = header.fileUrl(pageNumber: pageNumber)
|
||||
let filePath = header.filePath(pageNumber: pageNumber)
|
||||
|
||||
guard content.storage.write(fileContent, to: url) else {
|
||||
results.unsavedOutput(url, source: .tagOverview)
|
||||
guard content.storage.write(fileContent, to: filePath) else {
|
||||
results.unsavedOutput(filePath, source: .tagOverview)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,28 @@ struct FeedGeneratorSource: PostListPageGeneratorSource {
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
var showTitle: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
var postsPerPage: Int {
|
||||
content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
content.settings.posts.localized(in: language).title
|
||||
var pageTitle: String? {
|
||||
nil // Don't show title in page
|
||||
}
|
||||
|
||||
var pageDescription: String {
|
||||
content.settings.posts.localized(in: language).description
|
||||
private var linkPreview: LinkPreview {
|
||||
content.settings.posts.localized(in: language).linkPreview
|
||||
}
|
||||
|
||||
var linkTitle: String? {
|
||||
linkPreview.title
|
||||
}
|
||||
|
||||
var linkDescription: String? {
|
||||
linkPreview.description
|
||||
}
|
||||
|
||||
var linkImage: FileResource? {
|
||||
linkPreview.image
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,15 @@ final class PostListPageGenerator {
|
||||
}
|
||||
|
||||
private func pageUrl(in language: ContentLanguage, pageNumber: Int) -> String {
|
||||
"\(source.pageUrlPrefix(for: language))/\(pageNumber)"
|
||||
let base = source.content.settings.general.url + source.pageUrlPrefix(for: language)
|
||||
guard pageNumber > 1 else {
|
||||
return base
|
||||
}
|
||||
return base + "/\(pageNumber)"
|
||||
}
|
||||
|
||||
private func filePath(in language: ContentLanguage, pageNumber: Int) -> String {
|
||||
"\(source.pageUrlPrefix(for: language))/\(pageNumber).html"
|
||||
}
|
||||
|
||||
func createPages(for posts: [Post]) {
|
||||
@ -82,18 +90,23 @@ final class PostListPageGenerator {
|
||||
// Includes leading slash
|
||||
let languageButtonUrl = pageUrl(in: language.next, pageNumber: pageIndex)
|
||||
|
||||
// Includes leading slash
|
||||
let pageUrl = pageUrl(in: language, pageNumber: pageIndex)
|
||||
|
||||
let fileContent = feedPageGenerator.generatePage(
|
||||
language: language,
|
||||
posts: posts,
|
||||
title: source.pageTitle,
|
||||
description: source.pageDescription,
|
||||
showTitle: source.showTitle,
|
||||
title: source.linkTitle,
|
||||
description: source.linkDescription,
|
||||
image: source.linkImage,
|
||||
pageUrl: pageUrl,
|
||||
pageTitle: source.pageTitle,
|
||||
pageNumber: pageIndex,
|
||||
totalPages: pageCount,
|
||||
languageButtonUrl: languageButtonUrl,
|
||||
linkPrefix: source.pageUrlPrefix(for: language))
|
||||
// Includes leading slash
|
||||
let filePath = pageUrl(in: language, pageNumber: pageIndex) + ".html"
|
||||
|
||||
let filePath = self.filePath(in: language, pageNumber: pageIndex)
|
||||
guard save(fileContent, to: filePath) else {
|
||||
source.results.unsavedOutput(filePath, source: .feed)
|
||||
return
|
||||
|
@ -7,11 +7,13 @@ protocol PostListPageGeneratorSource {
|
||||
|
||||
var results: PageGenerationResults { get }
|
||||
|
||||
var showTitle: Bool { get }
|
||||
var pageTitle: String? { get }
|
||||
|
||||
var pageTitle: String { get }
|
||||
var linkTitle: String? { get }
|
||||
|
||||
var pageDescription: String { get }
|
||||
var linkDescription: String? { get }
|
||||
|
||||
var linkImage: FileResource? { get }
|
||||
|
||||
/**
|
||||
The url to the page, including a leading slash
|
||||
|
@ -9,20 +9,28 @@ struct TagPageGeneratorSource: PostListPageGeneratorSource {
|
||||
|
||||
let tag: Tag
|
||||
|
||||
var showTitle: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var postsPerPage: Int {
|
||||
content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
tag.localized(in: language).name
|
||||
var pageTitle: String? {
|
||||
linkPreview.title ?? tag.localized(in: language).name
|
||||
}
|
||||
|
||||
var pageDescription: String {
|
||||
tag.localized(in: language).linkPreview.description ?? ""
|
||||
private var linkPreview: LinkPreview {
|
||||
tag.localized(in: language).linkPreview
|
||||
}
|
||||
|
||||
var linkTitle: String? {
|
||||
linkPreview.title
|
||||
}
|
||||
|
||||
var linkDescription: String? {
|
||||
linkPreview.description
|
||||
}
|
||||
|
||||
var linkImage: FileResource? {
|
||||
linkPreview.image
|
||||
}
|
||||
|
||||
func pageUrlPrefix(for language: ContentLanguage) -> String {
|
||||
|
@ -281,13 +281,15 @@ extension Content {
|
||||
|
||||
results.require(files: page.requiredFiles)
|
||||
|
||||
guard let content = pageGenerator.generate(page: page, language: language, results: results) else {
|
||||
let relativePageUrl = page.absoluteUrl(in: language)
|
||||
let filePath = relativePageUrl + ".html"
|
||||
let pageUrl = settings.general.url + relativePageUrl
|
||||
|
||||
guard let content = pageGenerator.generate(page: page, language: language, results: results, pageUrl: pageUrl) else {
|
||||
print("Failed to generate page \(page.id) in language \(language)")
|
||||
return
|
||||
}
|
||||
|
||||
let pageUrl = page.absoluteUrl(in: language)
|
||||
let filePath = pageUrl + ".html"
|
||||
guard storage.write(content, to: filePath) else {
|
||||
print("Failed to save page \(page.id)")
|
||||
return
|
||||
|
@ -246,6 +246,20 @@ final class FileResource: Item, LocalizedItem {
|
||||
.init(image: self, type: type, maximumWidth: width, maximumHeight: height)
|
||||
}
|
||||
|
||||
func linkPreviewImage(results: PageGenerationResults) -> String {
|
||||
let type: FileType
|
||||
switch self.type {
|
||||
case .jpg, .png, .gif: type = self.type
|
||||
default: type = .jpg
|
||||
}
|
||||
let version = imageVersion(
|
||||
width: content.settings.general.linkPreviewImageWidth,
|
||||
height: content.settings.general.linkPreviewImageHeight,
|
||||
type: type)
|
||||
results.require(image: version)
|
||||
return content.settings.general.url + version.outputPath
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
func removeFileFromOutputFolder() {
|
||||
|
42
CHDataManagement/Model/Settings/GeneralSettings.swift
Normal file
42
CHDataManagement/Model/Settings/GeneralSettings.swift
Normal file
@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
|
||||
final class GeneralSettings: ObservableObject {
|
||||
|
||||
@Published
|
||||
var url: String
|
||||
|
||||
@Published
|
||||
var linkPreviewImageWidth: Int
|
||||
|
||||
@Published
|
||||
var linkPreviewImageHeight: Int
|
||||
|
||||
init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int) {
|
||||
self.url = url
|
||||
self.linkPreviewImageWidth = linkPreviewImageWidth
|
||||
self.linkPreviewImageHeight = linkPreviewImageHeight
|
||||
}
|
||||
}
|
||||
|
||||
extension GeneralSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(
|
||||
url: data.url,
|
||||
linkPreviewImageWidth: data.linkPreviewImageWidth,
|
||||
linkPreviewImageHeight: data.linkPreviewImageHeight)
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
url: url,
|
||||
linkPreviewImageWidth: linkPreviewImageWidth,
|
||||
linkPreviewImageHeight: linkPreviewImageHeight)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let url: String
|
||||
let linkPreviewImageWidth: Int
|
||||
let linkPreviewImageHeight: Int
|
||||
}
|
||||
}
|
@ -2,13 +2,7 @@ import Foundation
|
||||
|
||||
final class LocalizedPostSettings: ObservableObject {
|
||||
|
||||
/// The page title for the post feed
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
/// The page description for the post feed
|
||||
@Published
|
||||
var description: String
|
||||
var linkPreview: LinkPreview
|
||||
|
||||
/// The path to the feed in the final website, appended with the page number
|
||||
@Published
|
||||
@ -22,11 +16,10 @@ final class LocalizedPostSettings: ObservableObject {
|
||||
@Published
|
||||
var defaultPageLinkText: String
|
||||
|
||||
init(title: String, description: String, feedUrlPrefix: String, defaultPageLinkText: String) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
init(feedUrlPrefix: String, defaultPageLinkText: String, linkPreview: LinkPreview) {
|
||||
self.feedUrlPrefix = feedUrlPrefix
|
||||
self.defaultPageLinkText = defaultPageLinkText
|
||||
self.linkPreview = linkPreview
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,26 +27,23 @@ final class LocalizedPostSettings: ObservableObject {
|
||||
|
||||
extension LocalizedPostSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
title: data.feedTitle,
|
||||
description: data.feedDescription,
|
||||
feedUrlPrefix: data.feedUrlPrefix,
|
||||
defaultPageLinkText: data.defaultPageLinkText)
|
||||
defaultPageLinkText: data.defaultPageLinkText,
|
||||
linkPreview: .init(context: context, data: data.linkPreview))
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
feedTitle: title,
|
||||
feedDescription: description,
|
||||
feedUrlPrefix: feedUrlPrefix,
|
||||
defaultPageLinkText: defaultPageLinkText)
|
||||
defaultPageLinkText: defaultPageLinkText,
|
||||
linkPreview: linkPreview.data)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let feedTitle: String
|
||||
let feedDescription: String
|
||||
let feedUrlPrefix: String
|
||||
let defaultPageLinkText: String
|
||||
let linkPreview: LinkPreview.Data
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,8 @@ extension PostSettings {
|
||||
swiperCssFile: data.swiperCssFile.map(context.file),
|
||||
swiperJsFile: data.swiperJsFile.map(context.file),
|
||||
defaultCssFile: data.defaultCssFile.map(context.file),
|
||||
german: .init(data: data.german),
|
||||
english: .init(data: data.english))
|
||||
german: .init(context: context, data: data.german),
|
||||
english: .init(context: context, data: data.english))
|
||||
}
|
||||
|
||||
var data: PostSettings.Data {
|
||||
|
@ -2,6 +2,9 @@ import Foundation
|
||||
|
||||
final class Settings: ObservableObject {
|
||||
|
||||
@Published
|
||||
var general: GeneralSettings
|
||||
|
||||
@Published
|
||||
var paths: PathSettings
|
||||
|
||||
@ -18,11 +21,13 @@ final class Settings: ObservableObject {
|
||||
@Published
|
||||
var audioPlayer: AudioPlayerSettings
|
||||
|
||||
init(paths: PathSettings,
|
||||
init(general: GeneralSettings,
|
||||
paths: PathSettings,
|
||||
navigation: NavigationSettings,
|
||||
posts: PostSettings,
|
||||
pages: PageSettings,
|
||||
audioPlayer: AudioPlayerSettings) {
|
||||
self.general = general
|
||||
self.paths = paths
|
||||
self.navigation = navigation
|
||||
self.posts = posts
|
||||
@ -43,6 +48,7 @@ extension Settings {
|
||||
|
||||
convenience init(context: LoadingContext, data: Settings.Data) {
|
||||
self.init(
|
||||
general: .init(data: data.general),
|
||||
paths: .init(data: data.paths),
|
||||
navigation: .init(context: context, data: data.navigation),
|
||||
posts: .init(context: context, data: data.posts),
|
||||
@ -52,6 +58,7 @@ extension Settings {
|
||||
|
||||
func data(tagOverview: Tag?) -> Data {
|
||||
.init(
|
||||
general: general.data,
|
||||
paths: paths.data,
|
||||
navigation: navigation.data,
|
||||
posts: posts.data,
|
||||
@ -61,6 +68,7 @@ extension Settings {
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let general: GeneralSettings.Data
|
||||
let paths: PathSettings.Data
|
||||
let navigation: NavigationSettings.Data
|
||||
let posts: PostSettings.Data
|
||||
@ -73,6 +81,7 @@ extension Settings {
|
||||
extension Settings {
|
||||
|
||||
static let `default`: Settings = .init(
|
||||
general: .default,
|
||||
paths: .default,
|
||||
navigation: .default,
|
||||
posts: .default,
|
||||
@ -80,6 +89,14 @@ extension Settings {
|
||||
audioPlayer: .default)
|
||||
}
|
||||
|
||||
extension GeneralSettings {
|
||||
|
||||
static let `default`: GeneralSettings = .init(
|
||||
url: "https://example.com",
|
||||
linkPreviewImageWidth: 1200,
|
||||
linkPreviewImageHeight: 630)
|
||||
}
|
||||
|
||||
extension AudioPlayerSettings {
|
||||
|
||||
static let `default`: AudioPlayerSettings = .init(
|
||||
@ -100,15 +117,18 @@ extension PostSettings {
|
||||
swiperJsFile: nil,
|
||||
defaultCssFile: nil,
|
||||
german: .init(
|
||||
title: "Beiträge",
|
||||
description: "Alle Beiträge",
|
||||
feedUrlPrefix: "blog",
|
||||
defaultPageLinkText: "Anzeigen"),
|
||||
defaultPageLinkText: "Anzeigen",
|
||||
linkPreview: .init(
|
||||
title: "Beiträge",
|
||||
description: "Alle Beiträge")),
|
||||
english: .init(
|
||||
title: "Blog posts",
|
||||
description: "All blog posts",
|
||||
feedUrlPrefix: "blog",
|
||||
defaultPageLinkText: "View"))
|
||||
defaultPageLinkText: "View",
|
||||
linkPreview: .init(
|
||||
title: "Blog posts",
|
||||
description: "All blog posts"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,10 @@ struct PageHeader: HtmlProducer {
|
||||
|
||||
init(
|
||||
language: ContentLanguage,
|
||||
title: String,
|
||||
title: String?,
|
||||
description: String?,
|
||||
image: String?,
|
||||
pageUrl: String,
|
||||
iconUrl: String,
|
||||
languageButton: NavigationBar.Link,
|
||||
links: [NavigationBar.Link],
|
||||
@ -29,10 +31,18 @@ struct PageHeader: HtmlProducer {
|
||||
self.icons = icons
|
||||
|
||||
var headers = headers
|
||||
headers.insert(.title(title))
|
||||
if let title {
|
||||
headers.insert(.title(title))
|
||||
headers.insert(.ogTitle(title))
|
||||
}
|
||||
if let description {
|
||||
headers.insert(.description(description))
|
||||
headers.insert(.ogDescription(description))
|
||||
}
|
||||
if let image {
|
||||
headers.insert(.ogImage(image))
|
||||
}
|
||||
headers.insert(.ogUrl(pageUrl))
|
||||
self.headers = headers.sorted()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GeneralSettingsDetailView: View {
|
||||
|
||||
@ObservedObject
|
||||
var generalSettings: GeneralSettings
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
DetailTitle(title: "General",
|
||||
text: "General settings for the webpage")
|
||||
|
||||
StringPropertyView(
|
||||
title: "Website URL",
|
||||
text: $generalSettings.url,
|
||||
footer: "The base path where the website is deployed, to generate absolute links")
|
||||
|
||||
IntegerPropertyView(
|
||||
title: "Link Preview Image Width",
|
||||
value: $generalSettings.linkPreviewImageWidth,
|
||||
footer: "The maximum width of a link preview image")
|
||||
|
||||
IntegerPropertyView(
|
||||
title: "Link Preview Image Height",
|
||||
value: $generalSettings.linkPreviewImageHeight,
|
||||
footer: "The maximum height of a link preview image")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ struct GenerationDetailView: View {
|
||||
|
||||
var body: some View {
|
||||
switch section {
|
||||
case .general:
|
||||
GeneralSettingsDetailView(generalSettings: content.settings.general)
|
||||
case .folders:
|
||||
PathSettingsView()
|
||||
case .navigationBar:
|
||||
|
@ -5,6 +5,8 @@ struct LocalizedPostFeedSettingsView: View {
|
||||
@ObservedObject
|
||||
var settings: LocalizedPostSettings
|
||||
|
||||
let transferImage: (language: ContentLanguage, image: FileResource)?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
StringPropertyView(
|
||||
@ -12,25 +14,19 @@ struct LocalizedPostFeedSettingsView: View {
|
||||
text: $settings.defaultPageLinkText,
|
||||
footer: "The text to display when linking from a post to a page. Each post can provide a custom text.")
|
||||
|
||||
StringPropertyView(
|
||||
title: "Title",
|
||||
text: $settings.title,
|
||||
footer: "The title of all post feed pages.")
|
||||
|
||||
StringPropertyView(
|
||||
title: "URL prefix",
|
||||
text: $settings.feedUrlPrefix,
|
||||
footer: "The prefix to generate the urls for all post feed pages.")
|
||||
|
||||
TextFieldPropertyView(
|
||||
title: "Description",
|
||||
text: $settings.description,
|
||||
footer: "The description of all post feed pages.")
|
||||
LinkPreviewDetailView(linkPreview: settings.linkPreview, fallbackTitle: nil, transferImage: transferImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedPostFeedSettingsView(settings: PostSettings.default.english)
|
||||
.padding()
|
||||
LocalizedPostFeedSettingsView(
|
||||
settings: PostSettings.default.english,
|
||||
transferImage: nil)
|
||||
.padding()
|
||||
}
|
||||
|
@ -8,6 +8,12 @@ struct PostFeedSettingsView: View {
|
||||
@ObservedObject
|
||||
var postSettings: PostSettings
|
||||
|
||||
private var transferImage: (language: ContentLanguage, image: FileResource)? {
|
||||
postSettings.localized(in: language.next).linkPreview.image.map {
|
||||
(language.next, $0)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
@ -40,7 +46,8 @@ struct PostFeedSettingsView: View {
|
||||
selectedFile: $postSettings.swiperJsFile)
|
||||
|
||||
LocalizedPostFeedSettingsView(
|
||||
settings: postSettings.localized(in: language))
|
||||
settings: postSettings.localized(in: language),
|
||||
transferImage: transferImage)
|
||||
.id(language)
|
||||
}
|
||||
.padding()
|
||||
|
@ -2,6 +2,8 @@ import SFSafeSymbols
|
||||
|
||||
enum SettingsSection: String {
|
||||
|
||||
case general = "General"
|
||||
|
||||
case folders = "Folders"
|
||||
|
||||
case navigationBar = "Navigation Bar"
|
||||
@ -20,6 +22,7 @@ extension SettingsSection {
|
||||
|
||||
var icon: SFSymbol {
|
||||
switch self {
|
||||
case .general: return .noteText
|
||||
case .folders: return .folder
|
||||
case .navigationBar: return .menubarArrowUpRectangle
|
||||
case .postFeed: return .rectangleGrid1x2
|
||||
|
Loading…
x
Reference in New Issue
Block a user