Add lightweight self-hosted Hugo CMS with GitHub API backend

Implements a Go-based CMS for editing Hugo posts and uploading images
via the GitHub REST API, with no database and Git as the source of truth.

Features:
- Single admin auth with bcrypt password and signed session cookies
- Dashboard, post listing with search, create/edit with EasyMDE editor
- Media library for static/uploads/ with drag-drop and clipboard paste
- Client-side autosave, unsaved changes warning, dark mode, responsive UI
- Modular packages for auth, github, posts, media, session, and handlers

Co-authored-by: codegirl007 <s.raide@gmail.com>
This commit is contained in:
Cursor Agent
2026-07-06 18:56:17 +00:00
co-authored by codegirl007
parent 68dfdfa33b
commit ce15f54a52
28 changed files with 2945 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
package templates
import (
"embed"
"html/template"
"io"
"io/fs"
"net/http"
"time"
)
//go:embed all:templates
var templateFS embed.FS
//go:embed all:static
var staticFS embed.FS
// Renderer renders HTML templates with shared layout and functions.
type Renderer struct {
templates *template.Template
}
// New creates a template renderer.
func New() (*Renderer, error) {
funcs := template.FuncMap{
"formatDate": formatDate,
"draftBadge": draftBadgeClass,
}
tmpl, err := template.New("").Funcs(funcs).ParseFS(templateFS, "templates/*.html")
if err != nil {
return nil, err
}
return &Renderer{templates: tmpl}, nil
}
// Render executes a named template with data.
func (r *Renderer) Render(w io.Writer, name string, data any) error {
return r.templates.ExecuteTemplate(w, name, data)
}
// StaticHandler serves embedded static assets.
func StaticHandler() http.Handler {
sub, err := fs.Sub(staticFS, "static")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(sub))
}
func formatDate(t any) string {
switch v := t.(type) {
case time.Time:
if v.IsZero() {
return "—"
}
return v.Format("Jan 2, 2006")
case string:
return v
default:
return "—"
}
}
func draftBadgeClass(draft bool) string {
if draft {
return "badge-draft"
}
return "badge-published"
}