+
+ {/* Erläuterungen in Deutscher Gebärdensprache nach § 4 BITV 2.0.
+ Der native Player ist mit der Tastatur bedienbar; Untertitel
+ werden eingebunden, sobald eine WebVTT-Datei hinterlegt ist. */}
+ {page === "signLanguage" && video && (
+
+
+
+ Erläuterungen zu diesem Portal in Deutscher Gebärdensprache
+
+
+ )}
+
+ {/* Das Video steht in eigenen Feldern, nicht im Markdown:
+ Der Markdown-Prozessor filtert HTML weg. */}
+ {key === "signLanguage" && (
+
+ WebVTT-Datei. Nötig, sobald das Video gesprochenen Ton
+ enthält (WCAG 1.2.2).
+
+
+
+ )}
+
{
if (!row) return { ...EMPTY_SITE_PAGES }
return Object.fromEntries(
- SITE_PAGES.map((key) => [key, row[key] ?? ''])
+ [...SITE_PAGES, ...SITE_MEDIA].map((key) => [key, row[key] ?? ''])
) as SitePagesContent
}
diff --git a/lib/site-pages.ts b/lib/site-pages.ts
index dbfe539..96e87bd 100644
--- a/lib/site-pages.ts
+++ b/lib/site-pages.ts
@@ -53,8 +53,19 @@ export const SITE_PAGE_META: Record<
},
}
-/** Inhalte der Seiten, wie sie API und Admin austauschen. */
-export type SitePagesContent = Record
+/**
+ * Zusätzliche Angaben, die keine eigene Seite sind, sondern zu einer gehören.
+ *
+ * Das Gebärdensprach-Video steht bewusst in eigenen Feldern und nicht als
+ * HTML im Markdown: Der Markdown-Prozessor filtert HTML weg, und ihn dafür zu
+ * öffnen würde die Angriffsfläche über alle Textfelder ausweiten.
+ */
+export const SITE_MEDIA = ["signLanguageVideo", "signLanguageCaptions"] as const
+
+export type SiteMediaKey = (typeof SITE_MEDIA)[number]
+
+/** Alle Felder, die API und Admin austauschen. */
+export type SitePagesContent = Record
/** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */
export const EMPTY_SITE_PAGES: SitePagesContent = {
@@ -63,4 +74,23 @@ export const EMPTY_SITE_PAGES: SitePagesContent = {
accessibility: "",
easyLanguage: "",
signLanguage: "",
+ signLanguageVideo: "",
+ signLanguageCaptions: "",
+}
+
+/**
+ * Prüft eine Medienadresse. Zugelassen sind nur eine Adresse auf dem eigenen
+ * Server (mit / beginnend) und https. Damit sind `javascript:` und `data:`
+ * ausgeschlossen, bevor der Wert in ein src-Attribut gelangt.
+ */
+export function isSafeMediaUrl(value: string) {
+ const wert = value.trim()
+ if (!wert) return true
+ if (wert.startsWith("/") && !wert.startsWith("//")) return true
+
+ try {
+ return new URL(wert).protocol === "https:"
+ } catch {
+ return false
+ }
}
diff --git a/prisma/migrations/20260908160000_gebaerdensprache_video/migration.sql b/prisma/migrations/20260908160000_gebaerdensprache_video/migration.sql
new file mode 100644
index 0000000..b0899aa
--- /dev/null
+++ b/prisma/migrations/20260908160000_gebaerdensprache_video/migration.sql
@@ -0,0 +1,9 @@
+-- Video für die Erläuterungen in Deutscher Gebärdensprache (§ 4 BITV 2.0).
+--
+-- Bewusst als eigene Felder statt als rohes HTML im Markdown: Der Markdown-
+-- Prozessor filtert HTML aus gutem Grund weg, und ihn für alle Textfelder zu
+-- öffnen, würde die Angriffsfläche über sämtliche Erklärungen ausweiten.
+--
+-- Leer bedeutet: kein Video, es wird nur der Text der Seite ausgeliefert.
+ALTER TABLE "SitePages" ADD COLUMN "signLanguageVideo" TEXT NOT NULL DEFAULT '';
+ALTER TABLE "SitePages" ADD COLUMN "signLanguageCaptions" TEXT NOT NULL DEFAULT '';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6c26405..c31acf8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -43,6 +43,8 @@ model SitePages {
accessibility String @default("") // Erklärung zur Barrierefreiheit (§ 7 BITV 2.0)
easyLanguage String @default("") // Erläuterungen in Leichter Sprache (§ 4 BITV 2.0)
signLanguage String @default("") // Erläuterungen in Deutscher Gebärdensprache (§ 4 BITV 2.0)
+ signLanguageVideo String @default("") // Adresse des Gebärdensprach-Videos
+ signLanguageCaptions String @default("") // Adresse der Untertiteldatei (WebVTT), optional
updatedAt DateTime @updatedAt // Automatisch bei Update gesetzt
}