Add sitemap.xml endpointUnverified
3093622 parent: a934412 modified
docs/specs.md +2 -0 | @@ -966,6 +966,7 @@ The server renders HTML fragments that htmx swaps into the page. No JSON API nee | ||
| 966 | 966 | | `/auth/start` | POST | Start OAuth authorization flow | |
| 967 | 967 | | `/auth/callback` | GET | OAuth callback | |
| 968 | 968 | | `/terms` | GET | Terms of service | |
| 969 | +| `/sitemap.xml` | GET | XML sitemap for search engines | | |
| 969 | 970 | |
| 970 | 971 | ### 8.2 htmx Patterns |
| 971 | 972 | |
| @@ -1048,6 +1049,7 @@ glean/ | ||
| 1048 | 1049 | │ │ ├── settings_handler.go # User settings (language preferences, digest toggle) |
| 1049 | 1050 | │ │ ├── digest_handler.go # Daily digest handler (LLM summary, mark-read) |
| 1050 | 1051 | │ │ ├── recs_handler.go # Recommendation dismiss handlers |
| 1052 | +│ │ ├── sitemap_handler.go # XML sitemap handler (public pages) | |
| 1051 | 1053 | │ │ ├── terms_handler.go # Terms of service handler |
| 1052 | 1054 | │ │ ├── pagination.go # Pagination helpers |
| 1053 | 1055 | │ │ ├── middleware.go # Auth, logging, CSRF middleware |
| @@ -966,6 +966,7 @@ The server renders HTML fragments that htmx swaps into the page. No JSON API nee | |||
| 966 | | `/auth/start` | POST | Start OAuth authorization flow | | 966 | | `/auth/start` | POST | Start OAuth authorization flow | |
| 967 | | `/auth/callback` | GET | OAuth callback | | 967 | | `/auth/callback` | GET | OAuth callback | |
| 968 | | `/terms` | GET | Terms of service | | 968 | | `/terms` | GET | Terms of service | |
| 969 | +| `/sitemap.xml` | GET | XML sitemap for search engines | | ||
| 969 | 970 | ||
| 970 | ### 8.2 htmx Patterns | 971 | ### 8.2 htmx Patterns |
| 971 | 972 | ||
| @@ -1048,6 +1049,7 @@ glean/ | |||
| 1048 | │ │ ├── settings_handler.go # User settings (language preferences, digest toggle) | 1049 | │ │ ├── settings_handler.go # User settings (language preferences, digest toggle) |
| 1049 | │ │ ├── digest_handler.go # Daily digest handler (LLM summary, mark-read) | 1050 | │ │ ├── digest_handler.go # Daily digest handler (LLM summary, mark-read) |
| 1050 | │ │ ├── recs_handler.go # Recommendation dismiss handlers | 1051 | │ │ ├── recs_handler.go # Recommendation dismiss handlers |
| 1052 | +│ │ ├── sitemap_handler.go # XML sitemap handler (public pages) | ||
| 1051 | │ │ ├── terms_handler.go # Terms of service handler | 1053 | │ │ ├── terms_handler.go # Terms of service handler |
| 1052 | │ │ ├── pagination.go # Pagination helpers | 1054 | │ │ ├── pagination.go # Pagination helpers |
| 1053 | │ │ ├── middleware.go # Auth, logging, CSRF middleware | 1055 | │ │ ├── middleware.go # Auth, logging, CSRF middleware |
modified
internal/server/server.go +1 -0 | @@ -246,6 +246,7 @@ func (s *Server) setupRoutes() { | ||
| 246 | 246 | s.router.Get("/xrpc/at.glean.listFeedLists", xrpc.ListFeedLists) |
| 247 | 247 | |
| 248 | 248 | s.router.Get("/terms", s.handleTerms) |
| 249 | + s.router.Get("/sitemap.xml", s.handleSitemap) | |
| 249 | 250 | s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(static.Files)))) |
| 250 | 251 | s.router.Handle("/metrics", promhttp.Handler()) |
| 251 | 252 | s.router.Get("/stats", s.handleStats) |
| @@ -246,6 +246,7 @@ func (s *Server) setupRoutes() { | |||
| 246 | s.router.Get("/xrpc/at.glean.listFeedLists", xrpc.ListFeedLists) | 246 | s.router.Get("/xrpc/at.glean.listFeedLists", xrpc.ListFeedLists) |
| 247 | 247 | ||
| 248 | s.router.Get("/terms", s.handleTerms) | 248 | s.router.Get("/terms", s.handleTerms) |
| 249 | + s.router.Get("/sitemap.xml", s.handleSitemap) | ||
| 249 | s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(static.Files)))) | 250 | s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(static.Files)))) |
| 250 | s.router.Handle("/metrics", promhttp.Handler()) | 251 | s.router.Handle("/metrics", promhttp.Handler()) |
| 251 | s.router.Get("/stats", s.handleStats) | 252 | s.router.Get("/stats", s.handleStats) |
added
internal/server/sitemap_handler.go +55 -0 | new file mode 100644 | ||
| @@ -0,0 +1,55 @@ | ||
| 1 | +package server | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/xml" | |
| 5 | + "net/http" | |
| 6 | + "strings" | |
| 7 | + "time" | |
| 8 | +) | |
| 9 | + | |
| 10 | +type urlset struct { | |
| 11 | + XMLName xml.Name `xml:"urlset"` | |
| 12 | + Xmlns string `xml:"xmlns,attr"` | |
| 13 | + URLs []sitemapURL `xml:"url"` | |
| 14 | +} | |
| 15 | + | |
| 16 | +type sitemapURL struct { | |
| 17 | + Loc string `xml:"loc"` | |
| 18 | + Lastmod string `xml:"lastmod,omitempty"` | |
| 19 | + Changefreq string `xml:"changefreq,omitempty"` | |
| 20 | + Priority string `xml:"priority,omitempty"` | |
| 21 | +} | |
| 22 | + | |
| 23 | +func (s *Server) handleSitemap(w http.ResponseWriter, _ *http.Request) { | |
| 24 | + baseURL := s.baseURL() | |
| 25 | + | |
| 26 | + now := time.Now().UTC().Format("2006-01-02") | |
| 27 | + | |
| 28 | + urls := []sitemapURL{ | |
| 29 | + {Loc: baseURL + "/", Lastmod: now, Changefreq: "daily", Priority: "1.0"}, | |
| 30 | + {Loc: baseURL + "/trending", Lastmod: now, Changefreq: "hourly", Priority: "0.9"}, | |
| 31 | + {Loc: baseURL + "/terms", Changefreq: "monthly", Priority: "0.3"}, | |
| 32 | + } | |
| 33 | + | |
| 34 | + set := urlset{ | |
| 35 | + Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9", | |
| 36 | + URLs: urls, | |
| 37 | + } | |
| 38 | + | |
| 39 | + w.Header().Set("Content-Type", "application/xml") | |
| 40 | + w.Write([]byte(xml.Header)) | |
| 41 | + xml.NewEncoder(w).Encode(set) | |
| 42 | +} | |
| 43 | + | |
| 44 | +func (s *Server) baseURL() string { | |
| 45 | + if s.clientID == "" { | |
| 46 | + return "http://localhost:8080" | |
| 47 | + } | |
| 48 | + host := s.clientID | |
| 49 | + host, _ = strings.CutPrefix(host, "https://") | |
| 50 | + host, _ = strings.CutPrefix(host, "http://") | |
| 51 | + if i := strings.Index(host, "/"); i >= 0 { | |
| 52 | + host = host[:i] | |
| 53 | + } | |
| 54 | + return "https://" + host | |
| 55 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | +package server | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "encoding/xml" | ||
| 5 | + "net/http" | ||
| 6 | + "strings" | ||
| 7 | + "time" | ||
| 8 | +) | ||
| 9 | + | ||
| 10 | +type urlset struct { | ||
| 11 | + XMLName xml.Name `xml:"urlset"` | ||
| 12 | + Xmlns string `xml:"xmlns,attr"` | ||
| 13 | + URLs []sitemapURL `xml:"url"` | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +type sitemapURL struct { | ||
| 17 | + Loc string `xml:"loc"` | ||
| 18 | + Lastmod string `xml:"lastmod,omitempty"` | ||
| 19 | + Changefreq string `xml:"changefreq,omitempty"` | ||
| 20 | + Priority string `xml:"priority,omitempty"` | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +func (s *Server) handleSitemap(w http.ResponseWriter, _ *http.Request) { | ||
| 24 | + baseURL := s.baseURL() | ||
| 25 | + | ||
| 26 | + now := time.Now().UTC().Format("2006-01-02") | ||
| 27 | + | ||
| 28 | + urls := []sitemapURL{ | ||
| 29 | + {Loc: baseURL + "/", Lastmod: now, Changefreq: "daily", Priority: "1.0"}, | ||
| 30 | + {Loc: baseURL + "/trending", Lastmod: now, Changefreq: "hourly", Priority: "0.9"}, | ||
| 31 | + {Loc: baseURL + "/terms", Changefreq: "monthly", Priority: "0.3"}, | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + set := urlset{ | ||
| 35 | + Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9", | ||
| 36 | + URLs: urls, | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + w.Header().Set("Content-Type", "application/xml") | ||
| 40 | + w.Write([]byte(xml.Header)) | ||
| 41 | + xml.NewEncoder(w).Encode(set) | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +func (s *Server) baseURL() string { | ||
| 45 | + if s.clientID == "" { | ||
| 46 | + return "http://localhost:8080" | ||
| 47 | + } | ||
| 48 | + host := s.clientID | ||
| 49 | + host, _ = strings.CutPrefix(host, "https://") | ||
| 50 | + host, _ = strings.CutPrefix(host, "http://") | ||
| 51 | + if i := strings.Index(host, "/"); i >= 0 { | ||
| 52 | + host = host[:i] | ||
| 53 | + } | ||
| 54 | + return "https://" + host | ||
| 55 | +} | ||