Render News.body as Markdown over the public API

Club staff author body as Markdown in the control panel (help_text
now explains the syntax); the public API renders it to sanitized
HTML on the way out via news/services.py -- markdown for the
conversion, nh3 (Rust/ammonia) to strip anything staff's raw
Markdown source might smuggle through (script tags, event handler
attributes, javascript: URLs) before it reaches someone else's
public website. The control panel's own preview is untouched and
still shows the raw source.

Excerpt is now derived from the rendered HTML's plain text rather
than the raw Markdown source, so syntax like ** or [text](url)
doesn't leak into what's meant to be a short teaser.
This commit is contained in:
2026-08-07 22:19:46 +02:00
parent 3d03ff644c
commit 34bad16b19
7 changed files with 167 additions and 4 deletions

View File

@@ -11,13 +11,13 @@ import uuid
from datetime import datetime
from django.utils import timezone
from django.utils.text import Truncator
from ninja import Router, Schema
from ninja.errors import HttpError
from api.errors import require_club
from .models import News
from .services import render_body_excerpt, render_body_html
router = Router(tags=["news"])
@@ -68,8 +68,8 @@ def _to_news_item_out(item, request) -> NewsItemOut:
id=item.pk,
title=item.title,
slug=item.slug,
excerpt=Truncator(item.body).words(EXCERPT_WORDS, truncate=""),
body=item.body,
excerpt=render_body_excerpt(item.body, words=EXCERPT_WORDS),
body=render_body_html(item.body),
published_at=item.published_at,
teams=[team.name for team in item.teams.all()],
photos=[NewsPhotoOut(url=request.build_absolute_uri(photo.image.url), is_main=photo.is_main, ordering=photo.ordering) for photo in item.photos.all()],