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, the layout with // its modules and their data, the menu, announcements, the blog. func (s *Server) blogView(r *http.Request) (map[string]any, error) { blog := currentBlog(r) ctx := r.Context() pages, err := s.st.ListPages(ctx, blog.ID) if err != nil { return nil, err } menu, err := s.st.ListMenu(ctx, blog.ID) if err != nil { return nil, err } mods, err := s.st.ListModules(ctx, blog.ID) if err != nil { return nil, err } sections, err := s.st.EnabledSections(ctx, blog.ID) if err != nil { return nil, err } theme := ParseTheme(blog.ThemeJSON) layout := buildLayout(theme, mods) v := map[string]any{"theme": theme, "css": theme.CSS(), "layout": layout, "menu": menu, "pages": pages} // Module data is only fetched when a visible module shows it. if layout.NeedsArchive() { index, err := s.st.PublishedPostIndex(ctx, blog.ID) if err != nil { return nil, err } v["archive"] = groupArchive(index) } if n := layout.MaxRecent(); n > 0 { recent, err := s.st.RecentPublishedPosts(ctx, blog.ID, n) if err != nil { return nil, err } v["recent"] = recent } v["notices"] = placeNotices(sections, layout) return v, 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 } }