/** * 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 /** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */ export const EMPTY_SITE_PAGES: SitePagesContent = { imprint: "", privacy: "", accessibility: "", easyLanguage: "", signLanguage: "", signLanguageVideo: "", signLanguageCaptions: "", } /** * Anbieter, deren Player als eingebetteter Rahmen zugelassen ist. * * Bewusst eine kurze Liste statt einer allgemeinen Freigabe für iframes: Ein * eingebetteter Rahmen lädt beim Seitenaufruf von einem fremden Server und * überträgt dabei IP-Adresse und Browserkennung der Besucherinnen und * Besucher. Das ist bei einem Gebärdensprach-Dienst vertretbar, für den ein * Auftragsverarbeitungsvertrag besteht – bei einem Videoportal wie YouTube * wäre es das nicht. * * Eingetragen wird die Domain; Unterdomains sind eingeschlossen. */ export const EMBED_HOSTS = ["alangu.de"] as const /** * Hat eine Seite Inhalt und wird deshalb ausgeliefert und verlinkt? * * Auf der Gebärdensprach-Seite ist das Video der eigentliche Inhalt; sie zählt * daher auch ohne Text als gepflegt. Fußzeile und Seite selbst müssen dieselbe * Regel verwenden, sonst gibt es eine erreichbare Seite ohne Link oder einen * Link ins Leere. */ export function hasSitePageContent( pages: SitePagesContent, key: SitePageKey ): boolean { if (pages[key].trim()) return true return key === "signLanguage" && pages.signLanguageVideo.trim().length > 0 } /** Gehört die Adresse zu einem zugelassenen Einbettungs-Anbieter? */ export function isAllowedEmbedUrl(value: string) { const wert = value.trim() if (!wert) return false try { const url = new URL(wert) if (url.protocol !== "https:") return false return EMBED_HOSTS.some( (host) => url.hostname === host || url.hostname.endsWith(`.${host}`) ) } catch { return false } } /** * Prüft eine Medienadresse. Zugelassen sind 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 } }