package server import ( "fmt" "log" "net/http" "strings" "time" "gitblog/internal/cache" "gitblog/internal/config" "gitblog/internal/content" "gitblog/internal/github" "gitblog/internal/templates" "github.com/gorilla/handlers" "github.com/gorilla/mux" ) type Server struct { config *config.Config githubClient *github.Client contentMgr *content.Manager cacheMgr *cache.Manager templateMgr *templates.Manager updateTicker *time.Ticker } func New(cfg *config.Config) *Server { githubClient := github.NewClient(cfg.GitHubToken, cfg.GitHubOwner, cfg.GitHubRepo) contentMgr := content.NewManager(githubClient) cacheMgr := cache.NewManager(cfg.CacheDuration, 5*time.Minute) templateMgr := templates.NewManager() return &Server{ config: cfg, githubClient: githubClient, contentMgr: contentMgr, cacheMgr: cacheMgr, templateMgr: templateMgr, } } func (s *Server) Start() error { if err := s.templateMgr.LoadTemplates(); err != nil { return fmt.Errorf("failed to load templates: %w", err) } if err := s.contentMgr.LoadContent(); err != nil { return fmt.Errorf("failed to load content: %w", err) } s.startContentUpdater() router := s.setupRoutes() corsHandler := handlers.CORS( handlers.AllowedOrigins([]string{"*"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), )(router) loggedRouter := handlers.LoggingHandler(log.Writer(), corsHandler) server := &http.Server{ Addr: ":" + s.config.Port, Handler: loggedRouter, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } return server.ListenAndServe() } func (s *Server) setupRoutes() *mux.Router { router := mux.NewRouter() router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/")))) router.HandleFunc("/", s.handleHome).Methods("GET") router.HandleFunc("/post/{slug}", s.handlePost).Methods("GET") router.HandleFunc("/category/{category}", s.handleCategory).Methods("GET") router.HandleFunc("/page/{path:.*}", s.handlePage).Methods("GET") router.HandleFunc("/rss.xml", s.handleRSS).Methods("GET") router.HandleFunc("/api/update", s.handleUpdate).Methods("POST") router.NotFoundHandler = http.HandlerFunc(s.handle404) return router } func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { theme := s.getTheme(r) posts, found := s.cacheMgr.GetPosts() if !found { posts = s.contentMgr.GetAllPosts() s.cacheMgr.SetPosts(posts, s.config.CacheDuration) } data := templates.PageData{ Title: "Home", Posts: posts, Navigation: s.contentMgr.GetNavigation(), Theme: theme, CurrentYear: time.Now().Year(), } html, err := s.templateMgr.Render("home", data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func (s *Server) handlePost(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) slug := vars["slug"] theme := s.getTheme(r) post, found := s.cacheMgr.GetPost(slug) if !found { post, found = s.contentMgr.GetPost(slug) if !found { s.handle404(w, r) return } s.cacheMgr.SetPost(slug, post, s.config.CacheDuration) } data := templates.PageData{ Title: post.Title, Post: post, Navigation: s.contentMgr.GetNavigation(), Theme: theme, CurrentYear: time.Now().Year(), Meta: map[string]string{ "description": post.Excerpt, }, } html, err := s.templateMgr.Render("post", data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func (s *Server) handleCategory(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) category := vars["category"] theme := s.getTheme(r) posts, found := s.cacheMgr.GetPostsByCategory(category) if !found { posts = s.contentMgr.GetPostsByCategory(category) s.cacheMgr.SetPostsByCategory(category, posts, s.config.CacheDuration) } data := templates.PageData{ Title: strings.Title(category), Posts: posts, Navigation: s.contentMgr.GetNavigation(), Theme: theme, CurrentYear: time.Now().Year(), } html, err := s.templateMgr.Render("category", data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func (s *Server) handlePage(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) path := vars["path"] theme := s.getTheme(r) page, found := s.cacheMgr.GetPage(path) if !found { page, found = s.contentMgr.GetPage(path) if !found { s.handle404(w, r) return } s.cacheMgr.SetPage(path, page, s.config.CacheDuration) } data := templates.PageData{ Title: page.Title, Page: page, Navigation: s.contentMgr.GetNavigation(), Theme: theme, CurrentYear: time.Now().Year(), } html, err := s.templateMgr.Render("page", data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func (s *Server) handleRSS(w http.ResponseWriter, r *http.Request) { posts := s.contentMgr.GetAllPosts() rss := ` GitBlog A modern blog powered by GitHub ` + s.config.BaseURL + ` ` + time.Now().Format(time.RFC1123Z) + ` ` for _, post := range posts { rss += ` ` + post.Title + ` ` + s.config.BaseURL + `/post/` + post.Slug + ` ` + post.Date.Format(time.RFC1123Z) + ` ` } rss += ` ` w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") w.Write([]byte(rss)) } func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) { if err := s.contentMgr.LoadContent(); err != nil { http.Error(w, "Failed to update content", http.StatusInternalServerError) return } s.cacheMgr.Clear() w.WriteHeader(http.StatusOK) w.Write([]byte("Content updated successfully")) } func (s *Server) handle404(w http.ResponseWriter, r *http.Request) { theme := s.getTheme(r) data := templates.PageData{ Title: "404 - Page Not Found", Navigation: s.contentMgr.GetNavigation(), Theme: theme, CurrentYear: time.Now().Year(), } html, err := s.templateMgr.Render("404", data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusNotFound) w.Write([]byte(html)) } func (s *Server) getTheme(r *http.Request) string { cookie, err := r.Cookie("theme") if err != nil { return s.config.Theme } return cookie.Value } func (s *Server) startContentUpdater() { s.updateTicker = time.NewTicker(s.config.UpdateInterval) go func() { for range s.updateTicker.C { lastCommit, err := s.githubClient.GetLastCommit() if err != nil { log.Printf("Failed to check for updates: %v", err) continue } if lastCommit.Commit.Committer.Date.After(s.contentMgr.GetLastUpdate()) { log.Println("Content update detected, reloading...") if err := s.contentMgr.LoadContent(); err != nil { log.Printf("Failed to reload content: %v", err) continue } s.cacheMgr.Clear() } } }() }