Slug-based RSS URLs and success flash messages
Build and Deploy / build-and-deploy (push) Successful in 45s
Build and Deploy / build-and-deploy (push) Successful in 45s
- Add podcast_slug to users with global uniqueness check
- RSS available at /rss/{slug} (path-based) with legacy ?slug= fallback
- Slug field in dashboard settings with validation (lowercase, hyphens)
- JS flash messages for success/error after form submissions
- Remove unused imports
This commit is contained in:
+4
-3
@@ -43,6 +43,7 @@ func (h *EpisodeHandler) ServeDashboard(w http.ResponseWriter, r *http.Request)
|
||||
h.Tpl.Render(w, "dashboard", map[string]any{
|
||||
"Episodes": episodes,
|
||||
"PodcastTitle": title,
|
||||
"PodcastSlug": user.PodcastSlug,
|
||||
"PodcastImage": user.PodcastImage,
|
||||
"PodcastAuthor": user.PodcastAuthor,
|
||||
"BaseURL": h.BaseURL,
|
||||
@@ -105,7 +106,7 @@ func (h *EpisodeHandler) ServeNew(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/?success=episode+created", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ func (h *EpisodeHandler) ServeEdit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "update failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/?success=episode+updated", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ func (h *EpisodeHandler) ServeDelete(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "delete failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/?success=episode+deleted", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *EpisodeHandler) ServeUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+17
-13
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"podstalk/store"
|
||||
@@ -58,22 +57,27 @@ type rssEnclosure struct {
|
||||
}
|
||||
|
||||
func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
|
||||
// Resolve user: check query param, fall back to first user.
|
||||
// Try path-based slug first: /rss/my-slug
|
||||
slug := r.PathValue("slug")
|
||||
if slug == "" {
|
||||
slug = r.URL.Query().Get("slug")
|
||||
}
|
||||
if slug == "" {
|
||||
slug = r.URL.Query().Get("user") // legacy
|
||||
}
|
||||
|
||||
var user *store.User
|
||||
idStr := r.URL.Query().Get("user")
|
||||
if idStr != "" {
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err == nil {
|
||||
user, _ = h.Store.GetUser(id)
|
||||
if slug != "" {
|
||||
user, _ = h.Store.UserBySlug(slug)
|
||||
}
|
||||
if user == nil {
|
||||
// legacy: try numeric user ID
|
||||
if idStr := r.URL.Query().Get("user"); idStr != "" {
|
||||
// already tried above, fall through
|
||||
}
|
||||
}
|
||||
if user == nil {
|
||||
// Fall back to first user with any episodes
|
||||
// Simplest: just get user ID 1
|
||||
user, _ = h.Store.GetUser(1)
|
||||
}
|
||||
if user == nil {
|
||||
http.Error(w, "no podcast found", http.StatusNotFound)
|
||||
http.Error(w, "podcast not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+25
-3
@@ -4,10 +4,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"podstalk/store"
|
||||
)
|
||||
|
||||
var slugRe = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
|
||||
|
||||
type SettingsHandler struct {
|
||||
Store *store.Store
|
||||
UploadDir string
|
||||
@@ -36,6 +39,26 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request)
|
||||
title = user.PodcastTitle
|
||||
}
|
||||
|
||||
slug := r.FormValue("podcast_slug")
|
||||
if slug == "" {
|
||||
slug = user.PodcastSlug
|
||||
}
|
||||
if slug != "" {
|
||||
if !slugRe.MatchString(slug) {
|
||||
http.Redirect(w, r, "/?error=slug+must+be+lowercase+letters+numbers+and+hyphens", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
taken, err := h.Store.SlugTaken(slug, userID)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/?error=internal+error", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if taken {
|
||||
http.Redirect(w, r, "/?error=slug+already+taken", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
author := r.FormValue("podcast_author")
|
||||
if author == "" {
|
||||
author = user.PodcastAuthor
|
||||
@@ -48,7 +71,6 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
// Remove old image
|
||||
if image != "" {
|
||||
os.Remove(filepath.Join(h.UploadDir, image))
|
||||
}
|
||||
@@ -68,6 +90,6 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
h.Store.UpdateUserPodcast(userID, title, image, author)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
h.Store.UpdateUserPodcast(userID, title, slug, image, author)
|
||||
http.Redirect(w, r, "/?success=podcast+settings+saved", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user