662 lines
27 KiB
C#
662 lines
27 KiB
C#
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; }
|
|
}
|
|
}
|