WEB-0: CMS content model + /api/site-snapshot + Page/Block CRUD

- Entities: Site (singleton, navOrder JSON), Page (slug/title/seoDescription/status enum),
  Block (single table, Type enum + Data JSON; RichText=Markdown), Media. Migration
  AddCmsEntities, seeds 6 Jimdo-mirror pages (placeholder Heading each; abgabetiere also an
  auto AbgabetiereList block) + Site singleton nav.
- GET /api/site-snapshot: one JSON doc (site nav + pages + ordered blocks {id,order,type,data});
  AbgabetiereList(auto) resolved from live ForSale gerbils -> {name, farbschlag, group(=Becken),
  photos[urls], aiSaleText:null} (no price per god). SiteSnapshotService.
- CRUD: GET/POST/PUT/DELETE pages; add/update/delete blocks; PUT .../blocks/order reorder.
  Block type allowlisted by enum; data validated as JSON object.
This commit is contained in:
2026-06-06 09:19:42 +02:00
parent 13ff97a12b
commit 94f055f9bf
9 changed files with 2095 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
using System.ComponentModel.DataAnnotations;
namespace GerbilManagerWebAPI.Models
{
/// <summary>Publication state of a CMS page (serialised as string name).</summary>
public enum PageStatus
{
Draft = 0,
Published = 1,
}
/// <summary>
/// CMS block kinds. RichText holds MARKDOWN (sanitised on render). AbgabetiereList is
/// dynamic — resolved from live ForSale animals at snapshot time. Serialised as string.
/// </summary>
public enum BlockType
{
Heading = 0,
RichText = 1,
Image = 2,
Gallery = 3,
ContactInfo = 4,
AbgabetiereList = 5,
}
/// <summary>Singleton site settings (WEB epic CMS).</summary>
public class Site
{
/// <summary>Fixed id — there is exactly one Site row.</summary>
public static readonly Guid SingletonId = new("5172e000-0000-0000-0000-000000000001");
[Key]
public Guid Id { get; set; }
public string DefaultLocale { get; set; } = "de";
/// <summary>Ordered page ids for the public nav (JSON list).</summary>
public List<Guid> NavOrder { get; set; } = new();
}
/// <summary>A CMS page (one of the public website's pages).</summary>
public class Page
{
[Key]
public Guid Id { get; set; }
/// <summary>Stable URL slug (e.g. "abgabetiere") — the human key.</summary>
public required string Slug { get; set; }
public required string Title { get; set; }
public string? SeoDescription { get; set; }
public PageStatus Status { get; set; } = PageStatus.Draft;
public ICollection<Block> Blocks { get; } = new List<Block>();
}
/// <summary>
/// A content block on a page (single table, discriminated by <see cref="Type"/>).
/// Type-specific payload lives in <see cref="Data"/> as JSON (RichText = Markdown).
/// </summary>
public class Block
{
[Key]
public Guid Id { get; set; }
public Guid PageId { get; set; }
/// <summary>Explicit ordering within the page (AI tools reorder by this).</summary>
public int Order { get; set; }
public BlockType Type { get; set; }
/// <summary>Type-specific fields as a JSON object string.</summary>
public string Data { get; set; } = "{}";
}
/// <summary>A media asset referenced by Image/Gallery blocks.</summary>
public class Media
{
[Key]
public Guid Id { get; set; }
public required string FileName { get; set; }
public required string Url { get; set; }
public string? Alt { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
}
}