| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package handlers |
| 4 | |
| 5 | import ( |
| 6 | "html/template" |
| 7 | "log/slog" |
| 8 | "net/http" |
| 9 | |
| 10 | "github.com/tenseleyFlow/shithub/internal/version" |
| 11 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 12 | "github.com/tenseleyFlow/shithub/internal/web/render" |
| 13 | ) |
| 14 | |
| 15 | type helloHandler struct { |
| 16 | render *render.Renderer |
| 17 | logoSVG string |
| 18 | baseURL string |
| 19 | logger *slog.Logger |
| 20 | } |
| 21 | |
| 22 | type helloData struct { |
| 23 | Title string |
| 24 | Version string |
| 25 | Commit string |
| 26 | BuiltAt string |
| 27 | LogoSVG template.HTML |
| 28 | // Viewer + CSRFToken mirror the fields _nav.html branches on. Typed |
| 29 | // page-data structs must populate them explicitly - the renderer |
| 30 | // only auto-injects for map[string]any data. |
| 31 | Viewer middleware.CurrentUser |
| 32 | CSRFToken string |
| 33 | // SEO/social fields are read by optional layout helpers, so typed |
| 34 | // page-data structs may provide only the fields they actually need. |
| 35 | MetaDescription string |
| 36 | CanonicalURL string |
| 37 | OGTitle string |
| 38 | OGDescription string |
| 39 | OGImage string |
| 40 | OGType string |
| 41 | StructuredData template.JS |
| 42 | // GlobalSearchQuery is referenced by _nav.html's search input to |
| 43 | // preserve the query when re-rendering after a search. Hello has |
| 44 | // no query of its own, but the field must exist or template |
| 45 | // execution errors out. |
| 46 | GlobalSearchQuery string |
| 47 | // Repo/Org are optional nav contexts. They are nil on the home page, |
| 48 | // but typed data must still expose them because _nav.html probes |
| 49 | // these fields before deciding whether to render context tabs. |
| 50 | Repo any |
| 51 | Org any |
| 52 | } |
| 53 | |
| 54 | func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 55 | viewer := middleware.CurrentUserFromContext(r.Context()) |
| 56 | data := helloData{ |
| 57 | Title: "Welcome", |
| 58 | Version: version.Version, |
| 59 | Commit: version.Commit, |
| 60 | BuiltAt: version.BuiltAt, |
| 61 | LogoSVG: template.HTML(h.logoSVG), // #nosec G203 - embedded server-owned asset |
| 62 | Viewer: viewer, |
| 63 | CSRFToken: middleware.CSRFTokenForRequest(r), |
| 64 | MetaDescription: defaultMetaDescription, |
| 65 | CanonicalURL: canonicalURL(h.baseURL, r, "/"), |
| 66 | OGTitle: "shithub: GitHub-style git hosting without Copilot", |
| 67 | OGDescription: "A self-hostable, AGPL GitHub alternative with familiar repositories, pull requests, issues, organizations, code search, and Actions-style CI.", |
| 68 | OGType: "website", |
| 69 | StructuredData: organizationStructuredData(publicBaseURL(h.baseURL, r)), |
| 70 | } |
| 71 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 72 | if err := h.render.RenderPage(w, r, "hello", data); err != nil { |
| 73 | h.logger.Error("render hello", "error", err) |
| 74 | http.Error(w, "internal server error", http.StatusInternalServerError) |
| 75 | } |
| 76 | } |
| 77 |