Ein DGS-Video liess sich bisher gar nicht einbinden: Der Markdown- Prozessor laeuft mit Sanitizing, und remark reicht rohes HTML dann von vornherein nicht durch - <video> kam als leeres <p> heraus. Eine erweiterte Freigabeliste allein half deshalb nicht. Statt rohes HTML fuer alle Textfelder zu oeffnen - auch fuer die 121 Erklaerungen - bekommt die Gebaerdensprach-Seite zwei eigene Felder fuer Video- und Untertiteladresse. Ausgegeben wird ein natives <video controls> mit <track kind="captions"> und Bildunterschrift. Adressen werden zweimal geprueft, beim Speichern und vor der Ausgabe: zugelassen sind nur der eigene Server und https. javascript:, data:, protokollrelative und einfache http-Adressen werden abgewiesen. iframe bleibt bewusst gesperrt - eine YouTube-Einbindung wuerde Besucherdaten an Dritte uebertragen. Erklaerung zur Barrierefreiheit entsprechend gekuerzt: Die beiden Luecken nach § 4 BITV 2.0 entfallen, es bleibt die ungepruefte Druckansicht. Geprueft: Video wird mit Untertitelspur ausgeliefert und ist per Tastatur erreichbar, axe-core meldet auf beiden neuen Seiten in hell und dunkel keine Verstoesse, Adresspruefung weist alle acht Testfaelle korrekt zu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
/**
|
||
* Die eigenständigen Seiten des Portals.
|
||
*
|
||
* Drei rechtliche Pflichtseiten sowie zwei Seiten, die § 4 BITV 2.0 von
|
||
* öffentlichen Stellen verlangt: Erläuterungen in Leichter Sprache und in
|
||
* Deutscher Gebärdensprache.
|
||
*/
|
||
export const SITE_PAGES = [
|
||
"imprint",
|
||
"privacy",
|
||
"accessibility",
|
||
"easyLanguage",
|
||
"signLanguage",
|
||
] as const
|
||
|
||
/** Bezeichner einer Seite. */
|
||
export type SitePageKey = (typeof SITE_PAGES)[number]
|
||
|
||
/** Überschrift und URL je Seite – für Fußzeile, Admin und die Seiten selbst. */
|
||
export const SITE_PAGE_META: Record<
|
||
SitePageKey,
|
||
{ title: string; short: string; path: string; hint: string }
|
||
> = {
|
||
imprint: {
|
||
title: "Impressum",
|
||
short: "Impressum",
|
||
path: "/impressum",
|
||
hint: "Anbieterkennzeichnung nach § 5 DDG",
|
||
},
|
||
privacy: {
|
||
title: "Datenschutzerklärung",
|
||
short: "Datenschutz",
|
||
path: "/datenschutz",
|
||
hint: "Datenverarbeitung durch dieses Portal selbst",
|
||
},
|
||
accessibility: {
|
||
title: "Erklärung zur Barrierefreiheit",
|
||
short: "Barrierefreiheit",
|
||
path: "/barrierefreiheit",
|
||
hint: "Pflichtangabe öffentlicher Stellen nach § 7 BITV 2.0, jährlich zu überprüfen",
|
||
},
|
||
easyLanguage: {
|
||
title: "Leichte Sprache",
|
||
short: "Leichte Sprache",
|
||
path: "/leichte-sprache",
|
||
hint: "Pflichtangabe nach § 4 BITV 2.0: wesentliche Inhalte, Navigation und Barrierefreiheitserklärung in Leichter Sprache",
|
||
},
|
||
signLanguage: {
|
||
title: "Gebärdensprache",
|
||
short: "Gebärdensprache",
|
||
path: "/gebaerdensprache",
|
||
hint: "Pflichtangabe nach § 4 BITV 2.0: dieselben Inhalte in Deutscher Gebärdensprache, üblicherweise als Video",
|
||
},
|
||
}
|
||
|
||
/**
|
||
* 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<SitePageKey | SiteMediaKey, string>
|
||
|
||
/** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */
|
||
export const EMPTY_SITE_PAGES: SitePagesContent = {
|
||
imprint: "",
|
||
privacy: "",
|
||
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
|
||
}
|
||
}
|