Importer: Kategorien als Tags, Keywords als Meta, Bilder vollständig nach Ghost
Importer - Kategorien werden als Ghost-Tags angelegt; bei mehreren wird die primäre abgefragt, die übrigen folgen als sekundäre Tags - Schlagwörter werden ohne Nachfrage als <meta name="keywords"> in den Code-Injection-Header geschrieben (Ghost kennt kein meta_keywords-Feld) - Tag-Suche lädt alle Seiten, HTML-kodierte Namen werden dekodiert Bilder - Varianten eines WordPress-Bildes (Original, -scaled, -WxH, ?w=) werden zusammengefasst, einmal geladen und im HTML alle umgeschrieben - src bei Einzelbildern setzen, srcset/sizes entfernen - Cache-Schlüssel ist der Pfad statt nur der Dateiname; Dateinamen eindeutig - ZIP-Export enthält Cache-Bilder und feature_image - Warnung bei Bild-URLs, die weiter auf WordPress zeigen - Maximale Bildgröße 2048 px Plugin / GhostImageManager - Plugin liefert primäre Kategorie - Kategorie- und Keyword-Felder samt Filter im Image Manager Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a730639c2a
commit
24d88e224b
10 changed files with 313 additions and 379 deletions
|
|
@ -748,7 +748,7 @@ 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($" Schlagwörter: {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}");
|
||||||
|
|
||||||
|
|
@ -813,7 +813,13 @@ 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 (Kategorien + Tags)
|
// Bilder, die noch von WordPress geladen würden → Post neu abrufen (Hauptmenü 5, dann 1)
|
||||||
|
int wpImages = System.Text.RegularExpressions.Regex.Matches(html, @"<img\s[^>]*?\bsrc=[""'][^""']*/wp-content/uploads/", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Count;
|
||||||
|
if (wpImages > 0)
|
||||||
|
Console.WriteLine($" [Warnung] {wpImages} Bild(er) im HTML zeigen noch auf WordPress (/wp-content/uploads/)");
|
||||||
|
|
||||||
|
// 4. Kategorien als Ghost-Tags erstellen/zuordnen.
|
||||||
|
// Der erste Tag ist in Ghost der primäre, alle weiteren sind sekundär.
|
||||||
var tags = new List<GhostAdminTag>();
|
var tags = new List<GhostAdminTag>();
|
||||||
var allTagSources = new List<WpCategory>();
|
var allTagSources = new List<WpCategory>();
|
||||||
|
|
||||||
|
|
@ -826,36 +832,41 @@ class Program
|
||||||
{
|
{
|
||||||
Console.WriteLine($" [{i + 1}] {postFile.Categories[i].Name}");
|
Console.WriteLine($" [{i + 1}] {postFile.Categories[i].Name}");
|
||||||
}
|
}
|
||||||
Console.Write(" Relevante Kategorie (Nummer, Enter = erste): ");
|
|
||||||
string? catChoice = Console.ReadLine()?.Trim();
|
int catIdx;
|
||||||
if (int.TryParse(catChoice, out int catIdx) && catIdx >= 1 && catIdx <= postFile.Categories.Count)
|
while (true)
|
||||||
{
|
{
|
||||||
// Gewählte Kategorie an erste Stelle setzen
|
Console.Write(" Welche ist die primäre Kategorie? (Nummer): ");
|
||||||
|
string? catChoice = Console.ReadLine();
|
||||||
|
if (int.TryParse(catChoice?.Trim(), out catIdx) && catIdx >= 1 && catIdx <= postFile.Categories.Count)
|
||||||
|
break;
|
||||||
|
if (catChoice == null) { catIdx = 1; break; } // keine Eingabe möglich (stdin geschlossen)
|
||||||
|
Console.WriteLine($" Bitte eine Zahl von 1 bis {postFile.Categories.Count} eingeben.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gewählte Kategorie an erste Stelle setzen, alle anderen folgen als sekundäre
|
||||||
var primary = postFile.Categories[catIdx - 1];
|
var primary = postFile.Categories[catIdx - 1];
|
||||||
allTagSources.Add(primary);
|
allTagSources.Add(primary);
|
||||||
allTagSources.AddRange(postFile.Categories.Where((_, idx) => idx != catIdx - 1));
|
allTagSources.AddRange(postFile.Categories.Where((_, idx) => idx != catIdx - 1));
|
||||||
Console.WriteLine($" -> Primäre Kategorie: {primary.Name}");
|
Console.WriteLine($" -> Primär: {primary.Name}");
|
||||||
}
|
Console.WriteLine($" -> Sekundär: {string.Join(", ", allTagSources.Skip(1).Select(c => c.Name))}");
|
||||||
else
|
|
||||||
{
|
|
||||||
allTagSources.AddRange(postFile.Categories);
|
|
||||||
Console.WriteLine($" -> Erste Kategorie: {postFile.Categories[0].Name}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (postFile.Categories.Count == 1)
|
else if (postFile.Categories.Count == 1)
|
||||||
{
|
{
|
||||||
allTagSources.AddRange(postFile.Categories);
|
allTagSources.AddRange(postFile.Categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postFile.Tags.Count > 0)
|
|
||||||
allTagSources.AddRange(postFile.Tags);
|
|
||||||
|
|
||||||
if (allTagSources.Count > 0)
|
if (allTagSources.Count > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" [Tags] Verarbeite {postFile.Categories.Count} Kategorien + {postFile.Tags.Count} Tags...");
|
Console.WriteLine($" [Tags] Verarbeite {allTagSources.Count} Kategorien...");
|
||||||
tags = await GhostApi.EnsureTagsAsync(allTagSources);
|
tags = await GhostApi.EnsureTagsAsync(allTagSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4b. Schlagwörter (WP-Tags + SEO-Keywords) ohne Nachfrage als <meta name="keywords">
|
||||||
|
var keywords = postFile.Tags.Select(t => t.Name ?? "")
|
||||||
|
.Concat((postFile.Keywords ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
|
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
|
||||||
Console.Write(" Prüfe Ghost...");
|
Console.Write(" Prüfe Ghost...");
|
||||||
var existingPosts = await GhostApi.BrowsePostsAsync($"slug:{postFile.Slug}", 1);
|
var existingPosts = await GhostApi.BrowsePostsAsync($"slug:{postFile.Slug}", 1);
|
||||||
|
|
@ -865,21 +876,25 @@ class Program
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" vorhanden (ID: {existing.Id})");
|
Console.WriteLine($" vorhanden (ID: {existing.Id})");
|
||||||
|
string? head = GhostApi.BuildKeywordsHead(existing.CodeinjectionHead, keywords);
|
||||||
|
if (head != null) Console.WriteLine(" [Meta] Schlagwörter als <meta name=\"keywords\"> gesetzt");
|
||||||
Console.Write(" Aktualisiere...");
|
Console.Write(" Aktualisiere...");
|
||||||
result = await GhostApi.UpdatePostExtendedAsync(
|
result = await GhostApi.UpdatePostExtendedAsync(
|
||||||
existing.Id!, postFile.Title ?? "", html,
|
existing.Id!, postFile.Title ?? "", html,
|
||||||
existing.UpdatedAt ?? "", postFile.Status ?? "published",
|
existing.UpdatedAt ?? "", postFile.Status ?? "published",
|
||||||
featureImageUrl, tags, postFile.Keywords);
|
featureImageUrl, tags, head);
|
||||||
Console.WriteLine(result != null ? " OK" : " FEHLER");
|
Console.WriteLine(result != null ? " OK" : " FEHLER");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine(" nicht vorhanden");
|
Console.WriteLine(" nicht vorhanden");
|
||||||
|
string? head = GhostApi.BuildKeywordsHead(null, keywords);
|
||||||
|
if (head != null) Console.WriteLine(" [Meta] Schlagwörter als <meta name=\"keywords\"> gesetzt");
|
||||||
Console.Write(" Erstelle neuen Post...");
|
Console.Write(" Erstelle neuen Post...");
|
||||||
result = await GhostApi.CreatePostExtendedAsync(
|
result = await GhostApi.CreatePostExtendedAsync(
|
||||||
postFile.Title ?? "", html,
|
postFile.Title ?? "", html,
|
||||||
postFile.Status ?? "published", postFile.Slug,
|
postFile.Status ?? "published", postFile.Slug,
|
||||||
featureImageUrl, tags, postFile.Keywords);
|
featureImageUrl, tags, head);
|
||||||
Console.WriteLine(result != null ? $" OK (ID: {result.Id})" : " FEHLER");
|
Console.WriteLine(result != null ? $" OK (ID: {result.Id})" : " FEHLER");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ namespace wp2ghost
|
||||||
|
|
||||||
public static async Task<List<GhostAdminPost>> BrowsePostsAsync(string filter = "", int limit = 50)
|
public static async Task<List<GhostAdminPost>> BrowsePostsAsync(string filter = "", int limit = 50)
|
||||||
{
|
{
|
||||||
string endpoint = $"posts/?limit={limit}&filter={Uri.EscapeDataString(filter)}&fields=id,title,slug,status,created_at,updated_at,html,feature_image";
|
string endpoint = $"posts/?limit={limit}&filter={Uri.EscapeDataString(filter)}&fields=id,title,slug,status,created_at,updated_at,html,feature_image,codeinjection_head";
|
||||||
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
|
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
|
||||||
var response = await httpClient.SendAsync(request);
|
var response = await httpClient.SendAsync(request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
@ -151,15 +151,22 @@ namespace wp2ghost
|
||||||
// ===================== Tags =====================
|
// ===================== Tags =====================
|
||||||
public static async Task<List<GhostAdminTag>> BrowseTagsAsync(string filter = "", int limit = 100)
|
public static async Task<List<GhostAdminTag>> BrowseTagsAsync(string filter = "", int limit = 100)
|
||||||
{
|
{
|
||||||
string endpoint = $"tags/?limit={limit}&fields=id,name,slug";
|
// Browse-Endpunkte sind paginiert -> alle Seiten laden
|
||||||
|
var all = new List<GhostAdminTag>();
|
||||||
|
for (int page = 1; ; page++)
|
||||||
|
{
|
||||||
|
string endpoint = $"tags/?limit={limit}&page={page}&fields=id,name,slug";
|
||||||
if (!string.IsNullOrEmpty(filter))
|
if (!string.IsNullOrEmpty(filter))
|
||||||
endpoint += $"&filter={Uri.EscapeDataString(filter)}";
|
endpoint += $"&filter={Uri.EscapeDataString(filter)}";
|
||||||
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
|
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
|
||||||
var response = await httpClient.SendAsync(request);
|
var response = await httpClient.SendAsync(request);
|
||||||
if (!response.IsSuccessStatusCode) return new();
|
if (!response.IsSuccessStatusCode) break;
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
var result = JsonSerializer.Deserialize<GhostAdminTagsResponse>(json);
|
var tags = JsonSerializer.Deserialize<GhostAdminTagsResponse>(json)?.Tags ?? new();
|
||||||
return result?.Tags ?? new();
|
all.AddRange(tags);
|
||||||
|
if (tags.Count < limit) break;
|
||||||
|
}
|
||||||
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<GhostAdminTag?> CreateTagAsync(string name, string? slug = null)
|
public static async Task<GhostAdminTag?> CreateTagAsync(string name, string? slug = null)
|
||||||
|
|
@ -185,15 +192,19 @@ namespace wp2ghost
|
||||||
{
|
{
|
||||||
var result = new List<GhostAdminTag>();
|
var result = new List<GhostAdminTag>();
|
||||||
var existingTags = await BrowseTagsAsync();
|
var existingTags = await BrowseTagsAsync();
|
||||||
var existingByName = existingTags.Where(t => t.Name != null).ToDictionary(t => t.Name!.ToLowerInvariant(), t => t);
|
var existingByName = new Dictionary<string, GhostAdminTag>();
|
||||||
|
foreach (var t in existingTags.Where(t => t.Name != null))
|
||||||
|
existingByName.TryAdd(t.Name!.ToLowerInvariant(), t);
|
||||||
|
|
||||||
foreach (var cat in categories)
|
foreach (var cat in categories)
|
||||||
{
|
{
|
||||||
string name = cat.Name ?? "";
|
// WordPress liefert Namen HTML-kodiert (z.B. "Tipps & Tricks")
|
||||||
|
string name = System.Net.WebUtility.HtmlDecode(cat.Name ?? "").Trim();
|
||||||
if (string.IsNullOrEmpty(name)) continue;
|
if (string.IsNullOrEmpty(name)) continue;
|
||||||
if (existingByName.TryGetValue(name.ToLowerInvariant(), out var existing))
|
if (existingByName.TryGetValue(name.ToLowerInvariant(), out var existing))
|
||||||
{
|
{
|
||||||
result.Add(existing);
|
// Reihenfolge bleibt erhalten: der erste Tag ist in Ghost der primäre
|
||||||
|
if (!result.Contains(existing)) result.Add(existing);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -203,6 +214,7 @@ namespace wp2ghost
|
||||||
{
|
{
|
||||||
Console.WriteLine(" OK");
|
Console.WriteLine(" OK");
|
||||||
result.Add(newTag);
|
result.Add(newTag);
|
||||||
|
existingByName[name.ToLowerInvariant()] = newTag;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Console.WriteLine(" FEHLER");
|
Console.WriteLine(" FEHLER");
|
||||||
|
|
@ -262,7 +274,30 @@ namespace wp2ghost
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Posts (erweitert) =====================
|
// ===================== Posts (erweitert) =====================
|
||||||
public static async Task<GhostAdminPost?> CreatePostExtendedAsync(string title, string html, string status, string? slug, string? featureImage, List<GhostAdminTag> tags, string? metaKeywords = null)
|
private static readonly System.Text.RegularExpressions.Regex MetaKeywordsRegex = new(
|
||||||
|
@"<meta\s+name\s*=\s*[""']keywords[""'][^>]*>\s*",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ghost kennt kein meta_keywords-Feld -> Keywords als <meta name="keywords"> in den
|
||||||
|
/// Code-Injection-Header des Posts schreiben. Vorhandener Header-Code bleibt erhalten,
|
||||||
|
/// ein bereits vorhandenes Keywords-Meta wird ersetzt.
|
||||||
|
/// </summary>
|
||||||
|
public static string? BuildKeywordsHead(string? existingHead, IEnumerable<string> keywords)
|
||||||
|
{
|
||||||
|
var list = keywords
|
||||||
|
.Select(k => System.Net.WebUtility.HtmlDecode(k ?? "").Trim())
|
||||||
|
.Where(k => k.Length > 0)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
if (list.Count == 0) return null;
|
||||||
|
|
||||||
|
string rest = MetaKeywordsRegex.Replace(existingHead ?? "", "").Trim();
|
||||||
|
string meta = $"<meta name=\"keywords\" content=\"{System.Net.WebUtility.HtmlEncode(string.Join(", ", list))}\">";
|
||||||
|
return rest.Length > 0 ? rest + "\n" + meta : meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<GhostAdminPost?> CreatePostExtendedAsync(string title, string html, string status, string? slug, string? featureImage, List<GhostAdminTag> tags, string? codeinjectionHead = null)
|
||||||
{
|
{
|
||||||
var post = new GhostAdminPostEdit
|
var post = new GhostAdminPostEdit
|
||||||
{
|
{
|
||||||
|
|
@ -270,7 +305,7 @@ namespace wp2ghost
|
||||||
Html = html,
|
Html = html,
|
||||||
Status = status,
|
Status = status,
|
||||||
FeatureImage = featureImage,
|
FeatureImage = featureImage,
|
||||||
MetaKeywords = metaKeywords
|
CodeinjectionHead = codeinjectionHead
|
||||||
};
|
};
|
||||||
if (slug != null) post.Slug = slug;
|
if (slug != null) post.Slug = slug;
|
||||||
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
|
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
|
||||||
|
|
@ -290,7 +325,7 @@ namespace wp2ghost
|
||||||
return result?.Posts?.FirstOrDefault();
|
return result?.Posts?.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<GhostAdminPost?> UpdatePostExtendedAsync(string postId, string title, string html, string updatedAt, string status, string? featureImage, List<GhostAdminTag> tags, string? metaKeywords = null)
|
public static async Task<GhostAdminPost?> UpdatePostExtendedAsync(string postId, string title, string html, string updatedAt, string status, string? featureImage, List<GhostAdminTag> tags, string? codeinjectionHead = null)
|
||||||
{
|
{
|
||||||
var post = new GhostAdminPostEdit
|
var post = new GhostAdminPostEdit
|
||||||
{
|
{
|
||||||
|
|
@ -300,7 +335,7 @@ namespace wp2ghost
|
||||||
Status = status,
|
Status = status,
|
||||||
UpdatedAt = updatedAt,
|
UpdatedAt = updatedAt,
|
||||||
FeatureImage = featureImage,
|
FeatureImage = featureImage,
|
||||||
MetaKeywords = metaKeywords
|
CodeinjectionHead = codeinjectionHead
|
||||||
};
|
};
|
||||||
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
|
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
|
||||||
|
|
||||||
|
|
@ -522,6 +557,9 @@ namespace wp2ghost
|
||||||
|
|
||||||
[JsonPropertyName("featured")]
|
[JsonPropertyName("featured")]
|
||||||
public bool Featured { get; set; }
|
public bool Featured { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("codeinjection_head")]
|
||||||
|
public string? CodeinjectionHead { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GhostAdminPostEdit
|
public class GhostAdminPostEdit
|
||||||
|
|
@ -566,9 +604,9 @@ namespace wp2ghost
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? MetaDescription { get; set; }
|
public string? MetaDescription { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("meta_keywords")]
|
[JsonPropertyName("codeinjection_head")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? MetaKeywords { get; set; }
|
public string? CodeinjectionHead { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GhostImageUploadResponse
|
public class GhostImageUploadResponse
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace wp2ghost
|
||||||
[JsonPropertyName("slug")] public string? Slug { get; set; }
|
[JsonPropertyName("slug")] public string? Slug { get; set; }
|
||||||
[JsonPropertyName("html")] public string? Html { get; set; }
|
[JsonPropertyName("html")] public string? Html { get; set; }
|
||||||
[JsonPropertyName("markdown")] public string? Markdown { get; set; }
|
[JsonPropertyName("markdown")] public string? Markdown { get; set; }
|
||||||
|
[JsonPropertyName("feature_image")] public string? FeatureImage { get; set; }
|
||||||
[JsonPropertyName("status")] public string? Status { get; set; }
|
[JsonPropertyName("status")] public string? Status { get; set; }
|
||||||
[JsonPropertyName("created_at")] public string? CreatedAt { get; set; }
|
[JsonPropertyName("created_at")] public string? CreatedAt { get; set; }
|
||||||
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
|
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,11 @@ namespace wp2ghost
|
||||||
string src = GetBestImageUrl(img);
|
string src = GetBestImageUrl(img);
|
||||||
if (string.IsNullOrEmpty(src)) continue;
|
if (string.IsNullOrEmpty(src)) continue;
|
||||||
|
|
||||||
|
// Beste Variante als src setzen; srcset/sizes verweisen auf WordPress-Größenvarianten
|
||||||
|
img.SetAttributeValue("src", src);
|
||||||
|
img.Attributes.Remove("srcset");
|
||||||
|
img.Attributes.Remove("sizes");
|
||||||
|
|
||||||
var parentFigure = img.Ancestors("figure").FirstOrDefault();
|
var parentFigure = img.Ancestors("figure").FirstOrDefault();
|
||||||
if (parentFigure != null)
|
if (parentFigure != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace wp2ghost
|
namespace wp2ghost
|
||||||
{
|
{
|
||||||
|
|
@ -10,42 +10,64 @@ namespace wp2ghost
|
||||||
static class ImageManager
|
static class ImageManager
|
||||||
{
|
{
|
||||||
private static readonly HttpClient httpClient = new();
|
private static readonly HttpClient httpClient = new();
|
||||||
private static Dictionary<string, (string RelativePath, string LocalPath)> Cache = new();
|
// Schlüssel: Bild-Schlüssel (siehe GetImageKey) — alle WordPress-Varianten eines Bildes teilen sich einen Eintrag
|
||||||
private static Dictionary<string, string> WordpressToGhostImageName = new();
|
private static Dictionary<string, (string SourceUrl, string RelativePath, string LocalPath)> Cache = new();
|
||||||
private static List<(string RelativePath, string LocalPath)> FilesToZip = new();
|
private static List<(string RelativePath, string LocalPath)> FilesToZip = new();
|
||||||
private static string LocalImageDir = "";
|
private static string LocalImageDir = "";
|
||||||
private static string CachePath = "";
|
private static string CachePath = "";
|
||||||
|
|
||||||
|
private static readonly Regex SizeSuffixRegex = new(@"-\d+x\d+(?=\.\w+$)", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex ScaledSuffixRegex = new(@"-scaled(?=\.\w+$)", RegexOptions.Compiled);
|
||||||
|
|
||||||
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
|
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schlüssel, unter dem alle Varianten desselben WordPress-Bildes zusammenfallen:
|
||||||
|
/// Pfad ohne Host, Query (?w=1024), Größen-Suffix (-1024x683) und -scaled.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetImageKey(string url)
|
||||||
|
{
|
||||||
|
string path;
|
||||||
|
try { path = new Uri(url).AbsolutePath; }
|
||||||
|
catch { path = url.Split('?', '#')[0]; }
|
||||||
|
path = SizeSuffixRegex.Replace(path, "");
|
||||||
|
path = ScaledSuffixRegex.Replace(path, "");
|
||||||
|
return path.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
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 (TryGetCached(GetImageKey(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCached(string key, out (string RelativePath, string LocalPath) file)
|
||||||
|
{
|
||||||
|
if (Cache.TryGetValue(key, out var cached))
|
||||||
|
{
|
||||||
|
if (File.Exists(cached.LocalPath))
|
||||||
|
{
|
||||||
|
file = (cached.RelativePath, cached.LocalPath);
|
||||||
|
RegisterForZip(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Cache.Remove(key);
|
||||||
|
}
|
||||||
|
file = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterForZip((string RelativePath, string LocalPath) file)
|
||||||
|
{
|
||||||
|
if (!FilesToZip.Any(f => f.RelativePath == file.RelativePath))
|
||||||
|
FilesToZip.Add(file);
|
||||||
|
}
|
||||||
|
|
||||||
public static void Init(string baseDir)
|
public static void Init(string baseDir)
|
||||||
{
|
{
|
||||||
LocalImageDir = Path.Combine(baseDir, "images");
|
LocalImageDir = Path.Combine(baseDir, "images");
|
||||||
|
|
@ -63,10 +85,7 @@ 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.Value.SourceUrl}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}|{kvp.Key}");
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +108,7 @@ namespace wp2ghost
|
||||||
{
|
{
|
||||||
Console.WriteLine("\nLokal gespeicherte Bilder:");
|
Console.WriteLine("\nLokal gespeicherte Bilder:");
|
||||||
foreach (var kvp in Cache.Take(20))
|
foreach (var kvp in Cache.Take(20))
|
||||||
Console.WriteLine($" {kvp.Key} -> images/{kvp.Value.RelativePath}");
|
Console.WriteLine($" {kvp.Value.SourceUrl} -> images/{kvp.Value.RelativePath}");
|
||||||
if (Cache.Count > 20)
|
if (Cache.Count > 20)
|
||||||
Console.WriteLine($" ... und {Cache.Count - 20} weitere");
|
Console.WriteLine($" ... und {Cache.Count - 20} weitere");
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +122,7 @@ namespace wp2ghost
|
||||||
var uri = new Uri(url);
|
var uri = new Uri(url);
|
||||||
string fileName = Path.GetFileName(uri.LocalPath);
|
string fileName = Path.GetFileName(uri.LocalPath);
|
||||||
// WordPress-Sizes: -300x200, -scaled, -1024x768 etc.
|
// WordPress-Sizes: -300x200, -scaled, -1024x768 etc.
|
||||||
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, @"-\d+x\d+(?=\.\w+$)"))
|
if (SizeSuffixRegex.IsMatch(fileName))
|
||||||
return false;
|
return false;
|
||||||
if (fileName.Contains("-scaled"))
|
if (fileName.Contains("-scaled"))
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -112,86 +131,85 @@ namespace wp2ghost
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<(string RelativePath, string LocalPath)?> DownloadAsync(string imageUrl, DateTime? postDate)
|
/// <summary>
|
||||||
|
/// Lädt EIN Bild für eine Gruppe von URL-Varianten desselben WordPress-Bildes
|
||||||
|
/// (Original, -scaled, -1024x683, ?w=...). Es wird die beste verfügbare Variante geladen.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<(string RelativePath, string LocalPath)?> DownloadBestAsync(IEnumerable<string> variants, DateTime? postDate)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(imageUrl) || !Uri.IsWellFormedUriString(imageUrl, UriKind.Absolute))
|
// Query/Fragment entfernen: ?w=1024 & Co. liefern höchstens eine verkleinerte Fassung
|
||||||
return null;
|
var urls = variants
|
||||||
|
.Where(u => !string.IsNullOrEmpty(u))
|
||||||
|
.Select(u => u.Split('?', '#')[0])
|
||||||
|
.Where(u => Uri.IsWellFormedUriString(u, UriKind.Absolute))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
if (urls.Count == 0) return null;
|
||||||
|
|
||||||
string wpImageName = Path.GetFileNameWithoutExtension(new Uri(imageUrl).AbsolutePath);
|
string key = GetImageKey(urls[0]);
|
||||||
|
if (TryGetCached(key, out var cachedFile))
|
||||||
if (WordpressToGhostImageName.TryGetValue(wpImageName, out string? ghostRelativePath) && !string.IsNullOrEmpty(ghostRelativePath))
|
|
||||||
{
|
|
||||||
string localPath = Path.Combine(LocalImageDir, ghostRelativePath);
|
|
||||||
if (File.Exists(localPath))
|
|
||||||
{
|
|
||||||
Console.WriteLine($" [Bild] Bereits heruntergeladen (Wörterbuch): {wpImageName} → {ghostRelativePath}");
|
|
||||||
return (ghostRelativePath, localPath);
|
|
||||||
}
|
|
||||||
WordpressToGhostImageName.Remove(wpImageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Cache.TryGetValue(imageUrl, out var cachedFile))
|
|
||||||
{
|
|
||||||
if (File.Exists(cachedFile.LocalPath))
|
|
||||||
return cachedFile;
|
return cachedFile;
|
||||||
Cache.Remove(imageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Reihenfolge: Original -> -scaled -> erratenes Original -> Größenvarianten (größte zuerst)
|
||||||
|
var candidates = new List<string>();
|
||||||
|
candidates.AddRange(urls.Where(IsMainImage));
|
||||||
|
candidates.AddRange(urls.Where(u => ScaledSuffixRegex.IsMatch(u) && !SizeSuffixRegex.IsMatch(u)));
|
||||||
|
candidates.Add(ScaledSuffixRegex.Replace(SizeSuffixRegex.Replace(urls[0], ""), ""));
|
||||||
|
candidates.AddRange(urls.Where(u => SizeSuffixRegex.IsMatch(u)).OrderByDescending(GetVariantArea));
|
||||||
|
|
||||||
|
string lastError = "";
|
||||||
|
foreach (string candidate in candidates.Distinct())
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string fileName = Path.GetFileName(new Uri(imageUrl).AbsolutePath);
|
byte[] imageBytes = await httpClient.GetByteArrayAsync(candidate);
|
||||||
string urlHash = GetHashCode(imageUrl);
|
|
||||||
var datePrefix = DateTime.Now;
|
|
||||||
string datePath = $"{datePrefix:yyyy}/{datePrefix:MM}";
|
|
||||||
var localPath = Path.Combine(LocalImageDir, datePath);
|
|
||||||
if (!string.IsNullOrEmpty(localPath) && !Directory.Exists(localPath))
|
|
||||||
Directory.CreateDirectory(localPath);
|
|
||||||
|
|
||||||
var counter = 1 + new DirectoryInfo(localPath).GetFiles().Length;
|
var now = DateTime.Now;
|
||||||
var pDate = postDate ?? DateTime.Now;
|
string datePath = $"{now:yyyy}/{now:MM}";
|
||||||
string uniqueFileName = $"{pDate:yyyymmdd}_{counter}{Path.GetExtension(fileName)}";
|
string localDir = Path.Combine(LocalImageDir, datePath);
|
||||||
string relativePath = $"{datePath}/{uniqueFileName}";
|
if (!Directory.Exists(localDir))
|
||||||
string fallBackPath = $"{datePath}/{uniqueFileName}";
|
Directory.CreateDirectory(localDir);
|
||||||
|
|
||||||
|
// Eindeutigen Dateinamen bestimmen (nie eine vorhandene Datei wiederverwenden)
|
||||||
|
var pDate = postDate ?? now;
|
||||||
|
string ext = Path.GetExtension(new Uri(candidate).AbsolutePath);
|
||||||
|
int counter = 1 + new DirectoryInfo(localDir).GetFiles().Length;
|
||||||
|
string relativePath, localPath;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
relativePath = $"{datePath}/{pDate:yyyyMMdd}_{counter++}{ext}";
|
||||||
localPath = Path.Combine(LocalImageDir, relativePath);
|
localPath = Path.Combine(LocalImageDir, relativePath);
|
||||||
fallBackPath = Path.Combine(LocalImageDir, fallBackPath);
|
} while (File.Exists(localPath));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (File.Exists(fallBackPath))
|
|
||||||
{
|
|
||||||
File.Move(fallBackPath, localPath);
|
|
||||||
Console.WriteLine($" [Bild] Bereits vorhanden: {relativePath}");
|
|
||||||
}
|
|
||||||
else if (!File.Exists(localPath))
|
|
||||||
{
|
|
||||||
byte[] imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
|
|
||||||
await File.WriteAllBytesAsync(localPath, imageBytes);
|
await File.WriteAllBytesAsync(localPath, imageBytes);
|
||||||
Console.WriteLine($" [Bild] Heruntergeladen: {relativePath}");
|
Console.WriteLine($" [Bild] Heruntergeladen: {Path.GetFileName(new Uri(candidate).AbsolutePath)} → {relativePath}");
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($" [Bild] Bereits vorhanden: {relativePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
ResizeImage(localPath);
|
ResizeImage(localPath);
|
||||||
|
|
||||||
var fileResult = (relativePath, localPath);
|
var fileResult = (relativePath, localPath);
|
||||||
Cache[imageUrl] = fileResult;
|
Cache[key] = (candidate, relativePath, localPath);
|
||||||
WordpressToGhostImageName[wpImageName] = relativePath;
|
RegisterForZip(fileResult);
|
||||||
FilesToZip.Add(fileResult);
|
|
||||||
return fileResult;
|
return fileResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" [Bild-Fehler] Konnte {imageUrl} nicht laden: {ex.Message}");
|
lastError = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" [Bild-Fehler] Konnte {urls[0]} nicht laden: {lastError}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long GetVariantArea(string url)
|
||||||
|
{
|
||||||
|
var m = Regex.Match(url, @"-(\d+)x(\d+)(?=\.\w+$)");
|
||||||
|
return m.Success ? long.Parse(m.Groups[1].Value) * long.Parse(m.Groups[2].Value) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResizeImage(string localPath)
|
private static void ResizeImage(string localPath)
|
||||||
{
|
{
|
||||||
const int maxSize = 1024;
|
const int maxSize = 2048;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var image = Image.Load(localPath);
|
using var image = Image.Load(localPath);
|
||||||
|
|
@ -223,17 +241,6 @@ namespace wp2ghost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetHashCode(string input)
|
|
||||||
{
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
int hash = 17;
|
|
||||||
foreach (char c in input)
|
|
||||||
hash = hash * 23 + c;
|
|
||||||
return hash.ToString("x8");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void LoadCache()
|
private static void LoadCache()
|
||||||
{
|
{
|
||||||
if (!File.Exists(CachePath)) return;
|
if (!File.Exists(CachePath)) return;
|
||||||
|
|
@ -245,10 +252,8 @@ namespace wp2ghost
|
||||||
var parts = line.Split('|');
|
var parts = line.Split('|');
|
||||||
if (parts.Length >= 3)
|
if (parts.Length >= 3)
|
||||||
{
|
{
|
||||||
Cache[parts[0]] = (parts[1], parts[2]);
|
// Schlüssel immer aus der URL ableiten (ältere Cache-Dateien enthalten nur den Dateinamen)
|
||||||
// WordPress-Dateinamen auch ins Dictionary laden
|
Cache[GetImageKey(parts[0])] = (parts[0], parts[1], parts[2]);
|
||||||
if (parts.Length >= 4 && !string.IsNullOrEmpty(parts[3]))
|
|
||||||
WordpressToGhostImageName[parts[3]] = parts[1];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,71 +143,54 @@ namespace wp2ghost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle internen Bild-URLs aus dem HTML extrahieren (keine externen Links)
|
// Alle internen Bild-URLs im HTML (src, srcset, data-*, href) — keine externen Links
|
||||||
var htmlImageUrls = ExtractImageUrls(htmlContent, wpHost);
|
var htmlImageUrls = ExtractImageUrls(htmlContent, wpHost);
|
||||||
|
|
||||||
// Bevorzugte Media-Dateien ermitteln
|
// Varianten desselben Bildes (Original, -scaled, -1024x683, ?w=...) zusammenfassen:
|
||||||
var preferredMedia = GetPreferredMediaFiles(wpPost.MediaFiles, htmlImageUrls, wpHost);
|
// pro Bild wird genau EINE Datei geladen, auf die alle Varianten umgeschrieben werden.
|
||||||
|
var imageGroups = new Dictionary<string, List<string>>();
|
||||||
// Prüfe: Referenzierte Bilder im HTML, die NICHT in MediaFiles sind
|
var mediaUrls = (wpPost.MediaFiles ?? new()).Where(u => IsInternalUrl(u, wpHost)).ToList();
|
||||||
if (htmlImageUrls.Count > 0 && wpPost.MediaFiles != null)
|
if (!string.IsNullOrEmpty(wpPost.FeaturedImage))
|
||||||
|
mediaUrls.Add(wpPost.FeaturedImage);
|
||||||
|
foreach (string url in htmlImageUrls.Concat(mediaUrls))
|
||||||
{
|
{
|
||||||
var mediaFileNames = wpPost.MediaFiles.Select(m => GetFileNameFromUrl(m).ToLowerInvariant()).ToHashSet();
|
string key = ImageManager.GetImageKey(url);
|
||||||
foreach (var imgUrl in htmlImageUrls)
|
if (!imageGroups.TryGetValue(key, out var variants))
|
||||||
{
|
imageGroups[key] = variants = new List<string>();
|
||||||
string imgFileName = GetFileNameFromUrl(imgUrl).ToLowerInvariant();
|
variants.Add(url);
|
||||||
if (!mediaFileNames.Contains(imgFileName))
|
|
||||||
{
|
|
||||||
Console.WriteLine($" [Warnung] Bild im HTML nicht in MediaFiles: {Path.GetFileName(new Uri(imgUrl).AbsolutePath)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferredMedia.Count > 0)
|
var imageFiles = new List<string>();
|
||||||
|
var ghostPathByKey = new Dictionary<string, string>();
|
||||||
|
foreach (var group in imageGroups)
|
||||||
{
|
{
|
||||||
foreach (string mediaUrl in preferredMedia)
|
var downloaded = await ImageManager.DownloadBestAsync(group.Value, postDate);
|
||||||
{
|
if (!downloaded.HasValue) continue;
|
||||||
var downloaded = await ImageManager.DownloadAsync(mediaUrl, postDate);
|
ghostPathByKey[group.Key] = $"__GHOST_URL__/content/images/{downloaded.Value.RelativePath}";
|
||||||
if (downloaded.HasValue)
|
if (!imageFiles.Contains(downloaded.Value.RelativePath))
|
||||||
{
|
imageFiles.Add(downloaded.Value.RelativePath);
|
||||||
string ghostMediaPath = $"__GHOST_URL__/content/images/{downloaded.Value.RelativePath}";
|
|
||||||
htmlContent = htmlContent.Replace(mediaUrl, ghostMediaPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Längste URLs zuerst ersetzen, damit "bild.jpg" nicht in "bild.jpg?w=1024" hineinersetzt
|
||||||
|
foreach (string url in htmlImageUrls.OrderByDescending(u => u.Length))
|
||||||
|
{
|
||||||
|
if (ghostPathByKey.TryGetValue(ImageManager.GetImageKey(url), out string? ghostMediaPath))
|
||||||
|
htmlContent = htmlContent.Replace(url, ghostMediaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WordPress HTML zu Ghost-kompatibles HTML konvertieren (Galerien, Figures, etc.)
|
// WordPress HTML zu Ghost-kompatibles HTML konvertieren (Galerien, Figures, etc.)
|
||||||
htmlContent = HtmlGhostConverter.Convert(htmlContent);
|
htmlContent = HtmlGhostConverter.Convert(htmlContent);
|
||||||
|
|
||||||
// Lokale Bildpfade für JSON-Datei sammeln
|
int remainingWpImages = ExtractImageUrls(htmlContent, wpHost).Count;
|
||||||
var imageFiles = new List<string>();
|
if (remainingWpImages > 0)
|
||||||
if (preferredMedia.Count > 0)
|
Console.WriteLine($" [Warnung] {remainingWpImages} Bild-URL(s) zeigen weiterhin auf WordPress");
|
||||||
{
|
|
||||||
foreach (string mediaUrl in preferredMedia)
|
|
||||||
{
|
|
||||||
if (ImageManager.TryGetRelativePath(mediaUrl, out string relPath))
|
|
||||||
imageFiles.Add(relPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Featured Image verarbeiten
|
// Featured Image (wurde oben mit heruntergeladen)
|
||||||
string featureImageGhostPath = "";
|
string featureImageGhostPath = "";
|
||||||
if (!string.IsNullOrEmpty(wpPost.FeaturedImage))
|
if (!string.IsNullOrEmpty(wpPost.FeaturedImage)
|
||||||
|
&& ghostPathByKey.TryGetValue(ImageManager.GetImageKey(wpPost.FeaturedImage), out string? featPath))
|
||||||
{
|
{
|
||||||
if (ImageManager.TryGetRelativePath(wpPost.FeaturedImage, out string featRel))
|
featureImageGhostPath = featPath;
|
||||||
{
|
|
||||||
featureImageGhostPath = $"__GHOST_URL__/content/images/{featRel}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var featDownload = await ImageManager.DownloadAsync(wpPost.FeaturedImage, postDate);
|
|
||||||
if (featDownload.HasValue)
|
|
||||||
{
|
|
||||||
featureImageGhostPath = $"__GHOST_URL__/content/images/{featDownload.Value.RelativePath}";
|
|
||||||
if (!imageFiles.Contains(featDownload.Value.RelativePath))
|
|
||||||
imageFiles.Add(featDownload.Value.RelativePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ghostPost = new GhostPost
|
var ghostPost = new GhostPost
|
||||||
|
|
@ -216,6 +199,7 @@ namespace wp2ghost
|
||||||
Title = wpPost.Title,
|
Title = wpPost.Title,
|
||||||
Slug = wpPost.Slug,
|
Slug = wpPost.Slug,
|
||||||
Html = htmlContent,
|
Html = htmlContent,
|
||||||
|
FeatureImage = string.IsNullOrEmpty(featureImageGhostPath) ? null : featureImageGhostPath,
|
||||||
Status = "published",
|
Status = "published",
|
||||||
CreatedAt = postDate.ToString("o"),
|
CreatedAt = postDate.ToString("o"),
|
||||||
PublishedAt = postDate.ToString("o")
|
PublishedAt = postDate.ToString("o")
|
||||||
|
|
@ -285,83 +269,24 @@ namespace wp2ghost
|
||||||
// Console.WriteLine($"Markdown-Dateien gespeichert: {GhostPosts.Count} Dateien unter {MarkdownDir}");
|
// Console.WriteLine($"Markdown-Dateien gespeichert: {GhostPosts.Count} Dateien unter {MarkdownDir}");
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
// Bild-URL irgendwo im HTML: src, srcset, data-orig-file, data-large-file, href ...
|
||||||
|
// inkl. Query (?w=1024), damit beim Ersetzen nichts davon stehen bleibt.
|
||||||
|
private static readonly System.Text.RegularExpressions.Regex ImageUrlRegex = new(
|
||||||
|
@"https?://[^\s""'<>()]+?\.(?:jpe?g|png|gif|webp|svg|bmp|tiff?)(?:\?[^\s""'<>]*)?(?=[\s""'<>,)&\\]|$)",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
|
||||||
private static List<string> ExtractImageUrls(string html, string wpHost)
|
private static List<string> ExtractImageUrls(string html, string wpHost)
|
||||||
{
|
{
|
||||||
var urls = new List<string>();
|
var urls = new List<string>();
|
||||||
var imageExts = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".tiff", ".tif" };
|
foreach (System.Text.RegularExpressions.Match m in ImageUrlRegex.Matches(html))
|
||||||
|
|
||||||
// 1) <img src="..."> extrahieren
|
|
||||||
var imgMatches = System.Text.RegularExpressions.Regex.Matches(html, @"<img\s+[^>]*src=[""']([^""']+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in imgMatches)
|
|
||||||
{
|
{
|
||||||
string url = m.Groups[1].Value;
|
string url = m.Value;
|
||||||
if (!string.IsNullOrEmpty(url) && IsInternalUrl(url, wpHost) && !urls.Contains(url))
|
if (IsInternalUrl(url, wpHost) && !urls.Contains(url))
|
||||||
urls.Add(url);
|
urls.Add(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) data-orig-file (Vollbild) extrahieren
|
|
||||||
var origFileMatches = System.Text.RegularExpressions.Regex.Matches(html, @"data-orig-file=[""']([^""']+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in origFileMatches)
|
|
||||||
{
|
|
||||||
string url = m.Groups[1].Value;
|
|
||||||
if (!string.IsNullOrEmpty(url) && IsInternalUrl(url, wpHost) && !urls.Contains(url))
|
|
||||||
urls.Add(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) data-large-file extrahieren
|
|
||||||
var largeFileMatches = System.Text.RegularExpressions.Regex.Matches(html, @"data-large-file=[""']([^""']+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in largeFileMatches)
|
|
||||||
{
|
|
||||||
string url = m.Groups[1].Value;
|
|
||||||
if (!string.IsNullOrEmpty(url) && IsInternalUrl(url, wpHost) && !urls.Contains(url))
|
|
||||||
urls.Add(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) srcset (größte Variante pro img) extrahieren
|
|
||||||
var srcsetMatches = System.Text.RegularExpressions.Regex.Matches(html, @"srcset=[""']([^""']+)[""']", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in srcsetMatches)
|
|
||||||
{
|
|
||||||
string srcset = m.Groups[1].Value;
|
|
||||||
string widestUrl = GetWidestFromSrcset(srcset);
|
|
||||||
if (!string.IsNullOrEmpty(widestUrl) && IsInternalUrl(widestUrl, wpHost) && !urls.Contains(widestUrl))
|
|
||||||
urls.Add(widestUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) <a href="..."> mit Bildendung (Figure-Links auf Originalbild)
|
|
||||||
var aMatches = System.Text.RegularExpressions.Regex.Matches(html, @"<a\s+[^>]*href=[""']([^""']+)[""'][^>]*>", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in aMatches)
|
|
||||||
{
|
|
||||||
string url = m.Groups[1].Value;
|
|
||||||
if (string.IsNullOrEmpty(url)) continue;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string ext = Path.GetExtension(new Uri(url).AbsolutePath).ToLowerInvariant();
|
|
||||||
if (imageExts.Contains(ext) && IsInternalUrl(url, wpHost) && !urls.Contains(url))
|
|
||||||
urls.Add(url);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetWidestFromSrcset(string srcset)
|
|
||||||
{
|
|
||||||
string bestUrl = "";
|
|
||||||
int bestWidth = 0;
|
|
||||||
foreach (string part in srcset.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
string[] tokens = part.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (tokens.Length < 2) continue;
|
|
||||||
if (int.TryParse(tokens[1].Replace("w", ""), out int w) && w > bestWidth)
|
|
||||||
{
|
|
||||||
bestWidth = w;
|
|
||||||
bestUrl = tokens[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsInternalUrl(string url, string wpHost)
|
private static bool IsInternalUrl(string url, string wpHost)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(wpHost)) return true;
|
if (string.IsNullOrEmpty(wpHost)) return true;
|
||||||
|
|
@ -376,107 +301,6 @@ namespace wp2ghost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeImageUrl(string url)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var uri = new Uri(url);
|
|
||||||
string path = uri.AbsolutePath;
|
|
||||||
// Entferne -scaled UND Dimensionspostfixe für den Vergleich
|
|
||||||
path = System.Text.RegularExpressions.Regex.Replace(path, @"-scaled(?=\.\w+$)", "");
|
|
||||||
path = System.Text.RegularExpressions.Regex.Replace(path, @"-\d+x\d+(?=\.\w+$)", "");
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetFileNameFromUrl(string url)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Path.GetFileName(new Uri(url).AbsolutePath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> GetPreferredMediaFiles(List<string>? mediaFiles, List<string> htmlImageUrls, string wpHost)
|
|
||||||
{
|
|
||||||
var result = new List<string>();
|
|
||||||
var imageExts = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".tiff", ".tif" };
|
|
||||||
|
|
||||||
// Alle internen Bild-URLs zusammenführen: MediaFiles + HTML-referenzierte
|
|
||||||
var allUrls = new List<string>();
|
|
||||||
if (mediaFiles != null)
|
|
||||||
allUrls.AddRange(mediaFiles.Where(u => IsInternalUrl(u, wpHost)));
|
|
||||||
foreach (var u in htmlImageUrls)
|
|
||||||
{
|
|
||||||
if (!allUrls.Contains(u))
|
|
||||||
allUrls.Add(u);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allUrls.Count == 0)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
// Dateinamen aus den HTML-Links extrahieren
|
|
||||||
var htmlFileNames = htmlImageUrls.Select(u => GetFileNameFromUrl(u).ToLowerInvariant()).ToHashSet();
|
|
||||||
|
|
||||||
// Gruppiere nach normalisiertem Dateinamen (ohne -scaled und Dimensionspostfixe)
|
|
||||||
var grouped = new Dictionary<string, List<string>>();
|
|
||||||
foreach (var url in allUrls)
|
|
||||||
{
|
|
||||||
string normalized = NormalizeImageUrl(url);
|
|
||||||
if (!grouped.ContainsKey(normalized))
|
|
||||||
grouped[normalized] = new List<string>();
|
|
||||||
grouped[normalized].Add(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var group in grouped)
|
|
||||||
{
|
|
||||||
// Nur Hauptbilder berücksichtigen (keine WordPress-Sizes wie -300x200, keine -scaled)
|
|
||||||
var mainImages = group.Value.Where(u => ImageManager.IsMainImage(u)).ToList();
|
|
||||||
if (mainImages.Count == 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine($" [Bild] Übersprungen (kein Hauptbild): {Path.GetFileName(new Uri(@group.Value[0]).AbsolutePath)}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainImages.Count == 1)
|
|
||||||
{
|
|
||||||
result.Add(mainImages[0]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Prüfe ob eine der Varianten als <a href> im HTML referenziert wird
|
|
||||||
string? linked = mainImages.FirstOrDefault(u => htmlFileNames.Contains(GetFileNameFromUrl(u).ToLowerInvariant()));
|
|
||||||
|
|
||||||
if (linked != null)
|
|
||||||
{
|
|
||||||
result.Add(linked);
|
|
||||||
var skipped = mainImages.Where(u => u != linked).ToList();
|
|
||||||
foreach (var s in skipped)
|
|
||||||
Console.WriteLine($" [Bild] Übersprungen (nicht verlinkt): {Path.GetFileName(new Uri(s).AbsolutePath)}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Keine Verlinkung: Original (ohne -scaled) bevorzugen
|
|
||||||
var preferred = mainImages.FirstOrDefault(u => !u.Contains("-scaled")) ?? mainImages[0];
|
|
||||||
result.Add(preferred);
|
|
||||||
var skipped = mainImages.Where(u => u != preferred).ToList();
|
|
||||||
foreach (var s in skipped)
|
|
||||||
Console.WriteLine($" [Bild] Übersprungen (scaled): {Path.GetFileName(new Uri(s).AbsolutePath)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MarkProcessed(int wpId, string? wpSlug = null)
|
private static void MarkProcessed(int wpId, string? wpSlug = null)
|
||||||
{
|
{
|
||||||
ProcessedPosts.Add(wpId);
|
ProcessedPosts.Add(wpId);
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,16 @@ else
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Kategorie</label>
|
||||||
|
<select class="form-select" @bind="_categoryFilter" @bind:after="ApplyFilters">
|
||||||
|
<option value="">Alle Kategorien</option>
|
||||||
|
@foreach (var category in AvailableCategories)
|
||||||
|
{
|
||||||
|
<option value="@category.Key">@category.Value</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="col-md-4 d-flex align-items-end">
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
<button class="btn btn-outline-secondary me-2" @onclick="ResetFilters" title="Filter zurücksetzen">
|
<button class="btn btn-outline-secondary me-2" @onclick="ResetFilters" title="Filter zurücksetzen">
|
||||||
<i class="bi bi-x-circle"></i>
|
<i class="bi bi-x-circle"></i>
|
||||||
|
|
@ -118,18 +128,19 @@ else
|
||||||
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Feature)" @onclick="() => SetFilter(ImageFilter.Feature)">Feature (@_featureCount)</button>
|
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Feature)" @onclick="() => SetFilter(ImageFilter.Feature)">Feature (@_featureCount)</button>
|
||||||
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Content)" @onclick="() => SetFilter(ImageFilter.Content)">Content (@_contentCount)</button>
|
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Content)" @onclick="() => SetFilter(ImageFilter.Content)">Content (@_contentCount)</button>
|
||||||
<button class="btn btn-outline-danger @GetFilterClass(ImageFilter.Orphaned)" @onclick="() => SetFilter(ImageFilter.Orphaned)">Verwaist (@_orphanedCount)</button>
|
<button class="btn btn-outline-danger @GetFilterClass(ImageFilter.Orphaned)" @onclick="() => SetFilter(ImageFilter.Orphaned)">Verwaist (@_orphanedCount)</button>
|
||||||
|
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Category)" @onclick="() => SetFilter(ImageFilter.Category)">Kategorie (@_categoryCount)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<span class="text-muted small">
|
<span class="text-muted small">
|
||||||
Gefiltert: <strong>@_filteredImages.Count</strong> von @_allImages.Count
|
Gefiltert: <strong>@_filteredImages.Count</strong> von @_allImages.Count
|
||||||
@if (!string.IsNullOrWhiteSpace(_authorFilter) || !string.IsNullOrWhiteSpace(_themeFilter))
|
@if (!string.IsNullOrWhiteSpace(_authorFilter) || !string.IsNullOrWhiteSpace(_themeFilter) || !string.IsNullOrWhiteSpace(_categoryFilter))
|
||||||
{
|
{
|
||||||
<span class="ms-2">
|
<text>
|
||||||
(@(!string.IsNullOrWhiteSpace(_authorFilter) ? "Autor" : "")
|
@if (!string.IsNullOrWhiteSpace(_authorFilter)): @:Autor
|
||||||
@(!string.IsNullOrWhiteSpace(_authorFilter) && !string.IsNullOrWhiteSpace(_themeFilter) ? " + " : "")
|
@if (!string.IsNullOrWhiteSpace(_themeFilter)): @:Thema
|
||||||
@(!string.IsNullOrWhiteSpace(_themeFilter) ? "Thema" : ""))
|
@if (!string.IsNullOrWhiteSpace(_categoryFilter)): @:Kategorie
|
||||||
</span>
|
</text>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -437,6 +448,7 @@ else
|
||||||
private int _orphanedCount;
|
private int _orphanedCount;
|
||||||
private int _featureCount;
|
private int _featureCount;
|
||||||
private int _contentCount;
|
private int _contentCount;
|
||||||
|
private string _categoryFilter = "";
|
||||||
|
|
||||||
private GhostImage? _selectedImage;
|
private GhostImage? _selectedImage;
|
||||||
private bool _showReplaceDialog;
|
private bool _showReplaceDialog;
|
||||||
|
|
@ -518,6 +530,7 @@ else
|
||||||
_orphanedCount = _allImages.Count(i => i.IsOrphaned);
|
_orphanedCount = _allImages.Count(i => i.IsOrphaned);
|
||||||
_featureCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsFeatureImage));
|
_featureCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsFeatureImage));
|
||||||
_contentCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsInContent));
|
_contentCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsInContent));
|
||||||
|
_categoryCount = _allImages.Count(i => !string.IsNullOrEmpty(i.Category));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GhostImage> FilteredImages => _filteredImages;
|
private List<GhostImage> FilteredImages => _filteredImages;
|
||||||
|
|
@ -538,8 +551,16 @@ else
|
||||||
.OrderBy(t => t)
|
.OrderBy(t => t)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
private List<KeyValuePair<string, string>> AvailableCategories => _allImages
|
||||||
|
.Where(i => !string.IsNullOrEmpty(i.Category))
|
||||||
|
.GroupBy(i => i.Category!)
|
||||||
|
.Select(g => new KeyValuePair<string, string>(g.Key, g.Key))
|
||||||
|
.OrderBy(kvp => kvp.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
private int GetAuthorCount(string authorId) => _allImages.Count(i => i.AuthorId == authorId);
|
private int GetAuthorCount(string authorId) => _allImages.Count(i => i.AuthorId == authorId);
|
||||||
private int GetThemeCount(string theme) => _allImages.Count(i => i.Theme == theme);
|
private int GetThemeCount(string theme) => _allImages.Count(i => i.Theme == theme);
|
||||||
|
private int GetCategoryCount(string category) => _allImages.Count(i => i.Category == category);
|
||||||
|
|
||||||
private void ApplyFilters()
|
private void ApplyFilters()
|
||||||
{
|
{
|
||||||
|
|
@ -558,6 +579,9 @@ else
|
||||||
if (!string.IsNullOrWhiteSpace(_themeFilter))
|
if (!string.IsNullOrWhiteSpace(_themeFilter))
|
||||||
query = query.Where(i => i.Theme == _themeFilter);
|
query = query.Where(i => i.Theme == _themeFilter);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_categoryFilter))
|
||||||
|
query = query.Where(i => i.Category == _categoryFilter);
|
||||||
|
|
||||||
_filteredImages = query.ToList();
|
_filteredImages = query.ToList();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -566,6 +590,7 @@ else
|
||||||
{
|
{
|
||||||
_authorFilter = "";
|
_authorFilter = "";
|
||||||
_themeFilter = "";
|
_themeFilter = "";
|
||||||
|
_categoryFilter = "";
|
||||||
_searchTerm = "";
|
_searchTerm = "";
|
||||||
_currentFilter = ImageFilter.All;
|
_currentFilter = ImageFilter.All;
|
||||||
ApplyFilters();
|
ApplyFilters();
|
||||||
|
|
@ -858,6 +883,7 @@ else
|
||||||
All,
|
All,
|
||||||
Feature,
|
Feature,
|
||||||
Content,
|
Content,
|
||||||
Orphaned
|
Orphaned,
|
||||||
|
Category
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ public class AppDbContext : DbContext
|
||||||
entity.Property(e => e.Iso).HasMaxLength(50);
|
entity.Property(e => e.Iso).HasMaxLength(50);
|
||||||
entity.Property(e => e.FocalLength).HasMaxLength(50);
|
entity.Property(e => e.FocalLength).HasMaxLength(50);
|
||||||
entity.Property(e => e.Theme).HasMaxLength(500);
|
entity.Property(e => e.Theme).HasMaxLength(500);
|
||||||
|
entity.Property(e => e.Category).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.CategorySecondary).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.Keywords).HasMaxLength(500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,10 @@ public class ManagedImage
|
||||||
public DateTime FirstSeen { get; set; }
|
public DateTime FirstSeen { get; set; }
|
||||||
public DateTime LastUpdated { get; set; }
|
public DateTime LastUpdated { get; set; }
|
||||||
public bool IsOrphaned { get; set; }
|
public bool IsOrphaned { get; set; }
|
||||||
|
|
||||||
|
// Kategorien / Tags
|
||||||
|
public string? Category { get; set; } // Primäre Kategorie
|
||||||
|
public string? CategorySecondary { get; set; } // Sekundäre Kategorie (falls mehrere)
|
||||||
|
public bool HasMultipleCategories { get; set; }
|
||||||
|
public string? Keywords { get; set; } // HTML Meta Keywords
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,16 +175,26 @@ 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
|
// 4. Kategorien - bestimme das Primäre (erste Kategorie oder nach slug sortiert)
|
||||||
$categories = array();
|
$categories = array();
|
||||||
|
$primary_category = null;
|
||||||
$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)) {
|
||||||
|
// Sortiere nach term_id (älteste/erste Kategorie wird primär)
|
||||||
|
usort($cats, function($a, $b) {
|
||||||
|
return $a->term_id <=> $b->term_id;
|
||||||
|
});
|
||||||
foreach ($cats as $cat) {
|
foreach ($cats as $cat) {
|
||||||
|
$is_primary = ($cat->term_id === $cats[0]->term_id);
|
||||||
$categories[] = array(
|
$categories[] = array(
|
||||||
'id' => intval($cat->term_id),
|
'id' => intval($cat->term_id),
|
||||||
'name' => $cat->name,
|
'name' => $cat->name,
|
||||||
'slug' => $cat->slug,
|
'slug' => $cat->slug,
|
||||||
|
'primary' => $is_primary,
|
||||||
);
|
);
|
||||||
|
if ($is_primary) {
|
||||||
|
$primary_category = $cat->name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,6 +272,7 @@ function rbr_get_post_details_with_media($data) {
|
||||||
),
|
),
|
||||||
'featured_image' => $featured_image_url,
|
'featured_image' => $featured_image_url,
|
||||||
'categories' => $categories,
|
'categories' => $categories,
|
||||||
|
'primary_category' => $primary_category,
|
||||||
'tags' => $tags,
|
'tags' => $tags,
|
||||||
'keywords' => $keywords,
|
'keywords' => $keywords,
|
||||||
'comments' => $comments,
|
'comments' => $comments,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue