aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_pages.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/handlers_pages.go')
-rw-r--r--internal/web/handlers_pages.go161
1 files changed, 161 insertions, 0 deletions
diff --git a/internal/web/handlers_pages.go b/internal/web/handlers_pages.go
new file mode 100644
index 0000000..1b79280
--- /dev/null
+++ b/internal/web/handlers_pages.go
@@ -0,0 +1,161 @@
+package web
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/gramanas/blogspace/internal/markdown"
+ "github.com/gramanas/blogspace/internal/slug"
+ "github.com/gramanas/blogspace/internal/store"
+)
+
+// Page slugs that would collide with blog routes.
+var reservedPageSlugs = map[string]bool{"media": true, "static": true, "feed.xml": true}
+
+func (s *Server) handlePages(w http.ResponseWriter, r *http.Request) {
+ pages, err := s.st.ListPages(r.Context(), currentBlog(r).ID)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ s.render(w, r, "dashboard/pages.html", map[string]any{"pages": pages})
+}
+
+// loadPage fetches the page named in the URL, or nil (having written the response) on failure.
+func (s *Server) loadPage(w http.ResponseWriter, r *http.Request) *store.Page {
+ id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ p, err := s.st.PageByID(r.Context(), currentBlog(r).ID, id)
+ if err != nil {
+ if errors.Is(err, store.ErrNotFound) {
+ http.NotFound(w, r)
+ } else {
+ s.serverError(w, err)
+ }
+ return nil
+ }
+ return p
+}
+
+func (s *Server) handlePageForm(w http.ResponseWriter, r *http.Request) {
+ p := &store.Page{ShowInNav: true}
+ if r.PathValue("id") != "" {
+ if p = s.loadPage(w, r); p == nil {
+ return
+ }
+ }
+ s.render(w, r, "dashboard/page_form.html", map[string]any{"page": p})
+}
+
+func (s *Server) handlePageSave(w http.ResponseWriter, r *http.Request) {
+ blog := currentBlog(r)
+ p := &store.Page{BlogID: blog.ID}
+ if r.PathValue("id") != "" {
+ if p = s.loadPage(w, r); p == nil {
+ return
+ }
+ }
+ p.Title = strings.TrimSpace(r.FormValue("title"))
+ p.Slug = strings.TrimSpace(r.FormValue("slug"))
+ p.IntroMD = r.FormValue("intro")
+ p.ShowInNav = r.FormValue("show_in_nav") == "on"
+ autoSlug := p.Slug == ""
+ if autoSlug {
+ p.Slug = slug.Make(p.Title)
+ }
+ var msg string
+ switch {
+ case p.Title == "" || len(p.Title) > 120:
+ msg = "Title is required (max 120 characters)."
+ case !slug.Valid(p.Slug) || reservedPageSlugs[p.Slug]:
+ msg = "Slug may only contain lowercase letters, digits and dashes (and not be a reserved word)."
+ }
+ if msg != "" {
+ s.renderStatus(w, r, http.StatusBadRequest, "dashboard/page_form.html", map[string]any{"page": p, "error": msg})
+ return
+ }
+ p.IntroHTML = markdown.Render(p.IntroMD)
+ var err error
+ base := p.Slug
+ for n := 1; ; n++ { // generated slugs get -2, -3… on collision; typed ones report the conflict
+ p.Slug = slug.WithSuffix(base, n)
+ if p.ID == 0 {
+ var created *store.Page
+ if created, err = s.st.CreatePage(r.Context(), p); err == nil {
+ p = created
+ }
+ } else {
+ err = s.st.UpdatePage(r.Context(), p)
+ }
+ if !errors.Is(err, store.ErrConflict) || !autoSlug || n >= 50 {
+ break
+ }
+ }
+ if err != nil {
+ if errors.Is(err, store.ErrConflict) {
+ s.renderStatus(w, r, http.StatusConflict, "dashboard/page_form.html", map[string]any{"page": p, "error": "A page with that slug already exists."})
+ return
+ }
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/b/"+blog.Subdomain+"/pages/"+strconv.FormatInt(p.ID, 10)+"/edit", "Saved. Refresh your blog to see it.")
+}
+
+func (s *Server) handlePageDeleteConfirm(w http.ResponseWriter, r *http.Request) {
+ p := s.loadPage(w, r)
+ if p == nil {
+ return
+ }
+ s.render(w, r, "dashboard/confirm.html", map[string]any{
+ "what": "the page \"" + p.Title + "\" and all " + strconv.Itoa(p.PostCount) + " of its posts",
+ "action": r.URL.Path,
+ "back": "/b/" + currentBlog(r).Subdomain + "/pages",
+ "isHome": p.IsHome,
+ })
+}
+
+func (s *Server) handlePageDelete(w http.ResponseWriter, r *http.Request) {
+ p := s.loadPage(w, r)
+ if p == nil {
+ return
+ }
+ if p.IsHome {
+ s.plainError(w, http.StatusBadRequest, "The home page cannot be deleted. Make another page the home page first.")
+ return
+ }
+ if err := s.st.DeletePage(r.Context(), p.BlogID, p.ID); err != nil {
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", "Page deleted.")
+}
+
+func (s *Server) handlePageMove(w http.ResponseWriter, r *http.Request) {
+ p := s.loadPage(w, r)
+ if p == nil {
+ return
+ }
+ dir := 1
+ if r.FormValue("dir") == "up" {
+ dir = -1
+ }
+ if err := s.st.MovePage(r.Context(), p.BlogID, p.ID, dir); err != nil {
+ s.serverError(w, err)
+ return
+ }
+ http.Redirect(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", http.StatusSeeOther)
+}
+
+func (s *Server) handlePageHome(w http.ResponseWriter, r *http.Request) {
+ p := s.loadPage(w, r)
+ if p == nil {
+ return
+ }
+ if err := s.st.SetHomePage(r.Context(), p.BlogID, p.ID); err != nil {
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", "\""+p.Title+"\" is now the home page.")
+}