Add markdown event descriptions for hosts to provide context

Hosts can now add a free-form description (with markdown rendering via
goldmark) when creating or editing events. Descriptions render safely
with no raw HTML passthrough. Includes ALTER TABLE migration for
existing databases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 08:02:24 -04:00
parent 4fc79d4491
commit 7b0efb3c45
13 changed files with 189 additions and 35 deletions
+45 -13
View File
@@ -6,6 +6,7 @@ import (
"database/sql"
"encoding/hex"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
@@ -17,6 +18,7 @@ import (
const (
maxFieldLen = 200
maxDescLen = 5000
maxNoteLen = 500
maxSlots = 20
maxRsvps = 200
@@ -57,6 +59,7 @@ func (s *Server) handleCreateEvent(w http.ResponseWriter, r *http.Request) {
date := sanitize(r.FormValue("date"), maxFieldLen)
time_ := sanitize(r.FormValue("time"), maxFieldLen)
location := sanitize(r.FormValue("location"), maxFieldLen)
description := sanitize(r.FormValue("description"), maxDescLen)
if title == "" {
http.Error(w, "Title is required", http.StatusBadRequest)
@@ -66,7 +69,7 @@ func (s *Server) handleCreateEvent(w http.ResponseWriter, r *http.Request) {
slug := randomSlug()
token := randomToken()
event, err := s.q.CreateEvent(r.Context(), db.CreateEventParams{
Slug: slug, Title: title, Date: date, Time: time_, Location: location, AdminToken: token,
Slug: slug, Title: title, Date: date, Time: time_, Location: location, AdminToken: token, Description: description,
})
if err != nil {
log.Printf("create event: %v", err)
@@ -119,12 +122,13 @@ type SlotView struct {
}
type EventPageData struct {
Event db.Event
Slots []SlotView
Rsvps []db.Rsvp
TotalGoing int64
IsAdmin bool
BaseURL string
Event db.Event
Slots []SlotView
Rsvps []db.Rsvp
TotalGoing int64
IsAdmin bool
BaseURL string
DescriptionHTML template.HTML
}
func (s *Server) loadEventPage(r *http.Request, slug string, isAdmin bool) (*EventPageData, error) {
@@ -176,13 +180,19 @@ func (s *Server) loadEventPage(r *http.Request, slug string, isAdmin bool) (*Eve
}
totalGoing += int64(len(rsvps))
var descHTML template.HTML
if event.Description != "" {
descHTML = RenderMarkdown(event.Description)
}
return &EventPageData{
Event: event,
Slots: slotViews,
Rsvps: rsvps,
TotalGoing: totalGoing,
IsAdmin: isAdmin,
BaseURL: s.baseURL,
Event: event,
Slots: slotViews,
Rsvps: rsvps,
TotalGoing: totalGoing,
IsAdmin: isAdmin,
BaseURL: s.baseURL,
DescriptionHTML: descHTML,
}, nil
}
@@ -425,6 +435,28 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
pageTmpl["event"].ExecuteTemplate(w, "layout", data)
}
func (s *Server) handleUpdateDescription(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")
event, err := s.q.GetEventBySlug(r.Context(), slug)
if err != nil || event.AdminToken != token {
http.NotFound(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
r.ParseForm()
description := sanitize(r.FormValue("description"), maxDescLen)
s.q.UpdateEventDescription(r.Context(), db.UpdateEventDescriptionParams{
Description: description,
ID: event.ID,
})
http.Redirect(w, r, fmt.Sprintf("/e/%s/admin/%s", slug, token), http.StatusSeeOther)
}
func (s *Server) handleCreateSlot(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
token := chi.URLParam(r, "token")