package handler import ( "encoding/xml" "net/http" "path/filepath" "strconv" "strings" "time" "ridgwaysystems.org/website/internal/blog" "ridgwaysystems.org/website/internal/feed" "ridgwaysystems.org/website/internal/status" ) const postsPerPage = 10 // indexData is passed to the index template. type indexData struct { RecentPosts []*blog.Post } func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { h.renderErr(w, http.StatusNotFound, "Page not found.") return } posts, err := h.store.All(false) if err != nil { h.renderErr(w, http.StatusInternalServerError, "Could not load posts.") return } limit := 5 if len(posts) < limit { limit = len(posts) } h.render(w, "index", indexData{RecentPosts: posts[:limit]}) } // blogData is passed to the blog list template. type blogData struct { Posts []*blog.Post Tags []string ActiveTag string SearchQuery string Page int TotalPages int HasPrev bool HasNext bool PrevPage int NextPage int } func (h *Handler) BlogList(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() tag := q.Get("tag") search := strings.TrimSpace(q.Get("q")) page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } var posts []*blog.Post var err error switch { case search != "": posts, err = h.store.Search(search) case tag != "": posts, err = h.store.ByTag(tag) default: posts, err = h.store.All(false) } if err != nil { h.renderErr(w, http.StatusInternalServerError, "Could not load posts.") return } tags, _ := h.store.AllTags() // Paginate total := len(posts) totalPages := (total + postsPerPage - 1) / postsPerPage if totalPages < 1 { totalPages = 1 } if page > totalPages { page = totalPages } start := (page - 1) * postsPerPage end := start + postsPerPage if end > total { end = total } h.render(w, "blog", blogData{ Posts: posts[start:end], Tags: tags, ActiveTag: tag, SearchQuery: search, Page: page, TotalPages: totalPages, HasPrev: page > 1, HasNext: page < totalPages, PrevPage: page - 1, NextPage: page + 1, }) } func (h *Handler) BlogPost(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if slug == "" { http.Redirect(w, r, "/blog", http.StatusSeeOther) return } post, err := h.store.Get(slug) if err != nil { h.renderErr(w, http.StatusNotFound, "Post not found.") return } // Drafts are visible to authenticated admins only if post.Draft && !isAuthenticated(r) { h.renderErr(w, http.StatusNotFound, "Post not found.") return } h.render(w, "post", post) } func (h *Handler) Feed(w http.ResponseWriter, r *http.Request) { posts, err := h.store.All(false) if err != nil { http.Error(w, "feed unavailable", http.StatusInternalServerError) return } rss, err := feed.RSS(h.siteURL, "Ridgway Systems", "A homelab built on OpenBSD.", posts) if err != nil { http.Error(w, "feed error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") w.Write(rss) } func (h *Handler) Infrastructure(w http.ResponseWriter, r *http.Request) { h.render(w, "infrastructure", nil) } // statusData is passed to the status template. type statusData struct { Page *status.Page LastChecked string } func (h *Handler) Status(w http.ResponseWriter, r *http.Request) { p, err := status.Load(filepath.Join(h.dataDir, "status.json")) if err != nil { p = &status.Page{LastChecked: time.Now(), Services: []status.Service{}} } var lastChecked string if !p.LastChecked.IsZero() { lastChecked = p.LastChecked.UTC().Format("2006-01-02 15:04 UTC") } h.render(w, "status", statusData{Page: p, LastChecked: lastChecked}) } func (h *Handler) About(w http.ResponseWriter, r *http.Request) { h.render(w, "about", nil) } // --- Sitemap --- type urlset struct { XMLName xml.Name `xml:"urlset"` Xmlns string `xml:"xmlns,attr"` URLs []sitemapURL `xml:"url"` } type sitemapURL struct { Loc string `xml:"loc"` LastMod string `xml:"lastmod,omitempty"` Freq string `xml:"changefreq,omitempty"` Prio string `xml:"priority,omitempty"` } func (h *Handler) Sitemap(w http.ResponseWriter, r *http.Request) { posts, _ := h.store.All(false) urls := []sitemapURL{ {Loc: h.siteURL + "/", Freq: "weekly", Prio: "1.0"}, {Loc: h.siteURL + "/blog", Freq: "weekly", Prio: "0.9"}, {Loc: h.siteURL + "/infrastructure", Freq: "monthly", Prio: "0.7"}, {Loc: h.siteURL + "/status", Freq: "daily", Prio: "0.6"}, {Loc: h.siteURL + "/about", Freq: "monthly", Prio: "0.5"}, } for _, p := range posts { u := sitemapURL{ Loc: h.siteURL + "/blog/" + p.Slug, Freq: "never", Prio: "0.8", } if !p.ParsedDate.IsZero() { u.LastMod = p.ParsedDate.Format("2006-01-02") } urls = append(urls, u) } out, err := xml.MarshalIndent(urlset{ Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9", URLs: urls, }, "", " ") if err != nil { http.Error(w, "sitemap error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.Write([]byte(xml.Header)) w.Write(out) }