Improve plugin and Fetcher
This commit is contained in:
parent
d9df4947f4
commit
b598b0d5af
9 changed files with 522 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
/CSharpGhostImporter/.vs/*
|
/CSharpGhostImporter/.vs/*
|
||||||
/CSharpGhostImporter/bin/*
|
/CSharpGhostImporter/bin/*
|
||||||
/CSharpGhostImporter/obj/*
|
/CSharpGhostImporter/obj/*
|
||||||
|
/0
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using HtmlAgilityPack;
|
||||||
using wp2ghost;
|
using wp2ghost;
|
||||||
|
|
||||||
class Program
|
class Program
|
||||||
|
|
@ -22,6 +25,7 @@ class Program
|
||||||
ImageManager.Init(BaseDir);
|
ImageManager.Init(BaseDir);
|
||||||
PostFetcher.Init(BaseDir);
|
PostFetcher.Init(BaseDir);
|
||||||
GhostExporter.Init(BaseDir);
|
GhostExporter.Init(BaseDir);
|
||||||
|
LinkDecisionManager.Init(BaseDir);
|
||||||
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
|
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
|
||||||
|
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
|
|
@ -57,8 +61,12 @@ class Program
|
||||||
case "7":
|
case "7":
|
||||||
await GhostMenuAsync();
|
await GhostMenuAsync();
|
||||||
break;
|
break;
|
||||||
|
case "8":
|
||||||
|
LinkDecisionManager.ShowInfo();
|
||||||
|
break;
|
||||||
case "0":
|
case "0":
|
||||||
ImageManager.SaveCache();
|
ImageManager.SaveCache();
|
||||||
|
LinkDecisionManager.Save();
|
||||||
running = false;
|
running = false;
|
||||||
Console.WriteLine("Programm beendet. Auf Wiedersehen!");
|
Console.WriteLine("Programm beendet. Auf Wiedersehen!");
|
||||||
break;
|
break;
|
||||||
|
|
@ -89,6 +97,7 @@ class Program
|
||||||
Console.WriteLine(" 5 - Abgefragte Objekte zurücksetzen");
|
Console.WriteLine(" 5 - Abgefragte Objekte zurücksetzen");
|
||||||
Console.WriteLine(" 6 - Bilder lokal löschen");
|
Console.WriteLine(" 6 - Bilder lokal löschen");
|
||||||
Console.WriteLine(" 7 - Beiträge direkt an Ghost senden");
|
Console.WriteLine(" 7 - Beiträge direkt an Ghost senden");
|
||||||
|
Console.WriteLine(" 8 - Gespeicherte Link-Entscheidungen anzeigen");
|
||||||
Console.WriteLine(" 0 - Beenden");
|
Console.WriteLine(" 0 - Beenden");
|
||||||
Console.WriteLine("-----------------------------------------");
|
Console.WriteLine("-----------------------------------------");
|
||||||
Console.Write("Auswahl: ");
|
Console.Write("Auswahl: ");
|
||||||
|
|
@ -352,6 +361,9 @@ class Program
|
||||||
Console.Write("HTML-Inhalt: ");
|
Console.Write("HTML-Inhalt: ");
|
||||||
string? html = Console.ReadLine()?.Trim();
|
string? html = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
// Links validieren und ggf. korrigieren
|
||||||
|
html = await ValidateAndFixLinksAsync(html);
|
||||||
|
|
||||||
Console.Write("Status (published/draft) [published]: ");
|
Console.Write("Status (published/draft) [published]: ");
|
||||||
string? status = Console.ReadLine()?.Trim();
|
string? status = Console.ReadLine()?.Trim();
|
||||||
if (string.IsNullOrEmpty(status)) status = "published";
|
if (string.IsNullOrEmpty(status)) status = "published";
|
||||||
|
|
@ -459,6 +471,233 @@ class Program
|
||||||
Console.WriteLine($" Hochgeladen: {uploaded}/{cachedFiles.Count}");
|
Console.WriteLine($" Hochgeladen: {uploaded}/{cachedFiles.Count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// HTML-Links validieren und bei Bedarf korrigieren
|
||||||
|
// =========================================================================
|
||||||
|
private static async Task<string> ValidateAndFixLinksAsync(string? html)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(html)) return html ?? "";
|
||||||
|
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(html);
|
||||||
|
|
||||||
|
var invalidLinks = new List<(string text, string href, HtmlNode node)>();
|
||||||
|
|
||||||
|
// Alle <a>-Tags suchen
|
||||||
|
var anchors = doc.DocumentNode.SelectNodes("//a[@href]");
|
||||||
|
if (anchors == null) return html;
|
||||||
|
|
||||||
|
foreach (var anchor in anchors)
|
||||||
|
{
|
||||||
|
string href = anchor.GetAttributeValue("href", "").Trim();
|
||||||
|
string text = anchor.InnerText.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(href)) continue;
|
||||||
|
|
||||||
|
// Prüfe, ob URL valide ist (inkl. Reichbarkeitsprüfung)
|
||||||
|
if (!await IsValidUrlAsync(href, checkReachability: true))
|
||||||
|
{
|
||||||
|
invalidLinks.Add((text, href, anchor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidLinks.Count == 0) return html;
|
||||||
|
|
||||||
|
// User über invaliden Links informieren und zur Korrektur auffordern
|
||||||
|
Console.WriteLine($"\n {invalidLinks.Count} ungültiger Link(s) gefunden:");
|
||||||
|
for (int i = 0; i < invalidLinks.Count; i++)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [{i + 1}] Text: \"{invalidLinks[i].text}\" | Link: {invalidLinks[i].href}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("\nOptionen pro Link:");
|
||||||
|
Console.WriteLine(" d = Link löschen (Text bleibt, <a>-Tag entfernt)");
|
||||||
|
Console.WriteLine(" e = Link bearbeiten (neue URL eingeben)");
|
||||||
|
Console.WriteLine(" Enter = Link so übernehmen (beibehalten)");
|
||||||
|
|
||||||
|
// Für jeden invaliden Link die User-Entscheidung einholen
|
||||||
|
foreach (var (linkText, linkHref, anchorNode) in invalidLinks)
|
||||||
|
{
|
||||||
|
// 1. Memory: Frühere Entscheidung suchen und vorschlagen
|
||||||
|
var previous = LinkDecisionManager.GetPreviousDecision(linkText, linkHref);
|
||||||
|
|
||||||
|
Console.WriteLine($"\n ---------------------------------------------");
|
||||||
|
Console.WriteLine($" Link-Text : \"{linkText}\"");
|
||||||
|
Console.WriteLine($" Aktueller Link : {linkHref}");
|
||||||
|
|
||||||
|
if (previous != null)
|
||||||
|
{
|
||||||
|
string prevInfo = previous.Decision switch
|
||||||
|
{
|
||||||
|
"delete" => "LÖSCHEN",
|
||||||
|
"edit" => $"ERSETZEN durch '{previous.NewUrl}'",
|
||||||
|
_ => "BEIBEHALTEN"
|
||||||
|
};
|
||||||
|
Console.WriteLine($" [Memory] Frühere Entscheidung: {prevInfo} (vom {previous.Timestamp:dd.MM.yyyy HH:mm})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Browser-Öffnen anbieten (da Link nicht erreichbar ist)
|
||||||
|
string searchQuery = Uri.EscapeDataString(linkText);
|
||||||
|
string searchUrl = $"https://www.google.com/search?q={searchQuery}";
|
||||||
|
Console.WriteLine($" [Suche] Fallstrick: Link nicht erreichbar. Browser-Suche verfügbar.");
|
||||||
|
|
||||||
|
Console.Write($" Optionen: [Enter=übernehmen" + (previous != null ? $" / v=Vorschlag '{previous.Decision}' anwenden" : "") + " / d=löschen / e=bearbeiten / b=Browser öffnen]: ");
|
||||||
|
string? choice = Console.ReadLine()?.Trim()?.ToLowerInvariant();
|
||||||
|
|
||||||
|
// Browser öffnen
|
||||||
|
if (choice == "b")
|
||||||
|
{
|
||||||
|
Console.WriteLine($" -> Öffne Browser mit Suche nach: \"{linkText}\"");
|
||||||
|
OpenBrowser(searchUrl);
|
||||||
|
// Nach dem Browser-Öffnen erneut nach der Entscheidung fragen
|
||||||
|
Console.Write($" Jetzt entscheiden? [Enter=übernehmen / d=löschen / e=bearbeiten]: ");
|
||||||
|
choice = Console.ReadLine()?.Trim()?.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory-Vorschlag anwenden
|
||||||
|
if (choice == "v" && previous != null)
|
||||||
|
{
|
||||||
|
ApplyDecision(anchorNode, previous, linkText, linkHref, ref html, doc);
|
||||||
|
Console.WriteLine($" -> Frühere Entscheidung angewendet: {previous.Decision}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice == "d")
|
||||||
|
{
|
||||||
|
// Link löschen: <a>-Tag entfernen, Text erhalten
|
||||||
|
var parent = anchorNode.ParentNode;
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
parent.ReplaceChild(HtmlNode.CreateNode(anchorNode.InnerText), anchorNode);
|
||||||
|
}
|
||||||
|
Console.WriteLine($" -> Link gelöscht");
|
||||||
|
LinkDecisionManager.SaveDecision(linkText, linkHref, "delete");
|
||||||
|
}
|
||||||
|
else if (choice == "e")
|
||||||
|
{
|
||||||
|
// Link bearbeiten: neue URL eingeben
|
||||||
|
Console.Write(" Neue URL eingeben: ");
|
||||||
|
string? newUrl = Console.ReadLine()?.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(newUrl))
|
||||||
|
{
|
||||||
|
anchorNode.SetAttributeValue("href", newUrl);
|
||||||
|
Console.WriteLine($" -> Auf '{newUrl}' aktualisiert");
|
||||||
|
LinkDecisionManager.SaveDecision(linkText, linkHref, "edit", newUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($" -> Beibehalten");
|
||||||
|
LinkDecisionManager.SaveDecision(linkText, linkHref, "keep");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($" -> Beibehalten");
|
||||||
|
LinkDecisionManager.SaveDecision(linkText, linkHref, "keep");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.DocumentNode.OuterHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hilfsmethoden für Link-Entscheidungen
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wendet eine gespeicherte (frühere) Entscheidung auf den Link an.
|
||||||
|
/// </summary>
|
||||||
|
private static void ApplyDecision(HtmlNode anchorNode, LinkDecision decision, string linkText, string linkHref, ref string? html, HtmlDocument doc)
|
||||||
|
{
|
||||||
|
if (decision.Decision == "delete")
|
||||||
|
{
|
||||||
|
var parent = anchorNode.ParentNode;
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
parent.ReplaceChild(HtmlNode.CreateNode(anchorNode.InnerText), anchorNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (decision.Decision == "edit" && !string.IsNullOrEmpty(decision.NewUrl))
|
||||||
|
{
|
||||||
|
anchorNode.SetAttributeValue("href", decision.NewUrl);
|
||||||
|
}
|
||||||
|
// "keep" = nichts tun
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Öffnet die Standard-Browser-Anwendung mit der angegebenen URL.
|
||||||
|
/// </summary>
|
||||||
|
private static void OpenBrowser(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = url,
|
||||||
|
UseShellExecute = true
|
||||||
|
};
|
||||||
|
System.Diagnostics.Process.Start(psi);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [Browser-Fehler] Konnte Browser nicht öffnen: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> IsValidUrlAsync(string url, bool checkReachability = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) return false;
|
||||||
|
|
||||||
|
// Relative URLs (beginnen mit / oder sind Pfade) sind für interne Links gültig
|
||||||
|
if (url.StartsWith("/") || !url.Contains(":"))
|
||||||
|
{
|
||||||
|
if (checkReachability) { /* relative links are considered valid regardless */ }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe auf gültiges URI-Format mit http/https Schemes
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(url, UriKind.Absolute);
|
||||||
|
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Optionale Reichbarkeitsprüfung
|
||||||
|
if (checkReachability)
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Head, uri);
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
return (int)response.StatusCode < 400; // 2xx oder 3xx = erreichbar
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
// Verbindungsfehler, DNS-Failure, Timeout etc.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Timeout
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronen Wrapper für die bestehende Validierung
|
||||||
|
private static bool IsValidUrl(string url)
|
||||||
|
{
|
||||||
|
return IsValidUrlAsync(url, false).Result;
|
||||||
|
}
|
||||||
|
|
||||||
static async Task GhostCreateOrUpdateFromJsonAsync()
|
static async Task GhostCreateOrUpdateFromJsonAsync()
|
||||||
{
|
{
|
||||||
string ghostPostsDir = Path.Combine(BaseDir, "ghostPosts");
|
string ghostPostsDir = Path.Combine(BaseDir, "ghostPosts");
|
||||||
|
|
@ -509,12 +748,14 @@ class Program
|
||||||
Console.WriteLine($" Post: {postFile.Title}");
|
Console.WriteLine($" Post: {postFile.Title}");
|
||||||
Console.WriteLine($" Slug: {postFile.Slug}");
|
Console.WriteLine($" Slug: {postFile.Slug}");
|
||||||
Console.WriteLine($" Kategorien: {postFile.Categories.Count}");
|
Console.WriteLine($" Kategorien: {postFile.Categories.Count}");
|
||||||
|
Console.WriteLine($" Tags: {postFile.Tags.Count}");
|
||||||
Console.WriteLine($" Kommentare: {postFile.Comments.Count}");
|
Console.WriteLine($" Kommentare: {postFile.Comments.Count}");
|
||||||
Console.WriteLine($" Bilder: {postFile.ImageFiles.Count}");
|
Console.WriteLine($" Bilder: {postFile.ImageFiles.Count}");
|
||||||
|
|
||||||
// 1. ALLE Bilder hochladen + Ghost-URL-Mapping erstellen
|
// 1. ALLE Bilder hochladen + Ghost-URL-Mapping erstellen
|
||||||
var ghostUrlMap = new Dictionary<string, string>();
|
var ghostUrlMap = new Dictionary<string, string>();
|
||||||
string html = postFile.Html ?? "";
|
string html = postFile.Html ?? "";
|
||||||
|
html = await ValidateAndFixLinksAsync(html);
|
||||||
|
|
||||||
if (postFile.ImageFiles.Count > 0)
|
if (postFile.ImageFiles.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -572,12 +813,18 @@ class Program
|
||||||
Console.WriteLine($" [Warnung] {remaining} unrestrierte __GHOST_URL__ Platzhalter im HTML");
|
Console.WriteLine($" [Warnung] {remaining} unrestrierte __GHOST_URL__ Platzhalter im HTML");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Tags erstellen/zuordnen
|
// 4. Tags erstellen/zuordnen (Kategorien + Tags)
|
||||||
var tags = new List<GhostAdminTag>();
|
var tags = new List<GhostAdminTag>();
|
||||||
|
var allTagSources = new List<WpCategory>();
|
||||||
if (postFile.Categories.Count > 0)
|
if (postFile.Categories.Count > 0)
|
||||||
|
allTagSources.AddRange(postFile.Categories);
|
||||||
|
if (postFile.Tags.Count > 0)
|
||||||
|
allTagSources.AddRange(postFile.Tags);
|
||||||
|
|
||||||
|
if (allTagSources.Count > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(" [Tags] Verarbeite Kategorien...");
|
Console.WriteLine($" [Tags] Verarbeite {postFile.Categories.Count} Kategorien + {postFile.Tags.Count} Tags...");
|
||||||
tags = await GhostApi.EnsureTagsAsync(postFile.Categories);
|
tags = await GhostApi.EnsureTagsAsync(allTagSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
|
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ namespace wp2ghost
|
||||||
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
|
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
|
||||||
[JsonPropertyName("feature_image")] public string? FeatureImage { get; set; }
|
[JsonPropertyName("feature_image")] public string? FeatureImage { get; set; }
|
||||||
[JsonPropertyName("categories")] public List<WpCategory> Categories { get; set; } = new();
|
[JsonPropertyName("categories")] public List<WpCategory> Categories { get; set; } = new();
|
||||||
|
[JsonPropertyName("tags")] public List<WpCategory> Tags { get; set; } = new();
|
||||||
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
||||||
[JsonPropertyName("comments")] public List<WpComment> Comments { get; set; } = new();
|
[JsonPropertyName("comments")] public List<WpComment> Comments { get; set; } = new();
|
||||||
[JsonPropertyName("image_files")] public List<string> ImageFiles { get; set; } = new();
|
[JsonPropertyName("image_files")] public List<string> ImageFiles { get; set; } = new();
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ namespace wp2ghost
|
||||||
ConvertSingleImages(htmlDoc);
|
ConvertSingleImages(htmlDoc);
|
||||||
ConvertFigures(htmlDoc);
|
ConvertFigures(htmlDoc);
|
||||||
ConvertWpClasses(htmlDoc);
|
ConvertWpClasses(htmlDoc);
|
||||||
|
AddTargetBlankToExternalLinks(htmlDoc);
|
||||||
|
|
||||||
return htmlDoc.DocumentNode.OuterHtml;
|
return htmlDoc.DocumentNode.OuterHtml;
|
||||||
}
|
}
|
||||||
|
|
@ -134,11 +135,27 @@ namespace wp2ghost
|
||||||
container.AddClass("kg-gallery-container");
|
container.AddClass("kg-gallery-container");
|
||||||
|
|
||||||
var wrappers = new List<HtmlNode>();
|
var wrappers = new List<HtmlNode>();
|
||||||
|
var imageCaptions = new List<string>();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var img = item.SelectSingleNode(".//img");
|
var img = item.SelectSingleNode(".//img");
|
||||||
if (img == null) continue;
|
if (img == null) continue;
|
||||||
|
|
||||||
|
// Bild-Titel aus data-image-caption Attribut extrahieren
|
||||||
|
string dataCaption = img.GetAttributeValue("data-image-caption", "").Trim();
|
||||||
|
if (!string.IsNullOrEmpty(dataCaption))
|
||||||
|
imageCaptions.Add(dataCaption);
|
||||||
|
|
||||||
|
// Auch figcaption innerhalb des gallery-items prüfen
|
||||||
|
var figCap = item.SelectSingleNode(".//figcaption");
|
||||||
|
if (figCap != null && !string.IsNullOrWhiteSpace(figCap.InnerText))
|
||||||
|
{
|
||||||
|
string capText = figCap.InnerText.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(capText) && !imageCaptions.Contains(capText))
|
||||||
|
imageCaptions.Add(capText);
|
||||||
|
}
|
||||||
|
|
||||||
string src = GetBestImageUrl(img);
|
string src = GetBestImageUrl(img);
|
||||||
if (string.IsNullOrEmpty(src)) continue;
|
if (string.IsNullOrEmpty(src)) continue;
|
||||||
|
|
||||||
|
|
@ -150,11 +167,14 @@ namespace wp2ghost
|
||||||
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
||||||
ghostGallery.AppendChild(container);
|
ghostGallery.AppendChild(container);
|
||||||
|
|
||||||
var captionNode = gallery.SelectSingleNode(".//*[contains(@class, 'gallery-caption')] | .//figcaption");
|
// Gallery-Bild-Titel übertragen, wenn vorhanden
|
||||||
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
|
if (imageCaptions.Count > 0)
|
||||||
{
|
{
|
||||||
|
// Ersten nicht-leeren Titel nehmen als Gallery-Kapitel
|
||||||
|
string firstCaption = imageCaptions[0];
|
||||||
|
|
||||||
var newCaption = htmlDoc.CreateElement("figcaption");
|
var newCaption = htmlDoc.CreateElement("figcaption");
|
||||||
newCaption.InnerHtml = captionNode.InnerHtml;
|
newCaption.InnerHtml = firstCaption;
|
||||||
ghostGallery.AppendChild(newCaption);
|
ghostGallery.AppendChild(newCaption);
|
||||||
ghostGallery.AddClass("kg-card-hascaption");
|
ghostGallery.AddClass("kg-card-hascaption");
|
||||||
}
|
}
|
||||||
|
|
@ -207,14 +227,23 @@ namespace wp2ghost
|
||||||
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
||||||
ghostGallery.AppendChild(container);
|
ghostGallery.AppendChild(container);
|
||||||
|
|
||||||
// Caption aus erstem figcaption im Gallery-Container extrahieren
|
// Alle individuellen Bildunterschriften aus der Gutenberg-Gallery sammeln
|
||||||
var captionNode = gallery.SelectSingleNode(".//figcaption");
|
var captionNodes = gallery.SelectNodes(".//figcaption");
|
||||||
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
|
if (captionNodes != null && captionNodes.Count > 0)
|
||||||
{
|
{
|
||||||
var newCaption = htmlDoc.CreateElement("figcaption");
|
var distinctCaptions = captionNodes
|
||||||
newCaption.InnerHtml = captionNode.InnerHtml;
|
.Select(n => n.InnerText.Trim())
|
||||||
ghostGallery.AppendChild(newCaption);
|
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||||
ghostGallery.AddClass("kg-card-hascaption");
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (distinctCaptions.Count > 0)
|
||||||
|
{
|
||||||
|
var newCaption = htmlDoc.CreateElement("figcaption");
|
||||||
|
newCaption.InnerHtml = string.Join(" · ", distinctCaptions);
|
||||||
|
ghostGallery.AppendChild(newCaption);
|
||||||
|
ghostGallery.AddClass("kg-card-hascaption");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// data-carousel-extra als Ghost-Attribut übertragen (falls vorhanden)
|
// data-carousel-extra als Ghost-Attribut übertragen (falls vorhanden)
|
||||||
|
|
@ -567,6 +596,28 @@ namespace wp2ghost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Externe Links mit target="_blank" versehen
|
||||||
|
// =========================================================================
|
||||||
|
private static void AddTargetBlankToExternalLinks(HtmlDocument htmlDoc)
|
||||||
|
{
|
||||||
|
var anchors = htmlDoc.DocumentNode.SelectNodes("//a[@href]");
|
||||||
|
if (anchors == null) return;
|
||||||
|
|
||||||
|
foreach (var anchor in anchors.ToList())
|
||||||
|
{
|
||||||
|
string href = anchor.GetAttributeValue("href", "").Trim();
|
||||||
|
if (string.IsNullOrEmpty(href)) continue;
|
||||||
|
|
||||||
|
// Nur absolute externe URLs (http/https)
|
||||||
|
if (href.StartsWith("http://") || href.StartsWith("https://"))
|
||||||
|
{
|
||||||
|
anchor.SetAttributeValue("target", "_blank");
|
||||||
|
anchor.SetAttributeValue("rel", "noopener noreferrer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HasClass(HtmlNode node, string className)
|
private static bool HasClass(HtmlNode node, string className)
|
||||||
{
|
{
|
||||||
string classes = node.GetAttributeValue("class", "");
|
string classes = node.GetAttributeValue("class", "");
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,28 @@ namespace wp2ghost
|
||||||
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
|
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
|
||||||
public static bool TryGetRelativePath(string url, out string relativePath)
|
public static bool TryGetRelativePath(string url, out string relativePath)
|
||||||
{
|
{
|
||||||
|
// 1. Direkte URL-Prüfung im Cache
|
||||||
if (Cache.TryGetValue(url, out var cached))
|
if (Cache.TryGetValue(url, out var cached))
|
||||||
{
|
{
|
||||||
relativePath = cached.RelativePath;
|
relativePath = cached.RelativePath;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Prüfung über WordPress-Dateinamen (fürFeatured Images mit anderer URL)
|
||||||
|
string wpName = Path.GetFileNameWithoutExtension(new Uri(url).AbsolutePath);
|
||||||
|
if (WordpressToGhostImageName.TryGetValue(wpName, out string? relPath) && !string.IsNullOrEmpty(relPath))
|
||||||
|
{
|
||||||
|
string localPath = Path.Combine(LocalImageDir, relPath);
|
||||||
|
if (File.Exists(localPath))
|
||||||
|
{
|
||||||
|
relativePath = relPath;
|
||||||
|
// Auch im Cache eintragen für zukünftige Suche
|
||||||
|
Cache[url] = (relPath, localPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
WordpressToGhostImageName.Remove(wpName);
|
||||||
|
}
|
||||||
|
|
||||||
relativePath = "";
|
relativePath = "";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +63,10 @@ namespace wp2ghost
|
||||||
{
|
{
|
||||||
var lines = new List<string>();
|
var lines = new List<string>();
|
||||||
foreach (var kvp in Cache)
|
foreach (var kvp in Cache)
|
||||||
lines.Add($"{kvp.Key}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}");
|
{
|
||||||
|
string wpName = Path.GetFileNameWithoutExtension(new Uri(kvp.Key).AbsolutePath);
|
||||||
|
lines.Add($"{kvp.Key}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}|{wpName}");
|
||||||
|
}
|
||||||
File.WriteAllLines(CachePath, lines);
|
File.WriteAllLines(CachePath, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,8 +243,13 @@ namespace wp2ghost
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var parts = line.Split('|');
|
var parts = line.Split('|');
|
||||||
if (parts.Length == 3)
|
if (parts.Length >= 3)
|
||||||
|
{
|
||||||
Cache[parts[0]] = (parts[1], parts[2]);
|
Cache[parts[0]] = (parts[1], parts[2]);
|
||||||
|
// WordPress-Dateinamen auch ins Dictionary laden
|
||||||
|
if (parts.Length >= 4 && !string.IsNullOrEmpty(parts[3]))
|
||||||
|
WordpressToGhostImageName[parts[3]] = parts[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
138
CSharpGhostImporter/LinkDecisionManager.cs
Normal file
138
CSharpGhostImporter/LinkDecisionManager.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace wp2ghost
|
||||||
|
{
|
||||||
|
// =============================================================================
|
||||||
|
// LinkDecision — gespeicherte Entscheidung für einen Link
|
||||||
|
// =============================================================================
|
||||||
|
public class LinkDecision
|
||||||
|
{
|
||||||
|
[JsonPropertyName("link_text")]
|
||||||
|
public string LinkText { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("link_href")]
|
||||||
|
public string LinkHref { get; set; } = "";
|
||||||
|
|
||||||
|
// Entscheidung: "keep" (beibehalten), "delete" (löschen), "edit" (ersetzt)
|
||||||
|
[JsonPropertyName("decision")]
|
||||||
|
public string Decision { get; set; } = "";
|
||||||
|
|
||||||
|
// Nur bei "edit": die neue URL
|
||||||
|
[JsonPropertyName("new_url")]
|
||||||
|
public string? NewUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timestamp")]
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LinkDecisionManager — merkt sich User-Entscheidungen für Links (Memory)
|
||||||
|
// =============================================================================
|
||||||
|
public static class LinkDecisionManager
|
||||||
|
{
|
||||||
|
private static Dictionary<string, LinkDecision> _decisions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static string _decisionsPath = "";
|
||||||
|
|
||||||
|
// Key = Normalisierter Link (Text|Href)
|
||||||
|
private static string NormalizeKey(string linkText, string linkHref)
|
||||||
|
{
|
||||||
|
return $"{linkText.Trim().ToLowerInvariant()}|{linkHref.Trim().ToLowerInvariant()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Init(string baseDir)
|
||||||
|
{
|
||||||
|
_decisionsPath = Path.Combine(baseDir, "link_decisions.json");
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_decisionsPath)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
File.WriteAllText(_decisionsPath, JsonSerializer.Serialize(_decisions.Values.ToList(), options));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [Link-Memory] Fehler beim Speichern der Entscheidungen: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Load()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_decisionsPath)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(_decisionsPath);
|
||||||
|
var list = JsonSerializer.Deserialize<List<LinkDecision>>(json);
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
foreach (var d in list)
|
||||||
|
{
|
||||||
|
string key = NormalizeKey(d.LinkText, d.LinkHref);
|
||||||
|
_decisions[key] = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [Link-Memory] Fehler beim Laden der Entscheidungen: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Speichert eine User-Entscheidung für einen Link.
|
||||||
|
/// </summary>
|
||||||
|
public static void SaveDecision(string linkText, string linkHref, string decision, string? newUrl = null)
|
||||||
|
{
|
||||||
|
string key = NormalizeKey(linkText, linkHref);
|
||||||
|
_decisions[key] = new LinkDecision
|
||||||
|
{
|
||||||
|
LinkText = linkText,
|
||||||
|
LinkHref = linkHref,
|
||||||
|
Decision = decision,
|
||||||
|
NewUrl = newUrl
|
||||||
|
};
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versucht eine frühere Entscheidung für diesen Link zu finden.
|
||||||
|
/// </summary>
|
||||||
|
public static LinkDecision? GetPreviousDecision(string linkText, string linkHref)
|
||||||
|
{
|
||||||
|
string key = NormalizeKey(linkText, linkHref);
|
||||||
|
if (_decisions.TryGetValue(key, out var d))
|
||||||
|
return d;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt die Anzahl gespeicherter Entscheidungen zurück (für Info-Anzeige).
|
||||||
|
/// </summary>
|
||||||
|
public static int Count => _decisions.Count;
|
||||||
|
|
||||||
|
public static void ShowInfo()
|
||||||
|
{
|
||||||
|
if (_decisions.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nGespeicherte Link-Entscheidungen: {_decisions.Count}");
|
||||||
|
foreach (var kvp in _decisions.Take(20))
|
||||||
|
{
|
||||||
|
var d = kvp.Value;
|
||||||
|
string info = d.Decision switch
|
||||||
|
{
|
||||||
|
"delete" => "gelöscht",
|
||||||
|
"edit" => $"ersetzt durch '{d.NewUrl}'",
|
||||||
|
_ => "beibehalten"
|
||||||
|
};
|
||||||
|
Console.WriteLine($" \"{d.LinkText}\" -> {info}");
|
||||||
|
}
|
||||||
|
if (_decisions.Count > 20)
|
||||||
|
Console.WriteLine($" ... und {_decisions.Count - 20} weitere");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -78,6 +78,9 @@ namespace wp2ghost
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProcessedPosts.Clear();
|
||||||
|
LoadProcessedPosts();
|
||||||
|
|
||||||
var pendingIds = allPostIds.Where(id => !ProcessedPosts.Contains(id)).ToList();
|
var pendingIds = allPostIds.Where(id => !ProcessedPosts.Contains(id)).ToList();
|
||||||
Console.WriteLine($"Insgesamt gefunden: {allPostIds.Count}, Bereits abgefragt: {allPostIds.Count - pendingIds.Count}, Noch zu verarbeiten: {pendingIds.Count}");
|
Console.WriteLine($"Insgesamt gefunden: {allPostIds.Count}, Bereits abgefragt: {allPostIds.Count - pendingIds.Count}, Noch zu verarbeiten: {pendingIds.Count}");
|
||||||
|
|
||||||
|
|
@ -105,11 +108,15 @@ namespace wp2ghost
|
||||||
Console.WriteLine($"[{++processed}/{pendingIds.Count}] -> Beitrag/Seite ID: {wpId}...");
|
Console.WriteLine($"[{++processed}/{pendingIds.Count}] -> Beitrag/Seite ID: {wpId}...");
|
||||||
string detailUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/custom/v1/post-details/{wpId}";
|
string detailUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/custom/v1/post-details/{wpId}";
|
||||||
|
|
||||||
|
//string postUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/wp/v2/posts/{wpId}?_embed";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var detailJson = await httpClient.GetStringAsync(detailUrl);
|
var detailJson = await httpClient.GetStringAsync(detailUrl);
|
||||||
var wpPost = JsonSerializer.Deserialize<WpPostResponse>(detailJson);
|
var wpPost = JsonSerializer.Deserialize<WpPostResponse>(detailJson);
|
||||||
|
|
||||||
|
//var postJson = await httpClient.GetStringAsync(postUrl);
|
||||||
|
|
||||||
if (wpPost == null)
|
if (wpPost == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" Übersprungen (null).");
|
Console.WriteLine($" Übersprungen (null).");
|
||||||
|
|
@ -217,7 +224,7 @@ namespace wp2ghost
|
||||||
GhostPosts.Add(ghostPost);
|
GhostPosts.Add(ghostPost);
|
||||||
|
|
||||||
// JSON-Datei in ghostPosts/ speichern
|
// JSON-Datei in ghostPosts/ speichern
|
||||||
var postFile = new GhostPostFile
|
var ghostPostFile = new GhostPostFile
|
||||||
{
|
{
|
||||||
WpId = wpId,
|
WpId = wpId,
|
||||||
WpSlug = wpPost.Slug,
|
WpSlug = wpPost.Slug,
|
||||||
|
|
@ -230,13 +237,17 @@ namespace wp2ghost
|
||||||
PublishedAt = postDate.ToString("o"),
|
PublishedAt = postDate.ToString("o"),
|
||||||
FeatureImage = featureImageGhostPath,
|
FeatureImage = featureImageGhostPath,
|
||||||
Categories = wpPost.Categories ?? new(),
|
Categories = wpPost.Categories ?? new(),
|
||||||
|
Tags = wpPost.Tags ?? new(),
|
||||||
Keywords = wpPost.Keywords ?? "",
|
Keywords = wpPost.Keywords ?? "",
|
||||||
Comments = wpPost.Comments ?? new(),
|
Comments = wpPost.Comments ?? new(),
|
||||||
ImageFiles = imageFiles,
|
ImageFiles = imageFiles,
|
||||||
Html = htmlContent
|
Html = htmlContent
|
||||||
};
|
};
|
||||||
string jsonPath = Path.Combine(GhostPostsDir, $"{postFile.WpId}.json");
|
string jsonPath = Path.Combine(GhostPostsDir, $"{ghostPostFile.WpId}.json");
|
||||||
File.WriteAllText(jsonPath, JsonSerializer.Serialize(postFile, new JsonSerializerOptions { WriteIndented = true }));
|
File.WriteAllText(jsonPath, JsonSerializer.Serialize(ghostPostFile, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
|
||||||
|
// Cache sofort speichern (nicht erst beim Beenden)
|
||||||
|
ImageManager.SaveCache();
|
||||||
|
|
||||||
MarkProcessed(wpId, wpPost.Slug);
|
MarkProcessed(wpId, wpPost.Slug);
|
||||||
internalId++;
|
internalId++;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace wp2ghost
|
||||||
[JsonPropertyName("excerpt")] public string? Excerpt { get; set; }
|
[JsonPropertyName("excerpt")] public string? Excerpt { get; set; }
|
||||||
[JsonPropertyName("featured_image")] public string? FeaturedImage { get; set; }
|
[JsonPropertyName("featured_image")] public string? FeaturedImage { get; set; }
|
||||||
[JsonPropertyName("categories")] public List<WpCategory>? Categories { get; set; } = new();
|
[JsonPropertyName("categories")] public List<WpCategory>? Categories { get; set; } = new();
|
||||||
|
[JsonPropertyName("tags")] public List<WpCategory>? Tags { get; set; } = new();
|
||||||
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
||||||
[JsonPropertyName("comments")] public List<WpComment>? Comments { get; set; } = new();
|
[JsonPropertyName("comments")] public List<WpComment>? Comments { get; set; } = new();
|
||||||
[JsonPropertyName("media_files")] public List<string>? MediaFiles { get; set; } = new();
|
[JsonPropertyName("media_files")] public List<string>? MediaFiles { get; set; } = new();
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ function rbr_get_post_details_with_media($data) {
|
||||||
// Duplikate entfernen
|
// Duplikate entfernen
|
||||||
$unique_media = array_values(array_unique($media_urls));
|
$unique_media = array_values(array_unique($media_urls));
|
||||||
|
|
||||||
// 4. Kategorien ( Categories Only, keine Tags)
|
// 4. Kategorien
|
||||||
$categories = array();
|
$categories = array();
|
||||||
$cats = get_the_category($post_id);
|
$cats = get_the_category($post_id);
|
||||||
if (!empty($cats) && !is_wp_error($cats)) {
|
if (!empty($cats) && !is_wp_error($cats)) {
|
||||||
|
|
@ -188,23 +188,44 @@ function rbr_get_post_details_with_media($data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
|
// 5. Tags (post_tag Taxonomy)
|
||||||
|
$tags = array();
|
||||||
|
$post_tags = get_the_tags($post_id);
|
||||||
|
if (!empty($post_tags) && !is_wp_error($post_tags)) {
|
||||||
|
foreach ($post_tags as $tag) {
|
||||||
|
$tags[] = array(
|
||||||
|
'id' => intval($tag->term_id),
|
||||||
|
'name' => $tag->name,
|
||||||
|
'slug' => $tag->slug,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
|
||||||
$keywords = '';
|
$keywords = '';
|
||||||
// Yoast SEO
|
// Yoast SEO: Prüfe auf Meta-Key direkt (funktioniert in allen Versionen)
|
||||||
if (function_exists('yoast_get_primary_term')) {
|
$yoast_kw = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
|
||||||
$keywords = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
|
if (!empty($yoast_kw)) {
|
||||||
|
$keywords = $yoast_kw;
|
||||||
}
|
}
|
||||||
// Rank Math
|
// Rank Math
|
||||||
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
|
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
|
||||||
$rank_data = rank_math_get_single_post_data($post_id);
|
$rank_data = rank_math_get_single_post_data($post_id);
|
||||||
if (!empty($rank_data->get_keywords())) {
|
if (method_exists($rank_data, 'get_keywords')) {
|
||||||
$keywords = $rank_data->get_keywords();
|
$kw = $rank_data->get_keywords();
|
||||||
|
if (!empty($kw)) {
|
||||||
|
$keywords = $kw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: Custom Field 'post_keywords'
|
// Fallback: Custom Field 'post_keywords'
|
||||||
if (empty($keywords)) {
|
if (empty($keywords)) {
|
||||||
$keywords = get_post_meta($post_id, 'post_keywords', true);
|
$keywords = get_post_meta($post_id, 'post_keywords', true);
|
||||||
}
|
}
|
||||||
|
// Fallback: Tags als Keywords
|
||||||
|
if (empty($keywords) && !empty($tags)) {
|
||||||
|
$keywords = implode(', ', array_column($tags, 'name'));
|
||||||
|
}
|
||||||
// Fallback: Kategorien als Keywords
|
// Fallback: Kategorien als Keywords
|
||||||
if (empty($keywords) && !empty($categories)) {
|
if (empty($keywords) && !empty($categories)) {
|
||||||
$keywords = implode(', ', array_column($categories, 'name'));
|
$keywords = implode(', ', array_column($categories, 'name'));
|
||||||
|
|
@ -241,6 +262,7 @@ function rbr_get_post_details_with_media($data) {
|
||||||
),
|
),
|
||||||
'featured_image' => $featured_image_url,
|
'featured_image' => $featured_image_url,
|
||||||
'categories' => $categories,
|
'categories' => $categories,
|
||||||
|
'tags' => $tags,
|
||||||
'keywords' => $keywords,
|
'keywords' => $keywords,
|
||||||
'comments' => $comments,
|
'comments' => $comments,
|
||||||
'media_files' => $unique_media,
|
'media_files' => $unique_media,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue