Generate open graph meta tags

This commit is contained in:
Christoph Hagen 2025-01-15 22:04:48 +01:00
parent 4b448f3415
commit f6b868502d
21 changed files with 324 additions and 109 deletions

View File

@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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