diff --git a/BewerbungWebseite/Services/PageContentSerializer.cs b/BewerbungWebseite/Services/PageContentSerializer.cs
new file mode 100644
index 0000000..95ba743
--- /dev/null
+++ b/BewerbungWebseite/Services/PageContentSerializer.cs
@@ -0,0 +1,77 @@
+using System.Text.Json;
+using BewerbungsSeite.Models.SiteContent;
+
+namespace BewerbungsSeite.Services;
+
+///
+/// Wandelt den Seiteninhalt zwischen Objektmodell und UTF-8-JSON um, so wie er
+/// im Feld PageContent.Content abgelegt wird.
+///
+public static class PageContentSerializer
+{
+ public static readonly JsonSerializerOptions Options = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ WriteIndented = false
+ };
+
+ public static byte[] Serialize(PageContent content) =>
+ JsonSerializer.SerializeToUtf8Bytes(content, Options);
+
+ ///
+ /// Liest den Inhalt zurueck. Fehlende oder unbekannte Felder fuehren nicht zu
+ /// einem Fehler: unbekannte werden ignoriert, fehlende erhalten ihren Initialwert.
+ ///
+ public static PageContent Deserialize(byte[]? content)
+ {
+ if (content is null || content.Length == 0)
+ {
+ return new PageContent();
+ }
+
+ var result = JsonSerializer.Deserialize(content, Options) ?? new PageContent();
+ return Normalize(result);
+ }
+
+ ///
+ /// Stellt sicher, dass kein Bereich null ist, auch wenn ein aelterer
+ /// Datensatz ihn noch nicht kennt oder ihn ausdruecklich als null enthaelt.
+ ///
+ private static PageContent Normalize(PageContent content)
+ {
+ content.Hero ??= new HeroSection();
+ content.Hero.TitleLines ??= new List();
+ content.Hero.PrimaryButton ??= new LinkItem();
+ content.Hero.SecondaryButton ??= new LinkItem();
+ content.Hero.SocialLinks ??= new List();
+
+ content.About ??= new AboutSection();
+ content.About.Facts ??= new List();
+
+ content.Skills ??= new SkillsSection();
+ content.Skills.Items ??= new List();
+
+ content.Projects ??= new ProjectsSection();
+ content.Projects.Items ??= new List();
+ foreach (var project in content.Projects.Items)
+ {
+ project.Details ??= new List();
+ project.Links ??= new List();
+ }
+
+ content.Goals ??= new GoalsSection();
+ content.Goals.Items ??= new List();
+
+ content.Experience ??= new ExperienceSection();
+ content.Experience.Items ??= new List();
+
+ content.Documents ??= new DocumentsSection();
+ content.Documents.Items ??= new List();
+
+ content.Contact ??= new ContactSection();
+ content.Contact.InfoBoxes ??= new List();
+
+ return content;
+ }
+}
diff --git a/BewerbungWebseite/Services/PageContentService.cs b/BewerbungWebseite/Services/PageContentService.cs
new file mode 100644
index 0000000..5e71abe
--- /dev/null
+++ b/BewerbungWebseite/Services/PageContentService.cs
@@ -0,0 +1,44 @@
+using Microsoft.EntityFrameworkCore;
+using BewerbungsSeite.Data;
+using BewerbungsSeite.Models;
+using BewerbungsSeite.Models.SiteContent;
+
+namespace BewerbungsSeite.Services;
+
+public interface IPageContentService
+{
+ /// Laedt den Seiteninhalt. Existiert noch keine Zeile, kommt ein leerer Inhalt zurueck.
+ Task GetAsync(CancellationToken cancellationToken = default);
+
+ /// Schreibt den kompletten Seiteninhalt in die eine Zeile zurueck.
+ Task SaveAsync(PageContent content, CancellationToken cancellationToken = default);
+}
+
+public class PageContentService(RazorPagesMovieContext context) : IPageContentService
+{
+ public async Task GetAsync(CancellationToken cancellationToken = default)
+ {
+ var row = await context.PageContent
+ .AsNoTracking()
+ .SingleOrDefaultAsync(p => p.Id == PageContentEntity.SingletonId, cancellationToken);
+
+ return PageContentSerializer.Deserialize(row?.Content);
+ }
+
+ public async Task SaveAsync(PageContent content, CancellationToken cancellationToken = default)
+ {
+ var row = await context.PageContent
+ .SingleOrDefaultAsync(p => p.Id == PageContentEntity.SingletonId, cancellationToken);
+
+ if (row is null)
+ {
+ row = new PageContentEntity { Id = PageContentEntity.SingletonId };
+ context.PageContent.Add(row);
+ }
+
+ row.Content = PageContentSerializer.Serialize(content);
+ row.UpdatedAt = DateTime.UtcNow;
+
+ await context.SaveChangesAsync(cancellationToken);
+ }
+}