diff --git a/assets/layout.html b/assets/layout.html index c586496..7b5470f 100644 --- a/assets/layout.html +++ b/assets/layout.html @@ -31,6 +31,9 @@
{{block "content" .}}{{end}}
+ {{block "extras" .}}{{end}} diff --git a/assets/style.css b/assets/style.css index 1a4c66a..20cc7bd 100644 --- a/assets/style.css +++ b/assets/style.css @@ -37,6 +37,8 @@ body { font: 1rem "Iosevka Etoile", monospace; + display: flex; + flex-direction: column; } * { @@ -148,6 +150,8 @@ main { max-width: 860px; margin: 0 auto; padding: 1.5rem 1rem; + width: 100%; + flex: 1; } /* === Markdown content === */ @@ -439,6 +443,16 @@ textarea { font-size: 0.85rem; } +/* === Page footer === */ +footer { + padding: 0.75rem 1rem; + border-top: 1px dashed var(--secondary); + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + /* === Task lists === */ .content li:has(> input.task-checkbox:checked) { color: var(--text-muted); @@ -822,7 +836,8 @@ hr { } @media (max-width: 600px) { - header { + header, + footer { padding: 0.5rem 0.75rem; } main { diff --git a/main.go b/main.go index a21b528..cee6abc 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "embed" "flag" "html/template" @@ -12,6 +13,7 @@ import ( "path/filepath" "strconv" "strings" + "time" ) //go:embed assets @@ -94,7 +96,23 @@ type handler struct { authKey []byte } +// reqStartKey marks the request start time stored in the request context +// so HTML templates can render total server-side processing time. +type reqStartKeyT struct{} + +var reqStartKey = reqStartKeyT{} + +// elapsedMS returns the milliseconds since the request entered ServeHTTP. +func elapsedMS(r *http.Request) int64 { + if start, ok := r.Context().Value(reqStartKey).(time.Time); ok { + return time.Since(start).Milliseconds() + } + return 0 +} + func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(context.WithValue(r.Context(), reqStartKey, time.Now())) + if !h.checkAuth(w, r) { return } @@ -234,6 +252,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa if editMode { t = editTmpl } + data.RenderMS = elapsedMS(r) if err := t.ExecuteTemplate(w, "layout", data); err != nil { log.Printf("template error: %v", err) } diff --git a/render.go b/render.go index eba39ed..db0be50 100644 --- a/render.go +++ b/render.go @@ -47,6 +47,7 @@ type pageData struct { Entries []entry SpecialContent template.HTML SidebarWidget template.HTML + RenderMS int64 } // pageSettings holds the parsed contents of a .page-settings file. diff --git a/search.go b/search.go index 7fb74b1..3e136fe 100644 --- a/search.go +++ b/search.go @@ -23,6 +23,7 @@ type searchPageData struct { EditMode bool Query string Results []searchResult + RenderMS int64 } // handleSearch walks the wiki root and renders a search results page for the @@ -43,6 +44,7 @@ func (h *handler) handleSearch(w http.ResponseWriter, r *http.Request) { Results: results, } w.Header().Set("Content-Type", "text/html; charset=utf-8") + data.RenderMS = elapsedMS(r) if err := searchTmpl.ExecuteTemplate(w, "layout", data); err != nil { log.Printf("search template error: %v", err) }