Rich-text fields (hero lead, paragraphs, FAQ-style copy, datenschutz sections, etc.) now use a proper toolbar editor - bold/italic/underline buttons plus a per-selection font picker drawn from the same curated font list as the Design tab - instead of typed **markdown** syntax. Content is stored as a small allowlisted HTML subset (strong/em/u/span with a validated data-font/br) and sanitized server-side with sanitize-html on every save (admin/lib/rich-sanitize.js), independent of what the client sends - verified this rejects script tags, event handlers, and made-up font names while preserving legitimate markup, and that re-saving already-sanitized content is a no-op. The Astro site mirrors the same allowlist at render time for defense-in-depth (src/lib/rich-sanitize.ts) and only requests Google Fonts for fonts actually referenced somewhere in content, in addition to the two Design-tab fonts - verified a font used via a data-font span gets its own Google Fonts request and [data-font] CSS rule in the built page, and that unused fonts don't get requested. Migrated the three existing fields that used the old markdown-lite syntax (**bold**/*italic*) to real HTML so they keep rendering correctly under the new system. Also replaces the single long scrolling admin page with a tabbed sidebar (grouped "Inhalte"/"Design") to cut down on clutter, per feedback that the all-in-one-page layout was hard to navigate.
36 lines
1.2 KiB
JavaScript
36 lines
1.2 KiB
JavaScript
const sanitizeHtml = require('sanitize-html');
|
|
const { FONT_NAMES } = require('./fonts');
|
|
|
|
const OPTIONS = {
|
|
// b/i are included because execCommand('bold'/'italic') emits those in
|
|
// some browsers; transformTags normalizes them to the semantic tags.
|
|
allowedTags: ['strong', 'em', 'u', 'span', 'br', 'b', 'i'],
|
|
transformTags: {
|
|
b: 'strong',
|
|
i: 'em',
|
|
},
|
|
allowedAttributes: {
|
|
span: ['data-font'],
|
|
},
|
|
// Reject any span whose data-font isn't one of our curated fonts, instead
|
|
// of silently passing through an attacker-controlled string.
|
|
exclusiveFilter: (frame) => {
|
|
if (frame.tag === 'span') {
|
|
const font = frame.attribs && frame.attribs['data-font'];
|
|
return !font || !FONT_NAMES.includes(font);
|
|
}
|
|
return false;
|
|
},
|
|
disallowedTagsMode: 'discard',
|
|
};
|
|
|
|
/** Sanitizes rich-text HTML from the admin editor down to a small allowlist
|
|
* (bold/italic/underline + font-picker spans) before it's stored or rendered
|
|
* on the public site. Anything else - scripts, styles, links, attributes -
|
|
* is stripped, not escaped, so plain text content is preserved. */
|
|
function sanitizeRichText(html) {
|
|
if (typeof html !== 'string') return '';
|
|
return sanitizeHtml(html, OPTIONS).trim();
|
|
}
|
|
|
|
module.exports = { sanitizeRichText };
|