From 4e4c3cfe64218a7efc58caa299fafaf337719d99 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:09:33 +0100 Subject: [PATCH] Update dynamic manifest implementation --- docs/configuration.md | 14 +++--- internal/glance/config.go | 25 +++++------ internal/glance/glance.go | 58 +++++++++++++------------ internal/glance/templates/document.html | 8 ++-- internal/glance/templates/manifest.json | 4 +- internal/glance/templates/page.html | 2 +- 6 files changed, 57 insertions(+), 54 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8ebcb0e..977a890 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -270,7 +270,7 @@ branding: favicon-url: /assets/logo.png app-name: "My Dashboard" app-icon-url: "/assets/app-icon.png" - app-bg-color: "#151519" + app-background-color: "#151519" ``` ### Properties @@ -283,8 +283,8 @@ branding: | logo-url | string | no | | | favicon-url | string | no | | | app-name | string | no | Glance | -| app-icon-url | string | no | | -| app-bg-color | string | no | #151519 | +| app-icon-url | string | no | Glance's default icon | +| app-background-color | string | no | Glance's default background color | #### `hide-footer` Hides the footer when set to `true`. @@ -302,13 +302,13 @@ Specify a URL to a custom image to use instead of the "G" found in the navigatio Specify a URL to a custom image to use for the favicon. #### `app-name` -Specify the name of the web app shown in browser tab and PWA. Defaults to "Glance". +Specify the name of the web app shown in browser tab and PWA. #### `app-icon-url` -Specify URL for PWA and browser tab icon (512x512 PNG). Defaults to Glance icon if not set. +Specify URL for PWA and browser tab icon (512x512 PNG). -#### `app-bg-color` -Specify background color for PWA. Must be a valid CSS color. Defaults to "#151519". +#### `app-background-color` +Specify background color for PWA. Must be a valid CSS color. ## Theme Theming is done through a top level `theme` property. Values for the colors are in [HSL](https://giggster.com/guide/basics/hue-saturation-lightness/) (hue, saturation, lightness) format. You can use a color picker [like this one](https://hslpicker.com/) to convert colors from other formats to HSL. The values are separated by a space and `%` is not required for any of the numbers. diff --git a/internal/glance/config.go b/internal/glance/config.go index 4b9060c..2053ff9 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -27,11 +27,10 @@ const ( type config struct { Server struct { - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - AssetsPath string `yaml:"assets-path"` - BaseURL string `yaml:"base-url"` - StartedAt time.Time `yaml:"-"` // used in custom css file + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + AssetsPath string `yaml:"assets-path"` + BaseURL string `yaml:"base-url"` } `yaml:"server"` Document struct { @@ -50,14 +49,14 @@ type config struct { } `yaml:"theme"` Branding struct { - HideFooter bool `yaml:"hide-footer"` - CustomFooter template.HTML `yaml:"custom-footer"` - LogoText string `yaml:"logo-text"` - LogoURL string `yaml:"logo-url"` - FaviconURL string `yaml:"favicon-url"` - AppName string `yaml:"app-name"` - AppIconURL string `yaml:"app-icon-url"` - AppBgColor string `yaml:"app-bg-color"` + HideFooter bool `yaml:"hide-footer"` + CustomFooter template.HTML `yaml:"custom-footer"` + LogoText string `yaml:"logo-text"` + LogoURL string `yaml:"logo-url"` + FaviconURL string `yaml:"favicon-url"` + AppName string `yaml:"app-name"` + AppIconURL string `yaml:"app-icon-url"` + AppBackgroundColor string `yaml:"app-background-color"` } `yaml:"branding"` Pages []page `yaml:"pages"` diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 7952de1..e000f06 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -18,15 +18,19 @@ var ( pageTemplate = mustParseTemplate("page.html", "document.html") pageContentTemplate = mustParseTemplate("page-content.html") pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl") + manifestTemplate = mustParseTemplate("manifest.json") ) const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour type application struct { Version string + CreatedAt time.Time Config config ParsedThemeStyle template.HTML + parsedManifest []byte + slugToPage map[string]*page widgetByID map[uint64]widget } @@ -34,6 +38,7 @@ type application struct { func newApplication(config *config) (*application, error) { app := &application{ Version: buildVersion, + CreatedAt: time.Now(), Config: *config, slugToPage: make(map[string]*page), widgetByID: make(map[uint64]widget), @@ -42,7 +47,7 @@ func newApplication(config *config) (*application, error) { app.slugToPage[""] = &config.Pages[0] providers := &widgetProviders{ - assetResolver: app.AssetPath, + assetResolver: app.StaticAssetPath, } var err error @@ -89,9 +94,10 @@ func newApplication(config *config) (*application, error) { config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/") config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile) + config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) if config.Branding.FaviconURL == "" { - config.Branding.FaviconURL = app.AssetPath("favicon.png") + config.Branding.FaviconURL = app.StaticAssetPath("favicon.png") } else { config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) } @@ -101,14 +107,22 @@ func newApplication(config *config) (*application, error) { } if config.Branding.AppIconURL == "" { - config.Branding.AppIconURL = app.AssetPath("app-icon.png") + config.Branding.AppIconURL = app.StaticAssetPath("app-icon.png") } - if config.Branding.AppBgColor == "" { - config.Branding.AppBgColor = "#151519" + if config.Branding.AppBackgroundColor == "" { + config.Branding.AppBackgroundColor = ternary( + config.Theme.BackgroundColor != nil, + config.Theme.BackgroundColor.String(), + "hsl(240, 8%, 9%)", + ) } - config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) + var manifestWriter bytes.Buffer + if err := manifestTemplate.Execute(&manifestWriter, pageTemplateData{App: app}); err != nil { + return nil, fmt.Errorf("parsing manifest.json: %v", err) + } + app.parsedManifest = manifestWriter.Bytes() return app, nil } @@ -232,10 +246,15 @@ func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request widget.handleRequest(w, r) } -func (a *application) AssetPath(asset string) string { +func (a *application) StaticAssetPath(asset string) string { return a.Config.Server.BaseURL + "/static/" + staticFSHash + "/" + asset } +func (a *application) VersionedAssetPath(asset string) string { + return a.Config.Server.BaseURL + asset + + "?v=" + strconv.FormatInt(a.CreatedAt.Unix(), 10) +} + func (a *application) server() (func() error, func() error) { // TODO: add gzip support, static files must have their gzipped contents cached // TODO: add HTTPS support @@ -258,35 +277,21 @@ func (a *application) server() (func() error, func() error) { ), ) - cssBundleCacheControlValue := fmt.Sprintf( + assetCacheControlValue := fmt.Sprintf( "public, max-age=%d", int(STATIC_ASSETS_CACHE_DURATION.Seconds()), ) mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", cssBundleCacheControlValue) + w.Header().Add("Cache-Control", assetCacheControlValue) w.Header().Add("Content-Type", "text/css; charset=utf-8") w.Write(bundledCSSContents) }) - mux.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", staticFSHash), func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", cssBundleCacheControlValue) + mux.HandleFunc("GET /manifest.json", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", assetCacheControlValue) w.Header().Add("Content-Type", "application/json") - - template, err := template.New("manifest.json"). - Funcs(globalTemplateFunctions). - ParseFS(templateFS, "manifest.json") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Error parsing manifest.json template: %v", err))) - return - } - - if err := template.Execute(w, pageTemplateData{App: a}); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Error executing manifest.json template: %v", err))) - return - } + w.Write(a.parsedManifest) }) var absAssetsPath string @@ -302,7 +307,6 @@ func (a *application) server() (func() error, func() error) { } start := func() error { - a.Config.Server.StartedAt = time.Now() log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n", a.Config.Server.Host, a.Config.Server.Port, diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html index a6896e0..107a749 100644 --- a/internal/glance/templates/document.html +++ b/internal/glance/templates/document.html @@ -12,11 +12,11 @@ - - + + - - + + {{ block "document-head-after" . }}{{ end }}
diff --git a/internal/glance/templates/manifest.json b/internal/glance/templates/manifest.json index a7ccce4..eae4d19 100644 --- a/internal/glance/templates/manifest.json +++ b/internal/glance/templates/manifest.json @@ -1,7 +1,7 @@ { "name": "{{ .App.Config.Branding.AppName }}", "display": "standalone", - "background_color": "{{ .App.Config.Branding.AppBgColor }}", + "background_color": "{{ .App.Config.Branding.AppBackgroundColor }}", "scope": "/", "start_url": "/", "icons": [ @@ -11,4 +11,4 @@ "sizes": "512x512" } ] -} \ No newline at end of file +} diff --git a/internal/glance/templates/page.html b/internal/glance/templates/page.html index 8e1ddae..24baf78 100644 --- a/internal/glance/templates/page.html +++ b/internal/glance/templates/page.html @@ -17,7 +17,7 @@ {{ .App.ParsedThemeStyle }} {{ if ne "" .App.Config.Theme.CustomCSSFile }} - + {{ end }} {{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}