diff --git a/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs b/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs
new file mode 100644
index 0000000..8f70ff1
--- /dev/null
+++ b/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs
@@ -0,0 +1,111 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using BewerbungsSeite.Models.SiteContent;
+using BewerbungsSeite.Services;
+
+namespace BewerbungsSeite.Controllers.Api.V1;
+
+///
+/// Schreibzugriff auf den Seiteninhalt. Nur fuer angemeldete Administratoren:
+/// die Konvention AuthorizeFolder("/Admin") gilt nur fuer Razor Pages, deshalb
+/// steht das [Authorize] hier ausdruecklich am Controller.
+///
+[Authorize]
+[AutoValidateAntiforgeryToken]
+[ApiController]
+[Route("api/v1/[controller]")]
+public class PageContentController(
+ IPageContentService pageContentService,
+ IWebHostEnvironment environment,
+ ILogger logger) : ControllerBase
+{
+ private static readonly string[] AllowedImageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
+ private static readonly string[] AllowedDocumentExtensions = [".pdf"];
+
+ private const long MaxImageBytes = 5 * 1024 * 1024;
+ private const long MaxDocumentBytes = 20 * 1024 * 1024;
+
+ /// Schreibt den kompletten Seiteninhalt zurueck.
+ [HttpPut]
+ public async Task Put([FromBody] PageContent content)
+ {
+ if (content is null)
+ {
+ return BadRequest(new { error = "Kein Inhalt übermittelt." });
+ }
+
+ await pageContentService.SaveAsync(content, HttpContext.RequestAborted);
+ return Ok(new { success = true, savedAt = DateTime.UtcNow });
+ }
+
+ ///
+ /// Nimmt ein Bild entgegen, legt es unter wwwroot/uploads/images ab und
+ /// liefert den Pfad zurueck, der anschliessend im JSON gespeichert wird.
+ ///
+ [HttpPost("images")]
+ [RequestSizeLimit(MaxImageBytes)]
+ public Task UploadImage(IFormFile? file) =>
+ StoreAsync(file, "images", AllowedImageExtensions, MaxImageBytes,
+ requiredContentTypePrefix: "image/",
+ wrongTypeMessage: "Erlaubt sind JPG, PNG, WEBP und GIF.",
+ tooLargeMessage: "Die Datei ist grösser als 5 MB.");
+
+ ///
+ /// Nimmt ein Dokument entgegen (PDF), legt es unter wwwroot/uploads/documents
+ /// ab und liefert den Pfad zurueck.
+ ///
+ [HttpPost("documents")]
+ [RequestSizeLimit(MaxDocumentBytes)]
+ public Task UploadDocument(IFormFile? file) =>
+ StoreAsync(file, "documents", AllowedDocumentExtensions, MaxDocumentBytes,
+ requiredContentTypePrefix: null,
+ wrongTypeMessage: "Erlaubt sind nur PDF-Dateien.",
+ tooLargeMessage: "Die Datei ist grösser als 20 MB.");
+
+ private async Task StoreAsync(
+ IFormFile? file,
+ string folder,
+ string[] allowedExtensions,
+ long maxBytes,
+ string? requiredContentTypePrefix,
+ string wrongTypeMessage,
+ string tooLargeMessage)
+ {
+ if (file is null || file.Length == 0)
+ {
+ return BadRequest(new { error = "Keine Datei übermittelt." });
+ }
+
+ if (file.Length > maxBytes)
+ {
+ return BadRequest(new { error = tooLargeMessage });
+ }
+
+ var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
+ if (!allowedExtensions.Contains(extension))
+ {
+ return BadRequest(new { error = wrongTypeMessage });
+ }
+
+ if (requiredContentTypePrefix is not null &&
+ !file.ContentType.StartsWith(requiredContentTypePrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return BadRequest(new { error = wrongTypeMessage });
+ }
+
+ // Der Dateiname wird selbst vergeben; der Name aus dem Browser wird nie uebernommen.
+ var fileName = $"{Guid.NewGuid():N}{extension}";
+ var uploadDirectory = Path.Combine(environment.WebRootPath, "uploads", folder);
+ Directory.CreateDirectory(uploadDirectory);
+
+ var targetPath = Path.Combine(uploadDirectory, fileName);
+ await using (var stream = System.IO.File.Create(targetPath))
+ {
+ await file.CopyToAsync(stream, HttpContext.RequestAborted);
+ }
+
+ logger.LogInformation("Datei hochgeladen: {Folder}/{FileName}", folder, fileName);
+
+ return Ok(new { url = $"/uploads/{folder}/{fileName}" });
+ }
+}