Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,12 @@ func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
return nil, fmt.Errorf("verifying storage connectivity: %w", err)
}

// Load templates
templates, err := NewTemplates()
if err != nil {
_ = store.Close()
_ = db.Close()
return nil, fmt.Errorf("loading templates: %w", err)
}

return &Server{
cfg: cfg,
db: db,
storage: store,
logger: logger,
templates: templates,
templates: &Templates{},
}, nil
}

Expand Down
10 changes: 1 addition & 9 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,13 @@ func newTestServer(t *testing.T) *testServer {
r.Mount("/go", http.StripPrefix("/go", goHandler.Routes()))
r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes()))

// Load templates
templates, err := NewTemplates()
if err != nil {
_ = db.Close()
_ = os.RemoveAll(tempDir)
t.Fatalf("failed to load templates: %v", err)
}

// Create a minimal server struct for the handlers
s := &Server{
cfg: cfg,
db: db,
storage: store,
logger: logger,
templates: templates,
templates: &Templates{},
}

r.Get("/health", s.handleHealth)
Expand Down
79 changes: 44 additions & 35 deletions internal/server/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,70 @@ import (
"html/template"
"net/http"
"path/filepath"
"sync"
)

//go:embed templates/**/*.html
var templatesFS embed.FS

// Templates holds parsed templates for each page.
// Templates holds lazily-parsed templates for each page.
type Templates struct {
once sync.Once
pages map[string]*template.Template
err error
}

// NewTemplates loads and parses all templates from the embedded filesystem.
func NewTemplates() (*Templates, error) {
pages := make(map[string]*template.Template)
// load parses all templates from the embedded filesystem on first call.
func (t *Templates) load() error {
t.once.Do(func() {
pages := make(map[string]*template.Template)

// Define custom template functions
funcMap := template.FuncMap{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"supportedEcosystems": supportedEcosystems,
"ecosystemBadgeClass": ecosystemBadgeClasses,
"ecosystemBadgeLabel": ecosystemBadgeLabel,
}

// Get all page files
pageFiles, err := templatesFS.ReadDir("templates/pages")
if err != nil {
return nil, err
}

for _, pageFile := range pageFiles {
if pageFile.IsDir() {
continue
funcMap := template.FuncMap{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"supportedEcosystems": supportedEcosystems,
"ecosystemBadgeClass": ecosystemBadgeClasses,
"ecosystemBadgeLabel": ecosystemBadgeLabel,
}

pageName := pageFile.Name()
pageName = pageName[:len(pageName)-len(filepath.Ext(pageName))]

// Parse all layout files + components + this page with custom functions
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS,
"templates/layout/*.html",
"templates/components/*.html",
"templates/pages/"+pageFile.Name(),
)
pageFiles, err := templatesFS.ReadDir("templates/pages")
if err != nil {
return nil, err
t.err = err
return
}

pages[pageName] = tmpl
}
for _, pageFile := range pageFiles {
if pageFile.IsDir() {
continue
}

return &Templates{pages: pages}, nil
pageName := pageFile.Name()
pageName = pageName[:len(pageName)-len(filepath.Ext(pageName))]

tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS,
"templates/layout/*.html",
"templates/components/*.html",
"templates/pages/"+pageFile.Name(),
)
if err != nil {
t.err = err
return
}

pages[pageName] = tmpl
}

t.pages = pages
})
return t.err
}

// Render renders a page template with the given data.
func (t *Templates) Render(w http.ResponseWriter, pageName string, data any) error {
if err := t.load(); err != nil {
return err
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")

tmpl, ok := t.pages[pageName]
Expand Down
54 changes: 45 additions & 9 deletions internal/server/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import (
)

func TestTemplatesRenderAllPages(t *testing.T) {
templates, err := NewTemplates()
if err != nil {
t.Fatalf("failed to load templates: %v", err)
}
templates := &Templates{}

tests := []struct {
page string
Expand Down Expand Up @@ -156,14 +153,26 @@ func TestTemplatesRenderAllPages(t *testing.T) {
}
}

func TestTemplatesRenderUnknownPage(t *testing.T) {
templates, err := NewTemplates()
if err != nil {
t.Fatalf("failed to load templates: %v", err)
func TestTemplatesLazyLoading(t *testing.T) {
templates := &Templates{}

if templates.pages != nil {
t.Fatal("expected pages to be nil before first Render call")
}

w := httptest.NewRecorder()
err = templates.Render(w, "nonexistent_page", nil)
_ = templates.Render(w, "dashboard", DashboardData{})

if templates.pages == nil {
t.Fatal("expected pages to be populated after first Render call")
}
}

func TestTemplatesRenderUnknownPage(t *testing.T) {
templates := &Templates{}

w := httptest.NewRecorder()
err := templates.Render(w, "nonexistent_page", nil)
if err == nil {
t.Error("expected error for unknown page")
}
Expand Down Expand Up @@ -424,3 +433,30 @@ func TestCategorizeLicense(t *testing.T) {
}
}
}

func BenchmarkTemplatesParse(b *testing.B) {
for b.Loop() {
t := &Templates{}
if err := t.load(); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkServerCreate(b *testing.B) {
for b.Loop() {
_ = &Server{
templates: &Templates{},
}
}
}

func BenchmarkFirstRender(b *testing.B) {
for b.Loop() {
t := &Templates{}
w := httptest.NewRecorder()
if err := t.Render(w, "dashboard", DashboardData{}); err != nil {
b.Fatal(err)
}
}
}