Give the homepage teaser a rich text editor

Replace the plain textarea for the homepage_teaser setting with the shared
TipTap rich-text editor, and render the teaser as sanitized HTML in the
portal hero's default layout.

- SettingsAdmin: teaser field now uses RichTextEditor (lazy-loaded, code-split
  like PostEditor); rich fields render in a <div> wrapper instead of <label>.
- HeroElement: text-block lines flagged `html` render sanitized HTML.
- heroLayout: the default-layout teaser line is now an HTML line.
- admin.controller: sanitize homepage_teaser against the body allowlist on save.
- theme.css: collapse the teaser's nested block margins in the hero.

Closes #48

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 11:06:58 -05:00
parent 5ccb18e794
commit a2590812e0
5 changed files with 65 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'
import DOMPurify from 'dompurify'
const MOON_IMAGE = '/assets/img/hero-moon.png'
@@ -34,7 +35,19 @@ function TextBlock({ props }) {
return (
<div style={{ textAlign: align, textShadow: '0 2px 22px rgba(0,0,0,0.82)' }}>
{(props.lines || []).map((line, i) => {
const Tag = /^(h1|h2|h3|p|span)$/.test(line.tag) ? line.tag : 'p'
const Tag = /^(h1|h2|h3|p|span|div)$/.test(line.tag) ? line.tag : 'p'
// A rich-text line (e.g. the homepage teaser) carries sanitized HTML;
// sanitize again on render as defense in depth. Others render as text.
if (line.html) {
return (
<Tag
key={i}
className="hero-rich"
style={lineStyle(line)}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(line.text || '') }}
/>
)
}
return (
<Tag key={i} style={lineStyle(line)}>
{line.text}