aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_blog.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/handlers_blog.go')
-rw-r--r--internal/web/handlers_blog.go150
1 files changed, 150 insertions, 0 deletions
diff --git a/internal/web/handlers_blog.go b/internal/web/handlers_blog.go
new file mode 100644
index 0000000..b768c4d
--- /dev/null
+++ b/internal/web/handlers_blog.go
@@ -0,0 +1,150 @@
+package web
+
+import (
+ "encoding/xml"
+ "errors"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gramanas/blogspace/internal/store"
+)
+
+const postsPerPage = 10
+
+// blogView gathers what every public page needs: theme css, nav, the blog.
+func (s *Server) blogView(r *http.Request) (map[string]any, error) {
+ blog := currentBlog(r)
+ pages, err := s.st.ListPages(r.Context(), blog.ID)
+ if err != nil {
+ return nil, err
+ }
+ theme := ParseTheme(blog.ThemeJSON)
+ var nav []store.Page
+ for _, p := range pages {
+ if p.ShowInNav && (theme.NavShowHome || !p.IsHome) {
+ nav = append(nav, p)
+ }
+ }
+ return map[string]any{"theme": theme, "css": theme.CSS(), "nav": nav}, nil
+}
+
+func (s *Server) handleBlogHome(w http.ResponseWriter, r *http.Request) {
+ page, err := s.st.HomePage(r.Context(), currentBlog(r).ID)
+ if err != nil {
+ s.blogNotFound(w, r)
+ return
+ }
+ s.renderPage(w, r, page)
+}
+
+func (s *Server) handleBlogPage(w http.ResponseWriter, r *http.Request) {
+ page, err := s.st.PageBySlug(r.Context(), currentBlog(r).ID, r.PathValue("page"))
+ if err != nil {
+ s.blogNotFound(w, r)
+ return
+ }
+ if page.IsHome { // canonical URL for the home page is /
+ http.Redirect(w, r, "/", http.StatusMovedPermanently)
+ return
+ }
+ s.renderPage(w, r, page)
+}
+
+func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store.Page) {
+ v, err := s.blogView(r)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ n, _ := strconv.Atoi(r.URL.Query().Get("p"))
+ if n < 1 {
+ n = 1
+ }
+ posts, total, err := s.st.PublishedPosts(r.Context(), page.ID, postsPerPage, (n-1)*postsPerPage)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ last := (total + postsPerPage - 1) / postsPerPage
+ v["page"], v["posts"], v["pageNum"], v["lastPage"] = page, posts, n, last
+ v["base"] = "/" + page.Slug
+ if page.IsHome {
+ v["base"] = ""
+ }
+ s.render(w, r, "blog/page.html", v)
+}
+
+func (s *Server) handleBlogPost(w http.ResponseWriter, r *http.Request) {
+ blog := currentBlog(r)
+ page, err := s.st.PageBySlug(r.Context(), blog.ID, r.PathValue("page"))
+ if err != nil {
+ s.blogNotFound(w, r)
+ return
+ }
+ post, err := s.st.PublishedPostBySlug(r.Context(), page.ID, r.PathValue("post"))
+ if err != nil {
+ s.blogNotFound(w, r)
+ return
+ }
+ v, err := s.blogView(r)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ v["page"], v["post"] = page, post
+ s.render(w, r, "blog/post.html", v)
+}
+
+func (s *Server) blogNotFound(w http.ResponseWriter, r *http.Request) {
+ v, err := s.blogView(r)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ s.renderStatus(w, r, http.StatusNotFound, "blog/404.html", v)
+}
+
+// ---- RSS ---------------------------------------------------------------------
+
+type rss struct {
+ XMLName xml.Name `xml:"rss"`
+ Version string `xml:"version,attr"`
+ Channel struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ Description string `xml:"description"`
+ Items []rssItem `xml:"item"`
+ } `xml:"channel"`
+}
+
+type rssItem struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ GUID string `xml:"guid"`
+ PubDate string `xml:"pubDate"`
+ Desc string `xml:"description"`
+}
+
+func (s *Server) handleBlogFeed(w http.ResponseWriter, r *http.Request) {
+ blog := currentBlog(r)
+ posts, err := s.st.RecentPublishedPosts(r.Context(), blog.ID, 30)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ base := s.cfg.BlogURL(blog.Subdomain)
+ var f rss
+ f.Version = "2.0"
+ f.Channel.Title, f.Channel.Link, f.Channel.Description = blog.Title, base, blog.Tagline
+ for _, p := range posts {
+ link := base + "/" + p.PageSlug + "/" + p.Slug
+ f.Channel.Items = append(f.Channel.Items, rssItem{Title: p.Title, Link: link, GUID: link,
+ PubDate: p.CreatedAt.Format(time.RFC1123Z), Desc: p.BodyHTML})
+ }
+ w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
+ w.Write([]byte(xml.Header))
+ if err := xml.NewEncoder(w).Encode(f); err != nil && !errors.Is(err, http.ErrHandlerTimeout) {
+ return
+ }
+}