projects.go (3741B)
1 package handlers 2 3 import ( 4 "errors" 5 "html/template" 6 "mime" 7 "net/http" 8 "path/filepath" 9 "strings" 10 11 "git.prime8.dev/moncheeta/prime8dev/internal/config" 12 "git.prime8.dev/moncheeta/prime8dev/internal/content" 13 "git.prime8.dev/moncheeta/prime8dev/internal/external" 14 "git.prime8.dev/moncheeta/prime8dev/internal/utils" 15 "git.prime8.dev/moncheeta/prime8dev/internal/web" 16 "github.com/go-chi/chi/v5" 17 ) 18 19 func (h *Handler) ProjectsPageHandler(w http.ResponseWriter, r *http.Request) { 20 id := strings.TrimSpace(chi.URLParam(r, "id")) 21 about := config.GetAbout() 22 projects, err := content.LoadProjects(h.gitService) 23 if err != nil { 24 http.Error(w, "Failed to load projects", http.StatusInternalServerError) 25 return 26 } 27 28 data := web.BaseData("Projects", "projects", about) 29 data.Projects = projects 30 31 if id == "" { 32 if len(projects) > 0 { 33 applyProjectDetailData(&data, &projects[0]) 34 } 35 36 if utils.IsHTMXRequest(r) { 37 h.renderTemplate(w, r, "projects", data, detailMaxAge(data, fragmentCacheMaxAgeSeconds)) 38 return 39 } 40 41 h.renderTemplate(w, r, "layout", data, detailMaxAge(data, fullPageCacheMaxAgeSeconds)) 42 return 43 } 44 45 project, err := content.GetProjectByID(h.gitService, id) 46 if err != nil { 47 http.Error(w, "Failed to load project", http.StatusInternalServerError) 48 return 49 } 50 if project == nil { 51 http.NotFound(w, r) 52 return 53 } 54 55 data.SelectedProjectID = id 56 applyProjectDetailData(&data, project) 57 58 h.renderTemplate(w, r, "layout", data, detailMaxAge(data, fullPageCacheMaxAgeSeconds)) 59 } 60 61 // detailMaxAge never lets a browser cache a page whose README failed to 62 // load, so a transient fetch error isn't pinned for an hour. 63 func detailMaxAge(data web.Data, maxAge int) int { 64 if data.Project != nil && strings.TrimSpace(string(data.ProjectReadme)) == "" { 65 return 0 66 } 67 return maxAge 68 } 69 70 func applyProjectDetailData(data *web.Data, project *config.Project) { 71 if data == nil || project == nil { 72 return 73 } 74 75 data.Project = project 76 data.ProjectReadme = template.HTML(project.HTML) 77 } 78 79 func (h *Handler) ProjectDetailHandler(w http.ResponseWriter, r *http.Request) { 80 id := chi.URLParam(r, "id") 81 project, err := content.GetProjectByID(h.gitService, id) 82 if err != nil { 83 http.Error(w, "Failed to load project", http.StatusInternalServerError) 84 return 85 } 86 if project == nil { 87 http.Error(w, "Project not found", http.StatusNotFound) 88 return 89 } 90 91 data := web.Data{ 92 Project: project, 93 ProjectReadme: template.HTML(project.HTML), 94 } 95 96 h.renderTemplate(w, r, "project_detail", data, detailMaxAge(data, fragmentCacheMaxAgeSeconds)) 97 } 98 99 // ProjectRawHandler serves a file from HEAD of a public project's repo, so 100 // README images and links resolve without a separate git web frontend. 101 func (h *Handler) ProjectRawHandler(w http.ResponseWriter, r *http.Request) { 102 project := config.GetProjectByID(chi.URLParam(r, "id")) 103 if project == nil { 104 http.NotFound(w, r) 105 return 106 } 107 108 path := chi.URLParam(r, "*") 109 body, err := h.gitService.ReadFile(r.Context(), *project, path) 110 if errors.Is(err, external.ErrNotFound) { 111 http.NotFound(w, r) 112 return 113 } 114 if err != nil { 115 http.Error(w, "Failed to read file", http.StatusInternalServerError) 116 return 117 } 118 119 contentType := mime.TypeByExtension(filepath.Ext(path)) 120 if contentType == "" || strings.HasPrefix(contentType, "text/") { 121 contentType = http.DetectContentType(body) 122 if strings.HasPrefix(contentType, "text/") { 123 contentType = "text/plain; charset=utf-8" 124 } 125 } 126 127 w.Header().Set("Content-Type", contentType) 128 w.Header().Set("X-Content-Type-Options", "nosniff") 129 w.Header().Set("Content-Security-Policy", "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox") 130 w.Header().Set("Cache-Control", "public, max-age=300") 131 _, _ = w.Write(body) 132 }