first commit

This commit is contained in:
Roland 2026-09-20 08:31:23 +02:00
commit 48d2de05ed
19 changed files with 3448 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/CSharpGhostImporter/.vs/*
/CSharpGhostImporter/bin/*
/CSharpGhostImporter/obj/*

3
AGENTS.md Normal file
View file

@ -0,0 +1,3 @@
Du arbeitest an einem Ghost-Projekt. Nutze bei Fragen zur API oder Dokumentation
immer das integrierte `webfetch`-Tool, um die aktuelle Referenz abzurufen:
- Hauptdokumentation: https://docs.ghost.org/llms-full.txt

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>CSharpGhostImporter</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.13.0" />
<PackageReference Include="jose-jwt" Version="5.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,635 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using wp2ghost;
class Program
{
private static string WpBaseUrl = "https://deine-domain.com";
internal static string OutputZipPath = "ghost_import.zip";
private static int MaxPosts = -1;
private static string ConfigPath = "";
private static string BaseDir = "";
private static string GhostUrl = "";
private static string GhostAdminApiKey = "";
private static string GhostContentApiKey = "";
static async Task Main(string[] args)
{
BaseDir = Directory.GetCurrentDirectory();
ConfigPath = Path.Combine(BaseDir, "config.txt");
LoadConfig();
ImageManager.Init(BaseDir);
PostFetcher.Init(BaseDir);
GhostExporter.Init(BaseDir);
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.Title = "WP2Ghost Converter";
bool running = true;
while (running)
{
ShowMenu();
string? choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
await PostFetcher.FetchFromWordPressAsync(WpBaseUrl, MaxPosts);
break;
case "2":
await GhostExporter.CreateZipAsync();
break;
case "3":
ConfigureSettings();
break;
case "4":
ShowCacheInfo();
break;
case "5":
PostFetcher.ClearProcessedPosts();
break;
case "6":
ImageManager.ClearAll();
break;
case "7":
await GhostMenuAsync();
break;
case "0":
ImageManager.SaveCache();
running = false;
Console.WriteLine("Programm beendet. Auf Wiedersehen!");
break;
default:
Console.WriteLine("Ungültige Auswahl. Bitte erneut eingeben.");
break;
}
}
}
static void ShowMenu()
{
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine(" WP2Ghost - WordPress to Ghost");
Console.WriteLine("=========================================");
Console.WriteLine($"WordPress-URL : {WpBaseUrl}");
Console.WriteLine($"Ausgabe-Datei : {OutputZipPath}");
Console.WriteLine($"Max. Beiträge/Seiten: {(MaxPosts == -1 ? "Alle" : MaxPosts.ToString())}");
Console.WriteLine($"Abgefragte Objekte : {PostFetcher.ProcessedCount}");
Console.WriteLine($"Ghost-URL : {(string.IsNullOrEmpty(GhostUrl) ? "(nicht konfiguriert)" : GhostUrl)}");
Console.WriteLine($"Ghost-Status : {(GhostApi.IsConfigured() ? "verbunden" : "nicht konfiguriert")}");
Console.WriteLine("-----------------------------------------");
Console.WriteLine(" 1 - Von WordPress abrufen (Posts & Pages)");
Console.WriteLine(" 2 - Ghost ZIP erstellen");
Console.WriteLine(" 3 - Einstellungen konfigurieren");
Console.WriteLine(" 4 - Cache-Informationen anzeigen");
Console.WriteLine(" 5 - Abgefragte Objekte zurücksetzen");
Console.WriteLine(" 6 - Bilder lokal löschen");
Console.WriteLine(" 7 - Beiträge direkt an Ghost senden");
Console.WriteLine(" 0 - Beenden");
Console.WriteLine("-----------------------------------------");
Console.Write("Auswahl: ");
}
static void LoadConfig()
{
if (!File.Exists(ConfigPath))
{
SaveConfig();
Console.WriteLine($"Neue Konfigurationsdatei erstellt: {ConfigPath}");
return;
}
try
{
var lines = File.ReadAllLines(ConfigPath);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
continue;
var parts = line.Split('=', 2);
if (parts.Length == 2)
{
string key = parts[0].Trim();
string value = parts[1].Trim();
switch (key.ToLower())
{
case "wpbaseurl":
WpBaseUrl = value;
break;
case "outputzippath":
OutputZipPath = value;
break;
case "maxposts":
if (int.TryParse(value, out int max))
MaxPosts = max;
break;
case "ghosturl":
GhostUrl = value;
break;
case "ghostadminapikey":
GhostAdminApiKey = value;
break;
case "ghostcontentapikey":
GhostContentApiKey = value;
break;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Laden der Konfiguration: {ex.Message}");
}
}
static void SaveConfig()
{
var lines = new List<string>
{
"# WP2Ghost Konfiguration",
"# Ändere die Werte und starte das Programm neu",
"",
$"wpBaseUrl={WpBaseUrl}",
$"outputZipPath={OutputZipPath}",
$"maxPosts={MaxPosts}",
"",
"# Ghost API (optional - für direkten Push)",
$"ghostUrl={GhostUrl}",
$"ghostAdminApiKey={GhostAdminApiKey}",
};
File.WriteAllLines(ConfigPath, lines);
}
static void ConfigureSettings()
{
Console.WriteLine();
Console.WriteLine("--- Einstellungen konfigurieren ---");
Console.Write($"WordPress-URL [{WpBaseUrl}]: ");
string? input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
WpBaseUrl = input;
Console.Write($"Ausgabe-Datei [{OutputZipPath}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
OutputZipPath = input;
Console.Write($"Maximale Anzahl Posts (leer = alle) [{(MaxPosts == -1 ? "alle" : MaxPosts.ToString())}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input) && int.TryParse(input, out int max))
MaxPosts = max;
Console.WriteLine();
Console.WriteLine("--- Ghost API (optional) ---");
Console.Write($"Ghost-URL [{GhostUrl}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostUrl = input;
Console.Write($"Ghost Admin API Key [{(string.IsNullOrEmpty(GhostAdminApiKey) ? "(nicht gesetzt)" : "****")}] : ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostAdminApiKey = input;
Console.Write($"Ghost Content API Key [{(string.IsNullOrEmpty(GhostContentApiKey) ? "(nicht gesetzt)" : "****")}] : ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostContentApiKey = input;
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
SaveConfig();
Console.WriteLine("Einstellungen gespeichert.");
}
static void ShowCacheInfo()
{
Console.WriteLine();
Console.WriteLine("--- Cache-Informationen ---");
Console.WriteLine($"Abgefragte Objekte: {PostFetcher.ProcessedCount}");
Console.WriteLine($"Ghost-Beiträge geladen: {GhostExporter.PostCount}");
ImageManager.ShowInfo();
PostFetcher.ShowInfo();
}
static async Task GhostMenuAsync()
{
if (!GhostApi.IsConfigured())
{
Console.WriteLine();
Console.WriteLine("Ghost API nicht konfiguriert. Bitte zuerst Option 3 (Einstellungen) nutzen.");
return;
}
bool inGhostMenu = true;
while (inGhostMenu)
{
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine(" Ghost Admin API");
Console.WriteLine("=========================================");
Console.WriteLine($" Ghost-URL : {GhostUrl}");
Console.WriteLine("-----------------------------------------");
Console.WriteLine(" 1 - Posts anzeigen (Ghost)");
Console.WriteLine(" 2 - Post lesen (nach ID)");
Console.WriteLine(" 3 - Post erstellen");
Console.WriteLine(" 4 - Post bearbeiten");
Console.WriteLine(" 5 - Post löschen");
Console.WriteLine(" 6 - Alle lokalen Posts an Ghost senden");
Console.WriteLine(" 7 - Bilder hochladen");
Console.WriteLine(" 8 - Post aus JSON erstellen/aktualisieren");
Console.WriteLine(" 0 - Zurück zum Hauptmenü");
Console.WriteLine("-----------------------------------------");
Console.Write("Auswahl: ");
string? choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
await GhostBrowsePostsAsync();
break;
case "2":
await GhostReadPostAsync();
break;
case "3":
await GhostCreatePostAsync();
break;
case "4":
await GhostEditPostAsync();
break;
case "5":
await GhostDeletePostAsync();
break;
case "6":
await GhostApi.PushAllPostsAsync(html => HtmlGhostConverter.Convert(html));
break;
case "7":
await GhostUploadImagesAsync();
break;
case "8":
await GhostCreateOrUpdateFromJsonAsync();
break;
case "0":
inGhostMenu = false;
break;
default:
Console.WriteLine("Ungültige Auswahl.");
break;
}
}
}
static async Task GhostBrowsePostsAsync()
{
Console.Write("Filter (leer = alle, z.B. status:published): ");
string? filter = Console.ReadLine()?.Trim();
Console.Write("Anzahl (leer = 50): ");
string? limitStr = Console.ReadLine()?.Trim();
int limit = int.TryParse(limitStr, out int l) ? l : 50;
Console.WriteLine();
Console.WriteLine("Lade Posts von Ghost...");
var posts = await GhostApi.BrowsePostsAsync(filter ?? "", limit);
if (posts.Count == 0)
{
Console.WriteLine("Keine Posts gefunden.");
return;
}
Console.WriteLine();
Console.WriteLine($" {"ID",-24} {"Status",-12} {"Titel"}");
Console.WriteLine($" {new string('-', 23)} {new string('-', 11)} {new string('-', 30)}");
foreach (var post in posts)
{
Console.WriteLine($" {post.Id,-24} {post.Status,-12} {post.Title}");
}
Console.WriteLine();
Console.WriteLine($" Gesamt: {posts.Count} Posts");
}
static async Task GhostReadPostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var post = await GhostApi.GetPostAsync(id);
if (post == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine();
Console.WriteLine($" ID : {post.Id}");
Console.WriteLine($" Titel : {post.Title}");
Console.WriteLine($" Slug : {post.Slug}");
Console.WriteLine($" Status : {post.Status}");
Console.WriteLine($" Erstellt : {post.CreatedAt}");
Console.WriteLine($" Aktualisiert: {post.UpdatedAt}");
Console.WriteLine($" Feature Img : {post.FeatureImage ?? "(keine)"}");
Console.WriteLine($" HTML ({(post.Html?.Length ?? 0)} Zeichen):");
Console.WriteLine($" {new string('-', 50)}");
string preview = post.Html ?? "";
if (preview.Length > 500) preview = preview[..500] + "...";
Console.WriteLine($" {preview}");
}
static async Task GhostCreatePostAsync()
{
Console.Write("Titel: ");
string? title = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(title)) return;
Console.Write("HTML-Inhalt: ");
string? html = Console.ReadLine()?.Trim();
Console.Write("Status (published/draft) [published]: ");
string? status = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(status)) status = "published";
Console.Write("Slug (leer = automatisch): ");
string? slug = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(slug)) slug = null;
Console.WriteLine();
Console.Write("Erstelle Post...");
var post = await GhostApi.CreatePostAsync(title, html ?? "", status, slug);
if (post != null)
Console.WriteLine($" OK (ID: {post.Id})");
else
Console.WriteLine(" FEHLER");
}
static async Task GhostEditPostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var existing = await GhostApi.GetPostAsync(id);
if (existing == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine($" Aktueller Titel: {existing.Title}");
Console.Write("Neuer Titel (Enter = unverändert): ");
string? title = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(title)) title = existing.Title;
Console.Write("Neuer HTML-Inhalt (Enter = unverändert): ");
string? html = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(html)) html = existing.Html;
Console.Write($"Neuer Status ({existing.Status}): ");
string? status = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(status)) status = existing.Status;
Console.WriteLine();
Console.Write("Aktualisiere Post...");
var updated = await GhostApi.UpdatePostAsync(id, title ?? "", html ?? "", existing.UpdatedAt ?? "", status ?? "published");
if (updated != null)
Console.WriteLine(" OK");
else
Console.WriteLine(" FEHLER");
}
static async Task GhostDeletePostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var post = await GhostApi.GetPostAsync(id);
if (post == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine($" Post: {post.Title}");
Console.Write("Wirklich löschen? (j/n): ");
string? confirm = Console.ReadLine()?.Trim();
if (confirm?.ToLower() != "j") return;
Console.Write("Lösche Post...");
bool success = await GhostApi.DeletePostAsync(id);
Console.WriteLine(success ? " OK" : " FEHLER");
}
static async Task GhostUploadImagesAsync()
{
var cachedFiles = ImageManager.CachedFiles;
if (cachedFiles.Count == 0)
{
Console.WriteLine("Keine Bilder im Cache. Bitte zuerst Option 1 (Abruf) nutzen.");
return;
}
Console.WriteLine($" {cachedFiles.Count} Bilder zum Hochladen vorhanden.");
Console.Write("Alle Bilder hochladen? (j/n): ");
string? confirm = Console.ReadLine()?.Trim();
if (confirm?.ToLower() != "j") return;
int uploaded = 0;
foreach (var file in cachedFiles)
{
Console.Write($" [Upload] {Path.GetFileName(file.LocalPath)}...");
string? url = await GhostApi.UploadImageAsync(file.LocalPath);
if (url != null)
{
Console.WriteLine($" -> {url}");
uploaded++;
}
else
Console.WriteLine(" FEHLER");
}
Console.WriteLine();
Console.WriteLine($" Hochgeladen: {uploaded}/{cachedFiles.Count}");
}
static async Task GhostCreateOrUpdateFromJsonAsync()
{
string ghostPostsDir = Path.Combine(BaseDir, "ghostPosts");
if (!Directory.Exists(ghostPostsDir))
{
Console.WriteLine("Verzeichnis 'ghostPosts' nicht gefunden.");
return;
}
var jsonFiles = Directory.GetFiles(ghostPostsDir, "*.json");
if (jsonFiles.Length == 0)
{
Console.WriteLine("Keine JSON-Dateien in ghostPosts/ vorhanden.");
return;
}
Console.WriteLine($" {jsonFiles.Length} verfügbare Posts:");
foreach (var f in jsonFiles)
{
string id = Path.GetFileNameWithoutExtension(f);
Console.Write($" {id} - ");
try
{
var preview = JsonSerializer.Deserialize<GhostPostFile>(File.ReadAllText(f));
Console.WriteLine(preview?.Title ?? "(kein Titel)");
}
catch { Console.WriteLine("(Fehler beim Lesen)"); }
}
Console.Write("Ghost-ID des Posts: ");
string? ghostId = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(ghostId)) return;
string jsonPath = Path.Combine(ghostPostsDir, $"{ghostId}.json");
if (!File.Exists(jsonPath))
{
Console.WriteLine($"Datei {jsonPath} nicht gefunden.");
return;
}
var postFile = JsonSerializer.Deserialize<GhostPostFile>(File.ReadAllText(jsonPath));
if (postFile == null)
{
Console.WriteLine("JSON konnte nicht gelesen werden.");
return;
}
Console.WriteLine($" Post: {postFile.Title}");
Console.WriteLine($" Slug: {postFile.Slug}");
Console.WriteLine($" Kategorien: {postFile.Categories.Count}");
Console.WriteLine($" Kommentare: {postFile.Comments.Count}");
Console.WriteLine($" Bilder: {postFile.ImageFiles.Count}");
// 1. ALLE Bilder hochladen + Ghost-URL-Mapping erstellen
var ghostUrlMap = new Dictionary<string, string>();
string html = postFile.Html ?? "";
if (postFile.ImageFiles.Count > 0)
{
Console.WriteLine($" [Bilder] Lade {postFile.ImageFiles.Count} Bilder hoch...");
foreach (string relPath in postFile.ImageFiles)
{
string localPath = Path.Combine(BaseDir, "images", relPath);
if (!File.Exists(localPath))
{
Console.WriteLine($" [Übersprungen] {relPath} (nicht lokal)");
continue;
}
Console.Write($" [Upload] {Path.GetFileName(localPath)}...");
string? url = await GhostApi.UploadImageIfNewAsync(localPath);
if (url != null)
{
Console.WriteLine($" -> {url}");
// Backslash-normalisiert für alte JSONs
string normalized = relPath.Replace("\\", "/");
string placeholder1 = $"__GHOST_URL__/content/images/{normalized}";
string placeholder2 = $"__GHOST_URL__/content/images/{relPath}";
ghostUrlMap[placeholder1] = url;
if (placeholder1 != placeholder2)
ghostUrlMap[placeholder2] = url;
}
else
Console.WriteLine(" FEHLER");
}
}
// 2. Featured Image hochladen (separat, da nicht in ImageFiles)
string? featureImageUrl = null;
if (!string.IsNullOrEmpty(postFile.FeatureImage))
{
string localFeatPath = postFile.FeatureImage.Replace("__GHOST_URL__/content/images/", "");
localFeatPath = Path.Combine(BaseDir, "images", localFeatPath);
if (File.Exists(localFeatPath))
{
Console.Write(" [Featured Image]...");
featureImageUrl = await GhostApi.UploadImageIfNewAsync(localFeatPath);
Console.WriteLine(featureImageUrl != null ? $" OK" : " FEHLER");
if (featureImageUrl != null)
ghostUrlMap[postFile.FeatureImage] = featureImageUrl;
}
}
// 3. __GHOST_URL__ im HTML durch echte URLs ersetzen
foreach (var kvp in ghostUrlMap)
html = html.Replace(kvp.Key, kvp.Value);
// Falls noch Rest Platzhalter → Warnung
if (html.Contains("__GHOST_URL__"))
{
int remaining = System.Text.RegularExpressions.Regex.Matches(html, "__GHOST_URL__").Count;
Console.WriteLine($" [Warnung] {remaining} unrestrierte __GHOST_URL__ Platzhalter im HTML");
}
// 4. Tags erstellen/zuordnen
var tags = new List<GhostAdminTag>();
if (postFile.Categories.Count > 0)
{
Console.WriteLine(" [Tags] Verarbeite Kategorien...");
tags = await GhostApi.EnsureTagsAsync(postFile.Categories);
}
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
Console.Write(" Prüfe Ghost...");
var existingPosts = await GhostApi.BrowsePostsAsync($"slug:{postFile.Slug}", 1);
var existing = existingPosts.FirstOrDefault();
GhostAdminPost? result = null;
if (existing != null)
{
Console.WriteLine($" vorhanden (ID: {existing.Id})");
Console.Write(" Aktualisiere...");
result = await GhostApi.UpdatePostExtendedAsync(
existing.Id!, postFile.Title ?? "", html,
existing.UpdatedAt ?? "", postFile.Status ?? "published",
featureImageUrl, tags, postFile.Keywords);
Console.WriteLine(result != null ? " OK" : " FEHLER");
}
else
{
Console.WriteLine(" nicht vorhanden");
Console.Write(" Erstelle neuen Post...");
result = await GhostApi.CreatePostExtendedAsync(
postFile.Title ?? "", html,
postFile.Status ?? "published", postFile.Slug,
featureImageUrl, tags, postFile.Keywords);
Console.WriteLine(result != null ? $" OK (ID: {result.Id})" : " FEHLER");
}
// 4. Kommentare als Members verarbeiten
if (postFile.Comments.Count > 0)
{
Console.WriteLine();
Console.Write($" {postFile.Comments.Count} Kommentare verarbeiten? (j/n): ");
string? commentConfirm = Console.ReadLine()?.Trim();
if (commentConfirm?.ToLower() == "j")
{
foreach (var comment in postFile.Comments)
{
if (string.IsNullOrEmpty(comment.AuthorEmail)) continue;
var member = await GhostApi.EnsureMemberAsync(comment.AuthorName ?? "Anonymous", comment.AuthorEmail);
if (member != null)
{
Console.WriteLine($" [Kommentar] {comment.AuthorName}: {comment.Content?.Substring(0, Math.Min(50, comment.Content?.Length ?? 0))}...");
}
}
}
}
}
}

View file

@ -0,0 +1,662 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace wp2ghost
{
// =============================================================================
// GhostApi — Ghost Admin API: JWT-Auth, Bilder hochladen, Posts erstellen/bearbeiten
// =============================================================================
public static class GhostApi
{
private static readonly HttpClient httpClient = new();
private static string GhostUrl = "";
private static string AdminApiKey = "";
private static string ContentApiKey = "";
private static int ImageContainerPathParts = 0;
public static void Init(string ghostUrl, string adminApiKey, string contentApiKey, string imageContainerPath)
{
GhostUrl = ghostUrl.TrimEnd('/');
AdminApiKey = adminApiKey;
ContentApiKey = contentApiKey;
ImageContainerPathParts = Path.GetFullPath(imageContainerPath).Split(Path.DirectorySeparatorChar).Length;
}
public static bool IsConfigured()
{
return !string.IsNullOrEmpty(GhostUrl) && !string.IsNullOrEmpty(AdminApiKey);
}
private static string GenerateToken()
{
var parts = AdminApiKey.Split(':', 2);
if (parts.Length != 2)
throw new ArgumentException("Admin API Key muss im Format {id}:{secret} sein.");
string keyId = parts[0];
string secret = parts[1];
var now = DateTimeOffset.UtcNow;
var header = new Dictionary<string, object>
{
{ "alg", "HS256" },
{ "typ", "JWT" },
{ "kid", keyId }
};
var payload = new Dictionary<string, object>
{
{ "iat", now.ToUnixTimeSeconds() },
{ "exp", now.ToUnixTimeSeconds() + 300 },
{ "aud", "/admin/" }
};
byte[] secretBytes = Convert.FromHexString(secret);
return Jose.JWT.Encode(payload, secretBytes, Jose.JwsAlgorithm.HS256, extraHeaders: header);
}
private static async Task<HttpRequestMessage> CreateRequestAsync(HttpMethod method, string endpoint)
{
string token = GenerateToken();
var request = new HttpRequestMessage(method, $"{GhostUrl}/ghost/api/admin/{endpoint}");
request.Headers.Authorization = new AuthenticationHeaderValue("Ghost", token);
request.Headers.Add("Accept-Version", "v5.0");
return request;
}
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";
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
return result?.Posts ?? new List<GhostAdminPost>();
}
public static async Task<GhostAdminPost?> GetPostAsync(string postId)
{
var request = await CreateRequestAsync(HttpMethod.Get, $"posts/{postId}/?fields=id,title,slug,status,created_at,updated_at,html,feature_image,featured");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
return result?.Posts?.FirstOrDefault();
}
public static async Task<GhostAdminPost?> CreatePostAsync(string title, string html, string status = "published", string? slug = null)
{
var post = new GhostAdminPostEdit
{
Title = title,
Html = html,
Status = status
};
if (slug != null) post.Slug = slug;
var body = new { posts = new[] { post } };
var request = await CreateRequestAsync(HttpMethod.Post, "posts/?source=html");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
return result?.Posts?.FirstOrDefault();
}
public static async Task<GhostAdminPost?> UpdatePostAsync(string postId, string title, string html, string updatedAt, string status = "published")
{
var post = new GhostAdminPostEdit
{
Id = postId,
Title = title,
Html = html,
Status = status,
UpdatedAt = updatedAt
};
var body = new { posts = new[] { post } };
var request = await CreateRequestAsync(HttpMethod.Put, $"posts/{postId}/?source=html");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
return result?.Posts?.FirstOrDefault();
}
public static async Task<bool> DeletePostAsync(string postId)
{
var request = await CreateRequestAsync(HttpMethod.Delete, $"posts/{postId}/");
var response = await httpClient.SendAsync(request);
return response.IsSuccessStatusCode;
}
// ===================== Tags =====================
public static async Task<List<GhostAdminTag>> BrowseTagsAsync(string filter = "", int limit = 100)
{
string endpoint = $"tags/?limit={limit}&fields=id,name,slug";
if (!string.IsNullOrEmpty(filter))
endpoint += $"&filter={Uri.EscapeDataString(filter)}";
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode) return new();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminTagsResponse>(json);
return result?.Tags ?? new();
}
public static async Task<GhostAdminTag?> CreateTagAsync(string name, string? slug = null)
{
var tag = new GhostAdminTagEdit { Name = name };
if (slug != null) tag.Slug = slug;
var body = new { tags = new[] { tag } };
var request = await CreateRequestAsync(HttpMethod.Post, "tags/");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] Tag erstellen: {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminTagsResponse>(json);
return result?.Tags?.FirstOrDefault();
}
public static async Task<List<GhostAdminTag>> EnsureTagsAsync(List<WpCategory> categories)
{
var result = new List<GhostAdminTag>();
var existingTags = await BrowseTagsAsync();
var existingByName = existingTags.Where(t => t.Name != null).ToDictionary(t => t.Name!.ToLowerInvariant(), t => t);
foreach (var cat in categories)
{
string name = cat.Name ?? "";
if (string.IsNullOrEmpty(name)) continue;
if (existingByName.TryGetValue(name.ToLowerInvariant(), out var existing))
{
result.Add(existing);
}
else
{
Console.Write($" [Tag] Erstelle '{name}'...");
var newTag = await CreateTagAsync(name, cat.Slug);
if (newTag != null)
{
Console.WriteLine(" OK");
result.Add(newTag);
}
else
Console.WriteLine(" FEHLER");
}
}
return result;
}
// ===================== Members =====================
public static async Task<List<GhostAdminMember>> BrowseMembersAsync(string filter = "", int limit = 100)
{
string endpoint = $"members/?limit={limit}&fields=id,name,email,status";
if (!string.IsNullOrEmpty(filter))
endpoint += $"&filter={Uri.EscapeDataString(filter)}";
var request = await CreateRequestAsync(HttpMethod.Get, endpoint);
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode) return new();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminMembersResponse>(json);
return result?.Members ?? new();
}
public static async Task<GhostAdminMember?> CreateMemberAsync(string name, string email)
{
var member = new GhostAdminMemberEdit
{
Name = name,
Email = email,
Status = "free"
};
var body = new { members = new[] { member } };
var request = await CreateRequestAsync(HttpMethod.Post, "members/");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] Member erstellen: {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminMembersResponse>(json);
return result?.Members?.FirstOrDefault();
}
public static async Task<GhostAdminMember?> EnsureMemberAsync(string name, string email)
{
if (string.IsNullOrEmpty(email)) return null;
var existing = await BrowseMembersAsync($"email:'{email}'");
var found = existing.FirstOrDefault(m => m.Email?.ToLowerInvariant() == email.ToLowerInvariant());
if (found != null) return found;
Console.Write($" [Member] Erstelle '{name}' ({email})...");
var newMember = await CreateMemberAsync(name, email);
Console.WriteLine(newMember != null ? " OK" : " FEHLER");
return newMember;
}
// ===================== Posts (erweitert) =====================
public static async Task<GhostAdminPost?> CreatePostExtendedAsync(string title, string html, string status, string? slug, string? featureImage, List<GhostAdminTag> tags, string? metaKeywords = null)
{
var post = new GhostAdminPostEdit
{
Title = title,
Html = html,
Status = status,
FeatureImage = featureImage,
MetaKeywords = metaKeywords
};
if (slug != null) post.Slug = slug;
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
var body = new { posts = new[] { post } };
var request = await CreateRequestAsync(HttpMethod.Post, "posts/?source=html");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
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)
{
var post = new GhostAdminPostEdit
{
Id = postId,
Title = title,
Html = html,
Status = status,
UpdatedAt = updatedAt,
FeatureImage = featureImage,
MetaKeywords = metaKeywords
};
if (tags.Count > 0) post.Tags = tags.Select(t => new GhostAdminTagRef { Id = t.Id, Name = t.Name }).ToList();
var body = new { posts = new[] { post } };
var request = await CreateRequestAsync(HttpMethod.Put, $"posts/{postId}/?source=html");
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostAdminPostsResponse>(json);
return result?.Posts?.FirstOrDefault();
}
public static async Task<string?> UploadImageAsync(string localPath, DateTime? postDate = null)
{
if (!File.Exists(localPath))
{
Console.WriteLine($" [Ghost-Fehler] Datei nicht gefunden: {localPath}");
return null;
}
var request = await CreateRequestAsync(HttpMethod.Post, "images/upload/");
string fileName = Path.GetFileName(localPath);
var fileBytes = await File.ReadAllBytesAsync(localPath);
var fileContent = new ByteArrayContent(fileBytes);
var pathParts = Path.GetFullPath(localPath).Split("\\");
string ext = Path.GetExtension(fileName).ToLowerInvariant();
string mimeType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".webp" => "image/webp",
".svg" => "image/svg+xml",
_ => "application/octet-stream"
};
fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
var formContent = new MultipartFormDataContent();
formContent.Add(fileContent, "file", fileName);
if (pathParts.Length > ImageContainerPathParts + 1)
{
string refPath = string.Join("/", pathParts.Skip(ImageContainerPathParts).Take(pathParts.Length - ImageContainerPathParts - 1));
if (!string.IsNullOrEmpty(refPath))
formContent.Add(new StringContent(refPath), "ref");
}
request.Content = formContent;
var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($" [Ghost-Fehler] Upload fehlgeschlagen: {response.StatusCode}: {error}");
return null;
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GhostImageUploadResponse>(json);
return result?.Images?.FirstOrDefault()?.Url;
}
public static async Task<bool> ImageExistsAsync(string ghostUrl)
{
if (string.IsNullOrEmpty(ghostUrl) || !ghostUrl.StartsWith("http"))
return false;
try
{
var request = new HttpRequestMessage(HttpMethod.Head, ghostUrl);
var response = await httpClient.SendAsync(request);
return response.IsSuccessStatusCode;
}
catch { return false; }
}
public static async Task<string?> UploadImageIfNewAsync(string localPath, DateTime? postDate = null)
{
if (!File.Exists(localPath))
{
Console.WriteLine($" [Ghost-Fehler] Datei nicht gefunden: {localPath}");
return null;
}
string fileName = Path.GetFileName(localPath);
var pathParts = Path.GetFullPath(localPath).Split(Path.DirectorySeparatorChar);
string refPath = "";
if (pathParts.Length > ImageContainerPathParts + 1)
refPath = string.Join("/", pathParts.Skip(ImageContainerPathParts).Take(pathParts.Length - ImageContainerPathParts - 1));
string expectedUrl = $"{GhostUrl}/content/images/{refPath.Replace("\\", "/")}/{fileName}";
if (await ImageExistsAsync(expectedUrl))
{
Console.Write($" [Upload] {fileName} existiert bereits, überspringen");
return expectedUrl;
}
return await UploadImageAsync(localPath, postDate);
}
public static async Task<int> PushAllPostsAsync(Func<string, string> htmlTransformer)
{
if (!IsConfigured())
{
Console.WriteLine("Ghost API nicht konfiguriert. Bitte ghostUrl und ghostAdminApiKey in config.txt setzen.");
return 0;
}
Console.WriteLine();
Console.WriteLine("Sende Beiträge an Ghost...");
var localPosts = PostFetcher.Posts;
if (localPosts.Count == 0)
{
Console.WriteLine("Keine Beiträge geladen. Bitte zuerst Option 1 ausführen.");
return 0;
}
var ghostPosts = await BrowsePostsAsync();
var ghostBySlug = ghostPosts.Where(p => p.Slug != null).ToDictionary(p => p.Slug!, p => p);
var ghostByTitle = ghostPosts.Where(p => p.Title != null).ToDictionary(p => p.Title!, p => p);
int created = 0, updated = 0, skipped = 0;
foreach (var localPost in localPosts)
{
string title = localPost.Title ?? "";
string slug = localPost.Slug ?? "";
string html = htmlTransformer(localPost.Html ?? "");
GhostAdminPost? existing = null;
if (!string.IsNullOrEmpty(slug) && ghostBySlug.ContainsKey(slug))
existing = ghostBySlug[slug];
else if (!string.IsNullOrEmpty(title) && ghostByTitle.ContainsKey(title))
existing = ghostByTitle[title];
if (existing != null)
{
if (existing.UpdatedAt == localPost.CreatedAt)
{
skipped++;
continue;
}
Console.Write($" [Update] {title}...");
var updatedPost = await UpdatePostAsync(existing.Id!, title, html, existing.UpdatedAt ?? "");
if (updatedPost != null)
{
Console.WriteLine(" OK");
updated++;
}
else
Console.WriteLine(" FEHLER");
}
else
{
Console.Write($" [Neu] {title}...");
var newPost = await CreatePostAsync(title, html, "published", slug);
if (newPost != null)
{
Console.WriteLine(" OK");
created++;
}
else
Console.WriteLine(" FEHLER");
}
}
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine($" Ghost: {created} erstellt, {updated} aktualisiert, {skipped} übersprungen");
Console.WriteLine("=========================================");
return created + updated;
}
}
// =============================================================================
// Ghost Admin API DTOs
// =============================================================================
public class GhostAdminPostsResponse
{
[JsonPropertyName("posts")]
public List<GhostAdminPost>? Posts { get; set; }
}
public class GhostAdminPost
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("slug")]
public string? Slug { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
[JsonPropertyName("html")]
public string? Html { get; set; }
[JsonPropertyName("created_at")]
public string? CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public string? UpdatedAt { get; set; }
[JsonPropertyName("feature_image")]
public string? FeatureImage { get; set; }
[JsonPropertyName("featured")]
public bool Featured { get; set; }
}
public class GhostAdminPostEdit
{
[JsonPropertyName("id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Id { get; set; }
[JsonPropertyName("title")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Title { get; set; }
[JsonPropertyName("slug")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Slug { get; set; }
[JsonPropertyName("html")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Html { get; set; }
[JsonPropertyName("status")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Status { get; set; }
[JsonPropertyName("updated_at")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? UpdatedAt { get; set; }
[JsonPropertyName("feature_image")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FeatureImage { get; set; }
[JsonPropertyName("tags")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<GhostAdminTagRef>? Tags { get; set; }
[JsonPropertyName("meta_title")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MetaTitle { get; set; }
[JsonPropertyName("meta_description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MetaDescription { get; set; }
[JsonPropertyName("meta_keywords")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MetaKeywords { get; set; }
}
public class GhostImageUploadResponse
{
[JsonPropertyName("images")]
public List<GhostImageUploadResult>? Images { get; set; }
}
public class GhostImageUploadResult
{
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("ref")]
public string? Ref { get; set; }
}
// ===================== Tag DTOs =====================
public class GhostAdminTagsResponse
{
[JsonPropertyName("tags")]
public List<GhostAdminTag>? Tags { get; set; }
}
public class GhostAdminTag
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("slug")]
public string? Slug { get; set; }
}
public class GhostAdminTagEdit
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("slug")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Slug { get; set; }
}
public class GhostAdminTagRef
{
[JsonPropertyName("id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Id { get; set; }
[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Name { get; set; }
}
// ===================== Member DTOs =====================
public class GhostAdminMembersResponse
{
[JsonPropertyName("members")]
public List<GhostAdminMember>? Members { get; set; }
}
public class GhostAdminMember
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
}
public class GhostAdminMemberEdit
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
}
}

View file

@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace wp2ghost
{
public class GhostRoot { [JsonPropertyName("db")] public List<GhostDb> Db { get; set; } = new() { new GhostDb() }; }
public class GhostDb { [JsonPropertyName("data")] public GhostData Data { get; set; } = new(); }
public class GhostData { [JsonPropertyName("posts")] public List<GhostPost> Posts { get; set; } = new(); }
public class GhostPost
{
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("title")] public string? Title { get; set; }
[JsonPropertyName("slug")] public string? Slug { get; set; }
[JsonPropertyName("html")] public string? Html { get; set; }
[JsonPropertyName("markdown")] public string? Markdown { get; set; }
[JsonPropertyName("status")] public string? Status { get; set; }
[JsonPropertyName("created_at")] public string? CreatedAt { get; set; }
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
}
public class GhostPostFile
{
[JsonPropertyName("wp_id")] public int WpId { get; set; }
[JsonPropertyName("wp_slug")] public string? WpSlug { get; set; }
[JsonPropertyName("ghost_id")] public string? GhostId { get; set; }
[JsonPropertyName("title")] public string? Title { get; set; }
[JsonPropertyName("slug")] public string? Slug { get; set; }
[JsonPropertyName("excerpt")] public string? Excerpt { get; set; }
[JsonPropertyName("status")] public string? Status { get; set; }
[JsonPropertyName("created_at")] public string? CreatedAt { get; set; }
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
[JsonPropertyName("feature_image")] public string? FeatureImage { get; set; }
[JsonPropertyName("categories")] public List<WpCategory> Categories { get; set; } = new();
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
[JsonPropertyName("comments")] public List<WpComment> Comments { get; set; } = new();
[JsonPropertyName("image_files")] public List<string> ImageFiles { get; set; } = new();
[JsonPropertyName("html")] public string? Html { get; set; }
}
}

View file

@ -0,0 +1,107 @@
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace wp2ghost
{
// =============================================================================
// GhostExporter — Ghost ZIP erstellen
// =============================================================================
static class GhostExporter
{
private static string BaseDir = "";
private static string MarkdownDir = "";
public static int PostCount => PostFetcher.PostCount;
public static void Init(string baseDir)
{
BaseDir = baseDir;
MarkdownDir = Path.Combine(baseDir, "markdown");
}
public static async Task CreateZipAsync()
{
Console.WriteLine();
Console.WriteLine("Erstelle Ghost ZIP-Datei...");
var posts = PostFetcher.Posts;
if (posts.Count == 0)
{
Console.WriteLine("Keine Beiträge geladen. Bitte zuerst Option 1 ausführen.");
return;
}
string outputPath = Path.Combine(BaseDir, Program.OutputZipPath);
try
{
//PostFetcher.SaveAllMarkdownFiles();
string jsonString = JsonSerializer.Serialize(new GhostRoot
{
Db = new List<GhostDb> { new GhostDb { Data = new GhostData { Posts = posts.ToList() } } }
}, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
string outputDir = Path.GetDirectoryName(outputPath) ?? BaseDir;
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
using (var zipStream = new FileStream(outputPath, FileMode.Create))
using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
var jsonEntry = archive.CreateEntry("import.json");
using (var writer = new StreamWriter(jsonEntry.Open()))
{
await writer.WriteAsync(jsonString);
}
if (Directory.Exists(MarkdownDir))
{
foreach (string mdFile in Directory.GetFiles(MarkdownDir, "*.md", SearchOption.AllDirectories))
{
string relativeMdPath = Path.GetFileName(mdFile);
var mdEntry = archive.CreateEntry($"content/{relativeMdPath}");
using (var entryStream = mdEntry.Open())
using (var fileStream = File.OpenRead(mdFile))
{
await fileStream.CopyToAsync(entryStream);
}
}
}
foreach (var file in ImageManager.CachedFiles)
{
string fullLocalPath = Path.Combine(Path.Combine(BaseDir, "images"), file.RelativePath);
if (File.Exists(fullLocalPath))
{
var fileEntry = archive.CreateEntry($"content/images/{file.RelativePath}");
using (var entryStream = fileEntry.Open())
using (var fileStream = File.OpenRead(fullLocalPath))
{
await fileStream.CopyToAsync(entryStream);
}
}
}
}
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine($" Ghost ZIP erstellt!");
Console.WriteLine($" Beiträge: {posts.Count}");
Console.WriteLine($" Bilder: {ImageManager.CachedFiles.Count} Dateien");
Console.WriteLine($" Ausgabe: {outputPath}");
Console.WriteLine("=========================================");
}
catch (Exception ex)
{
Console.WriteLine($"\nFehler beim Erstellen der ZIP-Datei: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
}
}
}
}

View file

@ -0,0 +1,570 @@
using System.Globalization;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
namespace wp2ghost
{
// =============================================================================
// HtmlGhostConverter — WordPress HTML zu Ghost-kompatibles HTML konvertieren
// =============================================================================
public static class HtmlGhostConverter
{
private static readonly Dictionary<string, string?> WpClassMap = new(StringComparer.OrdinalIgnoreCase)
{
// Block-Elemente → Ghost-Äquivalent oder null (Klasse entfernen)
{ "wp-block-paragraph", null },
{ "wp-block-heading", null },
{ "wp-block-list", null },
{ "wp-block-quote", null },
{ "wp-block-code", null },
{ "wp-block-separator", null },
{ "wp-block-group", null },
{ "wp-block-columns", null },
{ "wp-block-column", null },
{ "wp-block-freeform", null },
{ "wp-block-verse", null },
{ "wp-block-preformatted", null },
// Ghost-Kompatible Klassen
{ "wp-block-image", "kg-card kg-image-card" },
{ "wp-block-gallery", "kg-card kg-gallery-card" },
{ "wp-block-pullquote", "kg-card kg-pullquote" },
{ "wp-block-embed", "kg-card kg-embed-card" },
{ "wp-block-embed-wrapper", "kg-card kg-embed-card" },
{ "wp-block-video", "kg-card kg-embed-card" },
{ "wp-block-audio", "kg-card kg-embed-card" },
{ "wp-block-cover", "kg-card kg-header-card" },
{ "wp-block-media-text", "kg-card kg-width-wide" },
{ "wp-block-table", "kg-card kg-table-card" },
{ "wp-block-button", "kg-card kg-button-card" },
{ "wp-block-buttons", "kg-card kg-button-card" },
{ "wp-block-file", "kg-card kg-file-card" },
// Sub-Elemente
{ "wp-block-button__link", "gh-btn" },
{ "wp-block-button__link__inline", "gh-btn" },
{ "wp-element-button", "gh-btn" },
{ "has-text-align-center", "kg-width-wide" },
{ "has-text-align-left", "" },
{ "has-text-align-right", "" },
{ "is-style-wide", "kg-width-wide" },
{ "is-style-full", "kg-width-wide" },
{ "is-style-large", "kg-width-wide" },
// Theme-spezifisch
{ "wp-block-post-title", null },
{ "wp-block-post-content", null },
{ "wp-block-site-title", null },
{ "wp-block-site-tagline", null },
{ "wp-block-query", null },
{ "wp-block-comments", null },
{ "wp-block-comment-template", null },
{ "wp-block-template-part", null },
// CoBlocks
{ "wp-block-coblocks-gallery-stacked", "kg-card kg-gallery-card" },
{ "wp-block-coblocks-gallery-collage", "kg-card kg-gallery-card" },
{ "wp-block-coblocks-gallery-masonry", "kg-card kg-gallery-card" },
{ "wp-block-coblocks-gallery-thumbnails", "kg-card kg-gallery-card" },
{ "wp-block-coblocks-alert", "kg-card kg-alert-card" },
{ "wp-block-coblocks-accordion", "kg-card kg-accordion-card" },
{ "wp-block-coblocks-accordion-item", "kg-accordion-item" },
{ "wp-block-coblocks-row", "kg-width-wide" },
{ "wp-block-coblocks-column", "" },
{ "wp-block-coblocks-click-to-tweet", "kg-card kg-click-to-tweet" },
{ "wp-block-coblocks-social-profiles", "kg-card kg-social-link" },
// Jetpack
{ "wp-block-jetpack-tiled-gallery", "kg-card kg-gallery-card" },
{ "wp-block-jetpack-slideshow", "kg-card kg-gallery-card" },
{ "wp-block-jetpack-story", "kg-card kg-gallery-card" },
};
public static string Convert(string html)
{
if (string.IsNullOrWhiteSpace(html)) return string.Empty;
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
int totalGalleries = 0;
totalGalleries += ConvertClassicGalleries(htmlDoc);
totalGalleries += ConvertGutenbergGalleries(htmlDoc);
totalGalleries += ConvertJetpackGalleries(htmlDoc);
totalGalleries += ConvertCoBlocksGalleries(htmlDoc);
if (totalGalleries > 0)
Console.WriteLine($" [Gallery] {totalGalleries} Galerie(n) konvertiert");
ConvertSingleImages(htmlDoc);
ConvertFigures(htmlDoc);
ConvertWpClasses(htmlDoc);
return htmlDoc.DocumentNode.OuterHtml;
}
// =========================================================================
// Gallery-Typ 1: WordPress Classic Gallery
// HTML: <div class='gallery gallery-columns-3'>
// <figure class='gallery-item'>
// <div class='gallery-icon portrait'>
// <a href='...'><img src='thumb' data-orig-file='full.jpg' /></a>
// </div>
// </figure>
// </div>
// =========================================================================
private static int ConvertClassicGalleries(HtmlDocument htmlDoc)
{
var galleryNodes = htmlDoc.DocumentNode.SelectNodes(
"//div[contains(@class, 'gallery') and not(contains(@class, 'wp-block'))]"
);
if (galleryNodes == null) return 0;
int count = 0;
foreach (var gallery in galleryNodes.ToList())
{
var items = gallery.SelectNodes(".//figure[contains(@class, 'gallery-item')]");
if (items == null || items.Count == 0) continue;
var ghostGallery = htmlDoc.CreateElement("figure");
SetClasses(ghostGallery, "kg-card kg-gallery-card");
AddWideClass(ghostGallery, gallery);
var container = htmlDoc.CreateElement("div");
container.AddClass("kg-gallery-container");
var wrappers = new List<HtmlNode>();
foreach (var item in items)
{
var img = item.SelectSingleNode(".//img");
if (img == null) continue;
string src = GetBestImageUrl(img);
if (string.IsNullOrEmpty(src)) continue;
wrappers.Add(BuildGalleryImage(htmlDoc, img, src));
}
if (wrappers.Count == 0) continue;
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
ghostGallery.AppendChild(container);
var captionNode = gallery.SelectSingleNode(".//*[contains(@class, 'gallery-caption')] | .//figcaption");
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
{
var newCaption = htmlDoc.CreateElement("figcaption");
newCaption.InnerHtml = captionNode.InnerHtml;
ghostGallery.AppendChild(newCaption);
ghostGallery.AddClass("kg-card-hascaption");
}
gallery.ParentNode?.ReplaceChild(ghostGallery, gallery);
count++;
}
return count;
}
// =========================================================================
// Gallery-Typ 2: WordPress Gutenberg Gallery
// =========================================================================
private static int ConvertGutenbergGalleries(HtmlDocument htmlDoc)
{
var galleryNodes = htmlDoc.DocumentNode.SelectNodes(
"//figure[contains(@class, 'wp-block-gallery')] | " +
"//div[contains(@class, 'wp-block-gallery')] | " +
"//ul[contains(@class, 'wp-block-gallery')]"
);
if (galleryNodes == null) return 0;
int count = 0;
foreach (var gallery in galleryNodes.ToList())
{
var imgs = gallery.SelectNodes(".//img");
if (imgs == null || imgs.Count == 0) continue;
var ghostGallery = htmlDoc.CreateElement("figure");
SetClasses(ghostGallery, "kg-card kg-gallery-card");
AddWideClass(ghostGallery, gallery);
var container = htmlDoc.CreateElement("div");
container.AddClass("kg-gallery-container");
var wrappers = new List<HtmlNode>();
foreach (var img in imgs)
{
string src = GetBestImageUrl(img);
if (string.IsNullOrEmpty(src)) continue;
wrappers.Add(BuildGalleryImage(htmlDoc, img, src));
}
if (wrappers.Count == 0) continue;
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
ghostGallery.AppendChild(container);
var captionNode = gallery.SelectSingleNode(".//figcaption");
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
{
var newCaption = htmlDoc.CreateElement("figcaption");
newCaption.InnerHtml = captionNode.InnerHtml;
ghostGallery.AppendChild(newCaption);
ghostGallery.AddClass("kg-card-hascaption");
}
gallery.ParentNode?.ReplaceChild(ghostGallery, gallery);
count++;
}
return count;
}
// =========================================================================
// Gallery-Typ 3: Jetpack Tiled Gallery
// =========================================================================
private static int ConvertJetpackGalleries(HtmlDocument htmlDoc)
{
var galleryNodes = htmlDoc.DocumentNode.SelectNodes(
"//div[contains(@class, 'wp-block-jetpack-tiled-gallery')] | " +
"//figure[contains(@class, 'wp-block-jetpack-tiled-gallery')]"
);
if (galleryNodes == null) return 0;
int count = 0;
foreach (var gallery in galleryNodes.ToList())
{
var imgs = gallery.SelectNodes(".//img");
if (imgs == null || imgs.Count == 0) continue;
var ghostGallery = htmlDoc.CreateElement("figure");
SetClasses(ghostGallery, "kg-card kg-gallery-card");
AddWideClass(ghostGallery, gallery);
var container = htmlDoc.CreateElement("div");
container.AddClass("kg-gallery-container");
var wrappers = new List<HtmlNode>();
foreach (var img in imgs)
{
string src = GetBestImageUrl(img);
if (string.IsNullOrEmpty(src)) continue;
wrappers.Add(BuildGalleryImage(htmlDoc, img, src));
}
if (wrappers.Count == 0) continue;
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
ghostGallery.AppendChild(container);
gallery.ParentNode?.ReplaceChild(ghostGallery, gallery);
count++;
}
return count;
}
// =========================================================================
// Gallery-Typ 4: CoBlocks Gallery
// =========================================================================
private static int ConvertCoBlocksGalleries(HtmlDocument htmlDoc)
{
var galleryNodes = htmlDoc.DocumentNode.SelectNodes(
"//figure[contains(@class, 'wp-block-coblocks-gallery-masonry')] | " +
"//figure[contains(@class, 'wp-block-coblocks-gallery-stacked')] | " +
"//figure[contains(@class, 'wp-block-coblocks-gallery-collage')] | " +
"//figure[contains(@class, 'wp-block-coblocks-gallery-thumbnails')]"
);
if (galleryNodes == null) return 0;
int count = 0;
foreach (var gallery in galleryNodes.ToList())
{
var imgs = gallery.SelectNodes(".//img");
if (imgs == null || imgs.Count == 0) continue;
var ghostGallery = htmlDoc.CreateElement("figure");
SetClasses(ghostGallery, "kg-card kg-gallery-card");
AddWideClass(ghostGallery, gallery);
var container = htmlDoc.CreateElement("div");
container.AddClass("kg-gallery-container");
var wrappers = new List<HtmlNode>();
foreach (var img in imgs)
{
string src = GetBestImageUrl(img);
if (string.IsNullOrEmpty(src)) continue;
wrappers.Add(BuildGalleryImage(htmlDoc, img, src));
}
if (wrappers.Count == 0) continue;
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
ghostGallery.AppendChild(container);
gallery.ParentNode?.ReplaceChild(ghostGallery, gallery);
count++;
}
return count;
}
// =========================================================================
// Galerie-Helfer: Spaltenanzahl, Wide-Klasse, Bild-Wrapper, Zeilen
// =========================================================================
private static int GetGalleryColumns(HtmlNode gallery)
{
string classes = gallery.GetAttributeValue("class", "");
var match = Regex.Match(classes, @"columns-(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int cols) && cols >= 1 && cols <= 9)
return cols;
return 3;
}
private static void AddWideClass(HtmlNode ghostGallery, HtmlNode originalGallery)
{
string origClass = originalGallery.GetAttributeValue("class", "");
if (origClass.Contains("is-style-wide") || origClass.Contains("alignwide"))
ghostGallery.AddClass("kg-width-wide");
}
private static HtmlNode BuildGalleryImage(HtmlDocument htmlDoc, HtmlNode originalImg, string src)
{
var imgWrapper = htmlDoc.CreateElement("div");
imgWrapper.AddClass("kg-gallery-image");
var newImg = htmlDoc.CreateElement("img");
newImg.SetAttributeValue("src", src);
double w = 0, h = 0;
string origSize = originalImg.GetAttributeValue("data-orig-size", "");
if (!string.IsNullOrEmpty(origSize))
{
var parts = origSize.Split(',');
if (parts.Length == 2 &&
double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double ow) &&
double.TryParse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double oh))
{
w = ow;
h = oh;
}
}
if (w <= 0 || h <= 0)
{
if (double.TryParse(originalImg.GetAttributeValue("width", ""), NumberStyles.Float, CultureInfo.InvariantCulture, out double aw) &&
double.TryParse(originalImg.GetAttributeValue("height", ""), NumberStyles.Float, CultureInfo.InvariantCulture, out double ah))
{
w = aw;
h = ah;
}
}
if (w > 0 && h > 0)
{
newImg.SetAttributeValue("width", ((int)w).ToString(CultureInfo.InvariantCulture));
newImg.SetAttributeValue("height", ((int)h).ToString(CultureInfo.InvariantCulture));
string ratio = (w / h).ToString("G6", CultureInfo.InvariantCulture);
imgWrapper.SetAttributeValue("style", $"flex: {ratio} 1 0%;");
}
else
{
imgWrapper.SetAttributeValue("style", "flex: 1 1 0%;");
}
newImg.SetAttributeValue("loading", "lazy");
newImg.SetAttributeValue("alt", originalImg.GetAttributeValue("alt", ""));
imgWrapper.AppendChild(newImg);
return imgWrapper;
}
private static void AppendGalleryRows(HtmlDocument htmlDoc, HtmlNode container, List<HtmlNode> wrappers, int columns)
{
for (int i = 0; i < wrappers.Count; i += columns)
{
var row = htmlDoc.CreateElement("div");
row.AddClass("kg-gallery-row");
int end = Math.Min(i + columns, wrappers.Count);
for (int j = i; j < end; j++)
row.AppendChild(wrappers[j]);
container.AppendChild(row);
}
}
// =========================================================================
// Helper: Beste Bild-URL aus img-Element holen
// Priorität: data-orig-file → data-large-file → srcset (letzter/Widest) → src
// =========================================================================
private static string GetBestImageUrl(HtmlNode img)
{
// 1. data-orig-file (Vollbild)
string src = img.GetAttributeValue("data-orig-file", "");
if (!string.IsNullOrEmpty(src)) return src;
// 2. data-large-file
src = img.GetAttributeValue("data-large-file", "");
if (!string.IsNullOrEmpty(src)) return src;
// 3. srcset (größte Variante nehmen)
string srcset = img.GetAttributeValue("srcset", "");
if (!string.IsNullOrEmpty(srcset))
{
src = GetLargestFromSrcset(srcset);
if (!string.IsNullOrEmpty(src)) return src;
}
// 4. src (Fallback)
return img.GetAttributeValue("src", "");
}
private static string GetLargestFromSrcset(string srcset)
{
string bestUrl = "";
int bestWidth = 0;
foreach (string part in srcset.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
string trimmed = part.Trim();
string[] tokens = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 2) continue;
string url = tokens[0];
string descriptor = tokens[1];
if (int.TryParse(descriptor.Replace("w", ""), out int width) && width > bestWidth)
{
bestWidth = width;
bestUrl = url;
}
}
return bestUrl;
}
// =========================================================================
// Einzelbilder (nicht in Galerien)
// =========================================================================
private static void ConvertSingleImages(HtmlDocument htmlDoc)
{
var singleImgNodes = htmlDoc.DocumentNode.SelectNodes("//img");
if (singleImgNodes == null) return;
foreach (var img in singleImgNodes.ToList())
{
if (img.Ancestors("figure").Any(a => HasClass(a, "kg-gallery-card")))
continue;
string src = GetBestImageUrl(img);
if (string.IsNullOrEmpty(src)) continue;
var parentFigure = img.Ancestors("figure").FirstOrDefault();
if (parentFigure != null)
{
SetClasses(parentFigure, "kg-card kg-image-card");
img.AddClass("kg-image");
}
else
{
var wrapper = htmlDoc.CreateElement("figure");
SetClasses(wrapper, "kg-card kg-image-card");
img.AddClass("kg-image");
var imgParent = img.ParentNode;
if (imgParent != null)
{
imgParent.InsertBefore(wrapper, img);
imgParent.RemoveChild(img);
wrapper.AppendChild(img);
}
}
}
}
// =========================================================================
// Figure ohne kg-card → kg-width-wide
// =========================================================================
private static void ConvertFigures(HtmlDocument htmlDoc)
{
var figures = htmlDoc.DocumentNode.SelectNodes("//figure");
if (figures == null) return;
foreach (var figure in figures.ToList())
{
if (HasClass(figure, "kg-card")) continue;
SetClasses(figure, "kg-width-wide kg-card-width-wide");
}
}
// =========================================================================
// WP-Klassen mappen
// =========================================================================
private static void ConvertWpClasses(HtmlDocument htmlDoc)
{
var allNodes = htmlDoc.DocumentNode.SelectNodes("//*[contains(@class, 'wp-block')]");
if (allNodes == null) return;
foreach (var node in allNodes.ToList())
{
string classes = node.GetAttributeValue("class", "");
if (string.IsNullOrEmpty(classes)) continue;
var classList = classes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
var newClasses = new List<string>();
bool changed = false;
foreach (string cls in classList)
{
if (WpClassMap.TryGetValue(cls, out string? ghostClass))
{
changed = true;
if (ghostClass != null && ghostClass.Length > 0)
newClasses.AddRange(ghostClass.Split(' '));
}
else
{
newClasses.Add(cls);
}
}
if (changed)
{
string finalClasses = string.Join(" ", newClasses.Where(c => c.Length > 0).Distinct());
if (finalClasses.Length > 0)
node.SetAttributeValue("class", finalClasses);
else
node.Attributes.Remove("class");
}
}
// Separator
var separators = htmlDoc.DocumentNode.SelectNodes("//div[contains(@class, 'wp-block-separator')]");
if (separators != null)
{
foreach (var sep in separators.ToList())
{
var hr = htmlDoc.CreateElement("hr");
foreach (var attr in sep.Attributes)
hr.SetAttributeValue(attr.Name, attr.Value);
sep.ParentNode?.ReplaceChild(hr, sep);
}
}
}
private static bool HasClass(HtmlNode node, string className)
{
string classes = node.GetAttributeValue("class", "");
return classes.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Any(c => c.Equals(className, StringComparison.OrdinalIgnoreCase));
}
private static void SetClasses(HtmlNode node, string classes)
{
node.SetAttributeValue("class", classes);
}
}
}

View file

@ -0,0 +1,143 @@
namespace wp2ghost
{
// =============================================================================
// HTML zu Markdown Konverter
// =============================================================================
public static class HtmlToMarkdownConverter
{
public static string Convert(string html)
{
if (string.IsNullOrEmpty(html))
return "";
string result = html;
result = System.Text.RegularExpressions.Regex.Replace(result, "<!--.*?-->", "", System.Text.RegularExpressions.RegexOptions.Singleline);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<h([1-6])[^>]*>(.*?)</h\1>", m => new string('#', int.Parse(m.Groups[1].Value)) + " " + StripTags(m.Groups[2].Value) + Environment.NewLine + Environment.NewLine, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<blockquote[^>]*>(.*?)</blockquote>", m => "> " + StripTags(m.Groups[1].Value).Replace(Environment.NewLine, Environment.NewLine + "> ") + Environment.NewLine, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<pre[^>]*><code[^>]*>(.*?)</code></pre>", m => "```\n" + StripTags(m.Groups[1].Value) + "\n```\n\n", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<code[^>]*>(.*?)</code>", m => "`" + StripTags(m.Groups[1].Value) + "`", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<ul[^>]*>(.*?)</ul>", m => ConvertList(m.Groups[1].Value, "-"), System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<ol[^>]*>(.*?)</ol>", m => ConvertOrderedList(m.Groups[1].Value), System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = ConvertTables(result);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<p[^>]*>(.*?)</p>", m => CleanInnerHtml(m.Groups[1].Value) + Environment.NewLine + Environment.NewLine, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<br\s*/?>", Environment.NewLine, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<strong[^>]*>(.*?)</strong>", "**$1**", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<b[^>]*>(.*?)</b>", "**$1**", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<em[^>]*>(.*?)</em>", "*$1*", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<i[^>]*>(.*?)</i>", "*$1*", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<a\s+[^>]*href=[""']([^""']+)[""'][^>]*>(.*?)</a>", "[$2]($1)", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<img\s+[^>]*src=[""']([^""']+)[""'][^>]*alt=[""']([^""']*)[""'][^>]*>", m => "![" + m.Groups[2].Value + "](" + m.Groups[1].Value + ")", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<img\s+[^>]*src=[""']([^""']+)[""'][^>]*>", m => "![](" + m.Groups[1].Value + ")", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result, @"<div[^>]*class=[""'][^""']*wp-block-heading[^""']*[""'][^>]*>(.*?)</div>", m => "# " + StripTags(m.Groups[1].Value) + Environment.NewLine + Environment.NewLine, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = System.Text.RegularExpressions.Regex.Replace(result,
@"<figure[^>]*>(.*?)</figure>",
m => ConvertFigure(m.Groups[1].Value),
System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
result = StripTagsKeepFigure(result);
result = System.Text.RegularExpressions.Regex.Replace(result, @"\n{3,}", "\n\n");
result = result.Replace("&amp;", "&").Replace("&lt;", "<").Replace("&gt;", ">").Replace("&quot;", "\"").Replace("&#8217;", "'").Replace("&nbsp;", " ").Replace("&mdash;", "—").Replace("&ndash;", "–");
return result.Trim();
}
private static string StripTags(string html) =>
System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", "");
private static string CleanInnerHtml(string html)
{
string cleaned = StripTags(html);
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\*\*(.+?)\*\*", "**$1**");
return cleaned.Trim();
}
private static string ConvertFigure(string figureContent)
{
string imgSrc = "", altText = "", caption = "";
var imgMatch = System.Text.RegularExpressions.Regex.Match(figureContent, @"<img\s+[^>]*src=[""']([^""']+)[""']");
if (imgMatch.Success) imgSrc = imgMatch.Groups[1].Value;
var altMatch = System.Text.RegularExpressions.Regex.Match(figureContent, @"alt=[""']([^""']*)[""']");
if (altMatch.Success) altText = altMatch.Groups[1].Value;
var figMatch = System.Text.RegularExpressions.Regex.Match(figureContent, @"<figcaption[^>]*>(.*?)</figcaption>", System.Text.RegularExpressions.RegexOptions.Singleline);
if (figMatch.Success) caption = System.Text.RegularExpressions.Regex.Replace(figMatch.Groups[1].Value, "<[^>]*>", "").Trim();
if (string.IsNullOrEmpty(imgSrc)) return "";
string result = $"<figure class=\"kg-width-wide kg-card-width-wide\"><img src=\"{imgSrc}\" loading=\"lazy\"";
if (!string.IsNullOrEmpty(altText)) result += $" alt=\"{altText}\"";
result += ">";
if (!string.IsNullOrEmpty(caption)) result += $"<figcaption>{caption}</figcaption>";
result += "</figure>";
return result + Environment.NewLine;
}
private static string StripTagsKeepFigure(string html)
{
var figureBlocks = new List<string>();
html = System.Text.RegularExpressions.Regex.Replace(html, @"<figure[^>]*>.*?</figure>", m => { figureBlocks.Add(m.Value); return $"__FIGURE_{figureBlocks.Count - 1}__"; }, System.Text.RegularExpressions.RegexOptions.Singleline);
html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", "");
for (int i = 0; i < figureBlocks.Count; i++)
html = html.Replace($"__FIGURE_{i}__", figureBlocks[i]);
return html;
}
private static string ConvertList(string listHtml, string bullet)
{
listHtml = System.Text.RegularExpressions.Regex.Replace(listHtml, @"<li[^>]*>(.*?)</li>", m => bullet + " " + CleanInnerHtml(m.Groups[1].Value) + Environment.NewLine, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return StripTags(listHtml).Trim() + Environment.NewLine;
}
private static string ConvertOrderedList(string listHtml)
{
int counter = 0;
listHtml = System.Text.RegularExpressions.Regex.Replace(listHtml, @"<li[^>]*>(.*?)</li>", m => { counter++; return counter + ". " + CleanInnerHtml(m.Groups[1].Value) + Environment.NewLine; }, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return StripTags(listHtml).Trim() + Environment.NewLine;
}
private static string ConvertTables(string html)
{
return System.Text.RegularExpressions.Regex.Replace(html, @"<table[^>]*>(.*?)</table>", m =>
{
string tableContent = m.Groups[1].Value;
var headerMatch = System.Text.RegularExpressions.Regex.Match(tableContent, @"<tr[^>]*>(.*?)</tr>", System.Text.RegularExpressions.RegexOptions.Singleline);
if (!headerMatch.Success) return StripTags(tableContent) + Environment.NewLine;
string headers = CleanInnerHtml(headerMatch.Groups[1].Value);
headers = headers.Replace("<th", "<td").Replace("</th>", "</td>");
string headerLine = "| " + string.Join(" | ", StripTags(headers).Split(new[] { "</td>", "<td" }, StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim())) + " |";
string separator = "|" + string.Join("|", StripTags(headers).Split(new[] { "</td>", "<td" }, StringSplitOptions.RemoveEmptyEntries).Select(c => "---")) + "|";
var rows = System.Text.RegularExpressions.Regex.Matches(tableContent, @"<tr[^>]*>(.*?)</tr>", System.Text.RegularExpressions.RegexOptions.Singleline);
var dataRows = new List<string>();
for (int i = 1; i < rows.Count; i++)
{
string rowText = CleanInnerHtml(rows[i].Groups[1].Value);
string[] cells = rowText.Split(new[] { "</td>", "<td" }, StringSplitOptions.RemoveEmptyEntries);
dataRows.Add("| " + string.Join(" | ", cells.Select(c => c.Trim().Replace("<b>", "").Replace("</b>", "").Replace("<strong>", "").Replace("</strong>", ""))) + " |");
}
return headerLine + Environment.NewLine + separator + Environment.NewLine + string.Join(Environment.NewLine, dataRows) + Environment.NewLine + Environment.NewLine;
}, System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
}
}
}

View file

@ -0,0 +1,221 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System.Diagnostics.Metrics;
namespace wp2ghost
{
// =============================================================================
// ImageManager — Bilder herunterladen, cachen, lokal verwalten
// =============================================================================
static class ImageManager
{
private static readonly HttpClient httpClient = new();
private static Dictionary<string, (string RelativePath, string LocalPath)> Cache = new();
private static List<(string RelativePath, string LocalPath)> FilesToZip = new();
private static string LocalImageDir = "";
private static string CachePath = "";
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
public static bool TryGetRelativePath(string url, out string relativePath)
{
if (Cache.TryGetValue(url, out var cached))
{
relativePath = cached.RelativePath;
return true;
}
relativePath = "";
return false;
}
public static void Init(string baseDir)
{
LocalImageDir = Path.Combine(baseDir, "images");
CachePath = Path.Combine(baseDir, "imagecache.txt");
if (!Directory.Exists(LocalImageDir))
Directory.CreateDirectory(LocalImageDir);
LoadCache();
}
public static string ImageContainerPath => LocalImageDir;
public static void SaveCache()
{
var lines = new List<string>();
foreach (var kvp in Cache)
lines.Add($"{kvp.Key}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}");
File.WriteAllLines(CachePath, lines);
}
public static void ClearAll()
{
if (Directory.Exists(LocalImageDir))
{
Directory.Delete(LocalImageDir, true);
Directory.CreateDirectory(LocalImageDir);
}
Cache.Clear();
FilesToZip.Clear();
SaveCache();
Console.WriteLine("Bilder wurden lokal gelöscht.");
}
public static void ShowInfo()
{
if (Cache.Count > 0)
{
Console.WriteLine("\nLokal gespeicherte Bilder:");
foreach (var kvp in Cache.Take(20))
Console.WriteLine($" {kvp.Key} -> images/{kvp.Value.RelativePath}");
if (Cache.Count > 20)
Console.WriteLine($" ... und {Cache.Count - 20} weitere");
}
}
public static bool IsMainImage(string url)
{
if (string.IsNullOrEmpty(url)) return false;
try
{
var uri = new Uri(url);
string fileName = Path.GetFileName(uri.LocalPath);
// WordPress-Sizes: -300x200, -scaled, -1024x768 etc.
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, @"-\d+x\d+(?=\.\w+$)"))
return false;
if (fileName.Contains("-scaled"))
return false;
return true;
}
catch { return false; }
}
public static async Task<(string RelativePath, string LocalPath)?> DownloadAsync(string imageUrl, DateTime? postDate)
{
if (string.IsNullOrEmpty(imageUrl) || !Uri.IsWellFormedUriString(imageUrl, UriKind.Absolute))
return null;
if (Cache.TryGetValue(imageUrl, out var cachedFile))
{
if (File.Exists(cachedFile.LocalPath))
return cachedFile;
Cache.Remove(imageUrl);
}
try
{
string fileName = Path.GetFileName(new Uri(imageUrl).AbsolutePath);
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 pDate = postDate ?? DateTime.Now;
string uniqueFileName = $"{pDate:yyyymmdd}_{counter}{Path.GetExtension(fileName)}";
string relativePath = $"{datePath}/{uniqueFileName}";
string fallBackPath = $"{datePath}/{uniqueFileName}";
localPath = Path.Combine(LocalImageDir, relativePath);
fallBackPath = Path.Combine(LocalImageDir, fallBackPath);
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);
Console.WriteLine($" [Bild] Heruntergeladen: {relativePath}");
}
else
{
Console.WriteLine($" [Bild] Bereits vorhanden: {relativePath}");
}
ResizeImage(localPath);
var fileResult = (relativePath, localPath);
Cache[imageUrl] = fileResult;
FilesToZip.Add(fileResult);
return fileResult;
}
catch (Exception ex)
{
Console.WriteLine($" [Bild-Fehler] Konnte {imageUrl} nicht laden: {ex.Message}");
return null;
}
}
private static void ResizeImage(string localPath)
{
const int maxSize = 1024;
try
{
using var image = Image.Load(localPath);
int width = image.Width;
int height = image.Height;
int longEdge = Math.Max(width, height);
if (longEdge <= maxSize) return;
int newWidth, newHeight;
if (width >= height)
{
newWidth = maxSize;
newHeight = (int)Math.Round(height * ((double)maxSize / width));
}
else
{
newHeight = maxSize;
newWidth = (int)Math.Round(width * ((double)maxSize / height));
}
image.Mutate(x => x.Resize(newWidth, newHeight));
image.Save(localPath);
Console.WriteLine($" [Bild] Angepasst: {width}x{height} → {newWidth}x{newHeight}");
}
catch (Exception ex)
{
Console.WriteLine($" [Bild-Warnung] Resize fehlgeschlagen für {Path.GetFileName(localPath)}: {ex.Message}");
}
}
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()
{
if (!File.Exists(CachePath)) return;
try
{
var lines = File.ReadAllLines(CachePath);
foreach (var line in lines)
{
var parts = line.Split('|');
if (parts.Length == 3)
Cache[parts[0]] = (parts[1], parts[2]);
}
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Laden des Bild-Caches: {ex.Message}");
}
}
}
}

View file

@ -0,0 +1,487 @@
using System.Text.Json;
namespace wp2ghost
{
// =============================================================================
// PostFetcher — Posts von WordPress abrufen, verarbeiten
// =============================================================================
static class PostFetcher
{
private static readonly HttpClient httpClient = new();
private static HashSet<int> ProcessedPosts = new();
private static List<GhostPost> GhostPosts = new();
private static string ProcessedPostsPath = "";
//private static string MarkdownDir = "";
private static string GhostPostsDir = "";
private static string BaseDir = "";
public static int ProcessedCount => ProcessedPosts.Count;
public static int PostCount => GhostPosts.Count;
public static IReadOnlyList<GhostPost> Posts => GhostPosts;
public static void Init(string baseDir)
{
BaseDir = baseDir;
ProcessedPostsPath = Path.Combine(baseDir, "processed_posts.txt");
//MarkdownDir = Path.Combine(baseDir, "markdown");
GhostPostsDir = Path.Combine(baseDir, "ghostPosts");
////if (!Directory.Exists(MarkdownDir))
//// Directory.CreateDirectory(MarkdownDir);
if (!Directory.Exists(GhostPostsDir))
Directory.CreateDirectory(GhostPostsDir);
LoadProcessedPosts();
}
public static void ClearProcessedPosts()
{
ProcessedPosts.Clear();
File.WriteAllText(ProcessedPostsPath, "wp_id,wp_slug,ghost_id" + Environment.NewLine);
Console.WriteLine("Abgefragte Posts wurden zurückgesetzt.");
}
public static void ShowInfo()
{
if (ProcessedPosts.Count > 0)
{
Console.WriteLine($"Letzte abgefragte Objekte: {string.Join(", ", ProcessedPosts.OrderByDescending(x => x).Take(10))}");
}
}
public static async Task FetchFromWordPressAsync(string wpBaseUrl, int maxPosts)
{
Console.WriteLine();
Console.WriteLine("Starte Abruf von WordPress...");
string wpHost = "";
try { wpHost = new Uri(wpBaseUrl).Host.ToLowerInvariant(); } catch { }
try
{
var allPostIds = new List<int>();
var postTypes = new[] { "post", "page" };
foreach (var postType in postTypes)
{
Console.WriteLine($"Hole {postType}-IDs von der WordPress-API...");
string idsUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/custom/v1/posts-ids?post_type={postType}";
var idsResponse = await httpClient.GetStringAsync(idsUrl);
var idsObj = JsonSerializer.Deserialize<IdsResponse>(idsResponse);
if (idsObj?.Ids != null)
allPostIds.AddRange(idsObj.Ids);
}
if (allPostIds.Count == 0)
{
Console.WriteLine("Keine Beiträge oder Seiten gefunden.");
return;
}
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}");
if (pendingIds.Count == 0)
{
Console.WriteLine("Alle Beiträge und Seiten wurden bereits abgefragt. Nichts zu tun. (Nutze Option 5 zum Zurücksetzen)");
return;
}
if (maxPosts > 0)
pendingIds = pendingIds.Take(maxPosts).ToList();
Console.WriteLine($"Es werden {pendingIds.Count} Beiträge/Seiten verarbeitet.");
int internalId = 1;
if (GhostPosts.Count > 0)
{
var lastId = GhostPosts.Select(gp => int.TryParse(gp.Id, out int id) ? id : 0).Max();
internalId = lastId + 1;
}
int processed = 0;
foreach (int wpId in pendingIds)
{
Console.WriteLine($"[{++processed}/{pendingIds.Count}] -> Beitrag/Seite ID: {wpId}...");
string detailUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/custom/v1/post-details/{wpId}";
try
{
var detailJson = await httpClient.GetStringAsync(detailUrl);
var wpPost = JsonSerializer.Deserialize<WpPostResponse>(detailJson);
if (wpPost == null)
{
Console.WriteLine($" Übersprungen (null).");
MarkProcessed(wpId, null);
continue;
}
string htmlContent = wpPost.Content ?? "";
var parsedDate = DateTime.MinValue;
var postDate = DateTime.TryParse(wpPost.Date, out parsedDate) ? parsedDate : new DateTime(2026, 9, 1);
// Alle internen Bild-URLs aus dem HTML extrahieren (keine externen Links)
var htmlImageUrls = ExtractImageUrls(htmlContent, wpHost);
// Bevorzugte Media-Dateien ermitteln
var preferredMedia = GetPreferredMediaFiles(wpPost.MediaFiles, htmlImageUrls, wpHost);
// Prüfe: Referenzierte Bilder im HTML, die NICHT in MediaFiles sind
if (htmlImageUrls.Count > 0 && wpPost.MediaFiles != null)
{
var mediaFileNames = wpPost.MediaFiles.Select(m => GetFileNameFromUrl(m).ToLowerInvariant()).ToHashSet();
foreach (var imgUrl in htmlImageUrls)
{
string imgFileName = GetFileNameFromUrl(imgUrl).ToLowerInvariant();
if (!mediaFileNames.Contains(imgFileName))
{
Console.WriteLine($" [Warnung] Bild im HTML nicht in MediaFiles: {Path.GetFileName(new Uri(imgUrl).AbsolutePath)}");
}
}
}
if (preferredMedia.Count > 0)
{
foreach (string mediaUrl in preferredMedia)
{
var downloaded = await ImageManager.DownloadAsync(mediaUrl, postDate);
if (downloaded.HasValue)
{
string ghostMediaPath = $"__GHOST_URL__/content/images/{downloaded.Value.RelativePath}";
htmlContent = htmlContent.Replace(mediaUrl, ghostMediaPath);
}
}
}
// WordPress HTML zu Ghost-kompatibles HTML konvertieren (Galerien, Figures, etc.)
htmlContent = HtmlGhostConverter.Convert(htmlContent);
// Lokale Bildpfade für JSON-Datei sammeln
var imageFiles = new List<string>();
if (preferredMedia.Count > 0)
{
foreach (string mediaUrl in preferredMedia)
{
if (ImageManager.TryGetRelativePath(mediaUrl, out string relPath))
imageFiles.Add(relPath);
}
}
// Featured Image verarbeiten
string featureImageGhostPath = "";
if (!string.IsNullOrEmpty(wpPost.FeaturedImage))
{
if (ImageManager.TryGetRelativePath(wpPost.FeaturedImage, out string featRel))
{
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
{
Id = internalId.ToString(),
Title = wpPost.Title,
Slug = wpPost.Slug,
Html = htmlContent,
Status = "published",
CreatedAt = postDate.ToString("o"),
PublishedAt = postDate.ToString("o")
};
GhostPosts.Add(ghostPost);
// JSON-Datei in ghostPosts/ speichern
var postFile = new GhostPostFile
{
WpId = wpId,
WpSlug = wpPost.Slug,
GhostId = ghostPost.Id,
Title = wpPost.Title,
Slug = wpPost.Slug,
Excerpt = wpPost.Excerpt,
Status = "published",
CreatedAt = postDate.ToString("o"),
PublishedAt = postDate.ToString("o"),
FeatureImage = featureImageGhostPath,
Categories = wpPost.Categories ?? new(),
Keywords = wpPost.Keywords ?? "",
Comments = wpPost.Comments ?? new(),
ImageFiles = imageFiles,
Html = htmlContent
};
string jsonPath = Path.Combine(GhostPostsDir, $"{postFile.WpId}.json");
File.WriteAllText(jsonPath, JsonSerializer.Serialize(postFile, new JsonSerializerOptions { WriteIndented = true }));
MarkProcessed(wpId, wpPost.Slug);
internalId++;
}
catch (Exception ex)
{
Console.WriteLine($" Fehler beim Abrufen von ID {wpId}: {ex.Message}");
MarkProcessed(wpId, null);
}
}
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine($" Abruf abgeschlossen! {GhostPosts.Count} Beiträge geladen.");
Console.WriteLine($" Abgefragte Posts insgesamt: {ProcessedPosts.Count}");
Console.WriteLine(" Nutze Option 2 um die Ghost ZIP zu erstellen.");
Console.WriteLine("=========================================");
}
catch (Exception ex)
{
Console.WriteLine($"\nFehler beim Abruf: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
}
}
//public static void SaveAllMarkdownFiles()
//{
// foreach (var post in GhostPosts)
// {
// if (string.IsNullOrEmpty(post.Slug)) continue;
// string mdPath = Path.Combine(MarkdownDir, $"{post.Slug}.md");
// if (!string.IsNullOrEmpty(post.Markdown))
// File.WriteAllText(mdPath, post.Markdown);
// }
// Console.WriteLine($"Markdown-Dateien gespeichert: {GhostPosts.Count} Dateien unter {MarkdownDir}");
//}
private static List<string> ExtractImageUrls(string html, string wpHost)
{
var urls = new List<string>();
var imageExts = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".tiff", ".tif" };
// 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;
if (!string.IsNullOrEmpty(url) && IsInternalUrl(url, wpHost) && !urls.Contains(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;
}
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)
{
if (string.IsNullOrEmpty(wpHost)) return true;
try
{
var uri = new Uri(url);
return uri.Host.ToLowerInvariant().EndsWith(wpHost) || wpHost.EndsWith(uri.Host.ToLowerInvariant());
}
catch
{
return url.StartsWith("/") || url.StartsWith("wp-content");
}
}
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)
{
ProcessedPosts.Add(wpId);
SaveProcessedPosts(wpId, wpSlug);
}
private static void LoadProcessedPosts()
{
if (!File.Exists(ProcessedPostsPath)) return;
try
{
var lines = File.ReadAllLines(ProcessedPostsPath);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("wp_id")) continue;
var parts = line.Split(',');
if (parts.Length >= 1 && int.TryParse(parts[0].Trim(), out int postId))
ProcessedPosts.Add(postId);
}
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Laden der abgefragten Posts: {ex.Message}");
}
}
private static void SaveProcessedPosts(int wpId, string? wpSlug)
{
string entry = $"{wpId},{wpSlug ?? ""}";
using var writer = File.AppendText(ProcessedPostsPath);
writer.WriteLine(entry);
}
}
}

View file

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace wp2ghost
{
public class IdsResponse
{
[JsonPropertyName("count")] public int Count { get; set; }
[JsonPropertyName("post_type")] public string? PostType { get; set; }
[JsonPropertyName("ids")] public List<int>? Ids { get; set; }
}
public class WpPostResponse
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("title")] public string? Title { get; set; }
[JsonPropertyName("slug")] public string? Slug { get; set; }
[JsonPropertyName("date")] public string? Date { get; set; }
[JsonPropertyName("content")] public string? Content { get; set; }
[JsonPropertyName("excerpt")] public string? Excerpt { get; set; }
[JsonPropertyName("featured_image")] public string? FeaturedImage { get; set; }
[JsonPropertyName("categories")] public List<WpCategory>? Categories { get; set; } = new();
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
[JsonPropertyName("comments")] public List<WpComment>? Comments { get; set; } = new();
[JsonPropertyName("media_files")] public List<string>? MediaFiles { get; set; } = new();
}
public class WpCategory
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("slug")] public string? Slug { get; set; }
}
public class WpComment
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("author_name")] public string? AuthorName { get; set; }
[JsonPropertyName("author_email")] public string? AuthorEmail { get; set; }
[JsonPropertyName("author_url")] public string? AuthorUrl { get; set; }
[JsonPropertyName("content")] public string? Content { get; set; }
[JsonPropertyName("date")] public string? Date { get; set; }
}
}

View file

@ -0,0 +1,6 @@
# WP2Ghost Konfiguration
# Ändere die Werte und starte das Programm neu
wpBaseUrl=https://deine-domain.com
outputZipPath=ghost_import.zip
maxPosts=-1

View file

@ -0,0 +1,3 @@
# Bild-Cache für WP2Ghost
# Format: imageUrl|relativePath|localTempPath
# Automatisch vom Programm generiert. Nicht manuell bearbeiten.

View file

@ -0,0 +1,3 @@
# Abgefragte Posts für WP2Ghost
# Eine Post-ID pro Zeile
# Automatisch vom Programm generiert. Nicht manuell bearbeiten.

View file

@ -0,0 +1,105 @@
<p>Am letzten Karnevalwochenende 2017 haben wir Venedig besucht.</p>
<p>Trotz der Massen und teilweise langen Wartenzeiten, dank der Terrorprävention, war es ein schönes Wochenende.</p>
<p>Hier ein paar Impressionen.</p>
<div id='gallery-1' class='gallery galleryid-0 gallery-columns-3 gallery-size-thumbnail'>
<figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00594/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00594-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="45" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00594/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00594.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488112702&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;92&quot;,&quot;iso&quot;:&quot;125&quot;,&quot;shutter_speed&quot;:&quot;0.0025&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00594" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00594.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00576/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00576-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="44" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00576/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00576.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488111512&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;97&quot;,&quot;iso&quot;:&quot;125&quot;,&quot;shutter_speed&quot;:&quot;0.00125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00576" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00576.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00573/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00573-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="43" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00573/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00573.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488111458&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;90&quot;,&quot;iso&quot;:&quot;125&quot;,&quot;shutter_speed&quot;:&quot;0.002&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00573" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00573.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00507/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00507-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="42" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00507/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00507.jpg" data-orig-size="640,474" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4.5&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488109764&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;198&quot;,&quot;iso&quot;:&quot;125&quot;,&quot;shutter_speed&quot;:&quot;0.003125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00507" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00507.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00506/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00506-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="41" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00506/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00506.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4.5&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488109732&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;200&quot;,&quot;iso&quot;:&quot;125&quot;,&quot;shutter_speed&quot;:&quot;0.002&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00506" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00506.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00470/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00470-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="38" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00470/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00470.jpg" data-orig-size="640,615" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4.5&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488105959&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;100&quot;,&quot;shutter_speed&quot;:&quot;0.0015625&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00470" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00470.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00459/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00459-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="37" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00459/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00459.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488105145&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;100&quot;,&quot;shutter_speed&quot;:&quot;0.00125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00459" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00459.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00457/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00457-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="36" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00457/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00457.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488105126&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;100&quot;,&quot;shutter_speed&quot;:&quot;0.00125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00457" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00457.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00382/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00382-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="35" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00382/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00382.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488046499&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;400&quot;,&quot;shutter_speed&quot;:&quot;0.05&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00382" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00382.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00365/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00365-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="34" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00365/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00365.jpg" data-orig-size="416,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488039926&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;177&quot;,&quot;iso&quot;:&quot;640&quot;,&quot;shutter_speed&quot;:&quot;0.00125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00365" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00365.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00349/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00349-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="33" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00349/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00349.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488038048&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;88&quot;,&quot;iso&quot;:&quot;640&quot;,&quot;shutter_speed&quot;:&quot;0.00015625&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00349" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00349.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00338/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00338-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="32" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00338/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00338.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488037878&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;640&quot;,&quot;shutter_speed&quot;:&quot;0.0005&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00338" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00338.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00320/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00320-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="31" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00320/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00320.jpg" data-orig-size="476,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;5.6&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488035411&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;189&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.0025&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00320" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00320.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00305/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" srcset="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305-150x150.jpg 150w, https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305-300x300.jpg 300w, https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305.jpg 640w" sizes="auto, (max-width: 150px) 100vw, 150px" data-attachment-id="30" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00305/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305.jpg" data-orig-size="640,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488034750&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;77&quot;,&quot;iso&quot;:&quot;64&quot;,&quot;shutter_speed&quot;:&quot;0.01&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00305" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00305.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00298/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00298-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="29" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00298/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00298.jpg" data-orig-size="640,467" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;5&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488034170&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;64&quot;,&quot;shutter_speed&quot;:&quot;0.004&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00298" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00298.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00294/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00294-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="28" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00294/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00294.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4.5&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488034129&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;107&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.001&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00294" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00294.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00291/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00291-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="27" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00291/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00291.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488033907&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;93&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.0025&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00291" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00291.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00279/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00279-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="25" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00279/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00279.jpg" data-orig-size="461,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;4&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488033695&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.00125&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00279" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00279.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00276/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00276-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="24" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00276/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00276.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;7.1&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488033461&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;200&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.00625&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00276" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00276.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00271/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00271-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="23" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00271/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00271.jpg" data-orig-size="427,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;7.1&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488033249&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.008&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00271" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00271.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon portrait'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00270/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00270-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="22" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00270/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00270.jpg" data-orig-size="460,640" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;7.1&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488033156&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;113&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.008&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00270" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00270.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00257/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00257-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="21" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00257/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00257.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488031611&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;70&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.005&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00257" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00257.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00255/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00255-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="20" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00255/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00255.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488031407&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;97&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.002&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00255" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00255.jpg" /></a>
</div>
</figure><figure class='gallery-item'>
<div class='gallery-icon landscape'>
<a href='https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00253/'><img loading="lazy" decoding="async" width="150" height="150" src="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00253-150x150.jpg" class="attachment-thumbnail size-thumbnail" alt="" data-attachment-id="19" data-permalink="https://www.brigitteundrolandunterwegs.com/erster-blogbeitrag/dsc00253/" data-orig-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00253.jpg" data-orig-size="640,427" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;8&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;ILCE-7M2&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;1488031351&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;111&quot;,&quot;iso&quot;:&quot;50&quot;,&quot;shutter_speed&quot;:&quot;0.004&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;lens&quot;:&quot;FE 70-200mm F4 G OSS&quot;}" data-image-title="DSC00253" data-image-description="" data-image-caption="" data-large-file="https://www.brigitteundrolandunterwegs.com/wp-content/uploads/2017/03/dsc00253.jpg" /></a>
</div>
</figure>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>

View file

@ -0,0 +1,3 @@
<Solution>
<Project Path="CSharpGhostImporter.csproj" />
</Solution>

View file

@ -0,0 +1,396 @@
<?php
/**
* Plugin Name: RBR Export to Ghost
* Plugin URI:
* Description: Exportiert WordPress-Beiträge und Medien über eine REST-API für den Import in Ghost CMS.
* Version: 1.0.0
* Author: Roland Brunner
* Author URI:
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: rbr-export-ghost
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
define('RBR_EXPORT_VERSION', '1.0.0');
define('RBR_EXPORT_PATH', plugin_dir_path(__FILE__));
define('RBR_EXPORT_URL', plugin_dir_url(__FILE__));
register_activation_hook(__FILE__, 'rbr_export_activate');
register_deactivation_hook(__FILE__, 'rbr_export_deactivate');
function rbr_export_activate() {
flush_rewrite_rules();
}
function rbr_export_deactivate() {
flush_rewrite_rules();
}
/**
* Sanitisiert den per_page-Wert und bewahrt -1 für "alle".
*/
function rbr_sanitize_per_page($value) {
return (int) $value;
}
add_action('rest_api_init', function () {
// 1. Endpunkt: Alle Post-IDs abrufen
register_rest_route('custom/v1', '/posts-ids', array(
'methods' => 'GET',
'callback' => 'rbr_get_all_post_ids',
'permission_callback' => '__return_true',
'args' => array(
'post_type' => array(
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
),
'per_page' => array(
'default' => -1,
'sanitize_callback' => 'rbr_sanitize_per_page',
),
),
));
// 2. Endpunkt: Content & Medien für eine bestimmte Post-ID
register_rest_route('custom/v1', '/post-details/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_post_details_with_media',
'permission_callback' => '__return_true',
));
// 3. Endpunkt: Kategorien/Tags für alle Beiträge
register_rest_route('custom/v1', '/post-taxonomies/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_post_taxonomies',
'permission_callback' => '__return_true',
));
// 4. Endpunkt: Alle Medien-Dateien (Attachments) abrufen
register_rest_route('custom/v1', '/media', array(
'methods' => 'GET',
'callback' => 'rbr_get_all_media',
'permission_callback' => '__return_true',
'args' => array(
'per_page' => array(
'default' => -1,
'sanitize_callback' => 'rbr_sanitize_per_page',
),
),
));
// 5. Endpunkt: Medien-Datei direkt über ID abrufen
register_rest_route('custom/v1', '/media/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_media_by_id',
'permission_callback' => '__return_true',
));
});
/**
* Liefert ein Array mit allen veröffentlichten Post-IDs.
* Unterstützt verschiedene Post-Types (post, page, custom).
*/
function rbr_get_all_post_ids($request) {
$post_type = sanitize_text_field($request['post_type']);
$per_page = rbr_sanitize_per_page($request['per_page']);
$args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'numberposts' => $per_page,
'fields' => 'ids',
'orderby' => 'date',
'order' => 'ASC',
);
$post_ids = get_posts($args);
if (empty($post_ids)) {
return new WP_Error('no_posts', 'Keine Beiträge gefunden.', array('status' => 404));
}
return rest_ensure_response(array(
'count' => count($post_ids),
'post_type' => $post_type,
'ids' => $post_ids,
));
}
/**
* Liefert Content, Featured Image, Kategorien, Keywords, Comments und alle im HTML/Post genutzten Medien-URLs.
*/
function rbr_get_post_details_with_media($data) {
$post_id = intval($data['id']);
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('no_post', 'Beitrag nicht gefunden.', array('status' => 404));
}
$content = apply_filters('the_content', $post->post_content);
$media_urls = array();
// 1. Featured Image (Titelbild) auslesen
$featured_image_url = '';
$featured_image_id = get_post_thumbnail_id($post_id);
if ($featured_image_id) {
$featured_image_url = wp_get_attachment_url($featured_image_id);
if ($featured_image_url) {
$media_urls[] = $featured_image_url;
}
}
// 2. Inhaltsbilder aus dem HTML extrahieren (via Regex für <img> Tags)
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $post->post_content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $img_url) {
if (filter_var($img_url, FILTER_VALIDATE_URL)) {
$media_urls[] = $img_url;
}
}
}
// 3. Attachments die mit dem Post verknüpft sind
$attachments = get_posts(array(
'post_type' => 'attachment',
'numberposts' => -1,
'post_status' => null,
'post_parent' => $post_id,
'fields' => 'ids',
'post_mime_type' => 'image',
));
foreach ($attachments as $att_id) {
$att_url = wp_get_attachment_url($att_id);
if ($att_url) {
$media_urls[] = $att_url;
}
}
// Duplikate entfernen
$unique_media = array_values(array_unique($media_urls));
// 4. Kategorien ( Categories Only, keine Tags)
$categories = array();
$cats = get_the_category($post_id);
if (!empty($cats) && !is_wp_error($cats)) {
foreach ($cats as $cat) {
$categories[] = array(
'id' => intval($cat->term_id),
'name' => $cat->name,
'slug' => $cat->slug,
);
}
}
// 5. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
$keywords = '';
// Yoast SEO
if (function_exists('yoast_get_primary_term')) {
$keywords = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
}
// Rank Math
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
$rank_data = rank_math_get_single_post_data($post_id);
if (!empty($rank_data->get_keywords())) {
$keywords = $rank_data->get_keywords();
}
}
// Fallback: Custom Field 'post_keywords'
if (empty($keywords)) {
$keywords = get_post_meta($post_id, 'post_keywords', true);
}
// Fallback: Kategorien als Keywords
if (empty($keywords) && !empty($categories)) {
$keywords = implode(', ', array_column($categories, 'name'));
}
// 6. Kommentare
$comments = array();
$wp_comments = get_comments(array(
'post_id' => $post_id,
'status' => 'approve',
'number' => 100,
));
foreach ($wp_comments as $comment) {
$comments[] = array(
'id' => intval($comment->comment_ID),
'author_name' => $comment->comment_author,
'author_email' => $comment->comment_author_email,
'author_url' => $comment->comment_author_url,
'content' => $comment->comment_content,
'date' => mysql_to_rfc3339($comment->comment_date),
);
}
$response = array(
'id' => intval($post->ID),
'title' => $post->post_title,
'slug' => $post->post_name,
'date' => mysql_to_rfc3339($post->post_date),
'content' => $content,
'excerpt' => wp_trim_words(strip_tags($post->post_excerpt), 55),
'author' => array(
'id' => intval($post->post_author),
'name' => get_the_author_meta('display_name', $post->post_author),
),
'featured_image' => $featured_image_url,
'categories' => $categories,
'keywords' => $keywords,
'comments' => $comments,
'media_files' => $unique_media,
);
return rest_ensure_response($response);
}
/**
* Liefert Kategorien und Tags für einen bestimmten Beitrag.
*/
function rbr_get_post_taxonomies($data) {
$post_id = intval($data['id']);
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('no_post', 'Beitrag nicht gefunden.', array('status' => 404));
}
$taxonomies = get_object_taxonomies('post');
$terms = array();
foreach ($taxonomies as $taxonomy) {
$terms_list = get_the_terms($post_id, $taxonomy);
if (!empty($terms_list) && !is_wp_error($terms_list)) {
foreach ($terms_list as $term) {
$terms[] = array(
'id' => intval($term->term_id),
'name' => $term->name,
'slug' => $term->slug,
'type' => $taxonomy,
);
}
}
}
return rest_ensure_response(array(
'post_id' => $post_id,
'taxonomies' => $terms,
));
}
/**
* Liefert alle Medien-Dateien (Attachments) der WordPress-Instanz.
*/
function rbr_get_all_media($request) {
$per_page = rbr_sanitize_per_page($request['per_page']);
$args = array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'numberposts' => $per_page,
'orderby' => 'date',
'order' => 'ASC',
'post_mime_type' => array('image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'),
);
$attachments = get_posts($args);
$media_list = array();
foreach ($attachments as $att) {
$media_list[] = array(
'id' => intval($att->ID),
'url' => wp_get_attachment_url($att->ID),
'title' => $att->post_title,
'alt' => get_post_meta($att->ID, '_wp_attachment_image_alt', true),
'mime' => $att->post_mime_type,
'date' => mysql_to_rfc3339($att->post_date),
);
}
return rest_ensure_response(array(
'count' => count($media_list),
'media' => $media_list,
));
}
/**
* Liefert eine einzelne Medien-Datei anhand ihrer ID.
*/
function rbr_get_media_by_id($data) {
$media_id = intval($data['id']);
$attachment = get_post($media_id);
if (!$attachment || $attachment->post_type !== 'attachment') {
return new WP_Error('no_media', 'Medien-Datei nicht gefunden.', array('status' => 404));
}
return rest_ensure_response(array(
'id' => intval($attachment->ID),
'url' => wp_get_attachment_url($attachment->ID),
'title' => $attachment->post_title,
'alt' => get_post_meta($attachment->ID, '_wp_attachment_image_alt', true),
'mime' => $attachment->post_mime_type,
'date' => mysql_to_rfc3339($attachment->post_date),
));
}
/**
* Shortcode: [rbr_export] – Zeigt eine Export-Statistik im Frontend an.
*/
function rbr_export_shortcode($atts) {
$atts = shortcode_atts(array(
'post_type' => 'post',
), $atts, 'rbr_export');
$count = wp_count_posts($atts['post_type']);
$published = $count->publish ?? 0;
return sprintf(
'<div class="rbr-export-info"><p>Es gibt <strong>%d</strong> veröffentlichte %s-Beiträge, die über die REST-API exportiert werden können.</p></div>',
intval($published),
esc_html($atts['post_type'])
);
}
add_shortcode('rbr_export', 'rbr_export_shortcode');
/**
* Admin-Notiz: Plugin ist aktiv und bereit für den Export.
*/
add_action('admin_notices', function () {
if (current_user_can('manage_options')) {
echo '<div class="notice notice-info is-dismissible"><p>';
printf(
__('RBR Export to Ghost ist aktiv. <a href="%s">REST-API-Endpunkte</a> stehen unter <code>/wp-json/custom/v1/</code> zur Verfügung.', 'rbr-export-ghost'),
admin_url('tools.php')
);
echo '</p></div>';
}
});
/**
* CORS-Header erlauben (falls nötig für den C#-Importer)
*/
add_filter('rest_pre_serve_request', function ($served, $result, $request) {
if (strpos($request->get_route(), 'custom/v1/') === 0) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
return $served;
}, 10, 3);
add_action('rest_api_init', function () {
// OPTIONS-Requests für CORS erlauben
register_rest_route('custom/v1', '/cors', array(
'methods' => 'OPTIONS',
'callback' => function () {
return new WP_REST_Response(null, 200);
},
'permission_callback' => '__return_true',
));
});

Binary file not shown.