Update dynamic manifest implementation

This commit is contained in:
Svilen Markov
2025-04-23 12:09:33 +01:00
parent e2112e0d83
commit 4e4c3cfe64
6 changed files with 57 additions and 54 deletions

View File

@@ -270,7 +270,7 @@ branding:
favicon-url: /assets/logo.png favicon-url: /assets/logo.png
app-name: "My Dashboard" app-name: "My Dashboard"
app-icon-url: "/assets/app-icon.png" app-icon-url: "/assets/app-icon.png"
app-bg-color: "#151519" app-background-color: "#151519"
``` ```
### Properties ### Properties
@@ -283,8 +283,8 @@ branding:
| logo-url | string | no | | | logo-url | string | no | |
| favicon-url | string | no | | | favicon-url | string | no | |
| app-name | string | no | Glance | | app-name | string | no | Glance |
| app-icon-url | string | no | | | app-icon-url | string | no | Glance's default icon |
| app-bg-color | string | no | #151519 | | app-background-color | string | no | Glance's default background color |
#### `hide-footer` #### `hide-footer`
Hides the footer when set to `true`. 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. Specify a URL to a custom image to use for the favicon.
#### `app-name` #### `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` #### `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` #### `app-background-color`
Specify background color for PWA. Must be a valid CSS color. Defaults to "#151519". Specify background color for PWA. Must be a valid CSS color.
## Theme ## 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. 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.

View File

@@ -31,7 +31,6 @@ type config struct {
Port uint16 `yaml:"port"` Port uint16 `yaml:"port"`
AssetsPath string `yaml:"assets-path"` AssetsPath string `yaml:"assets-path"`
BaseURL string `yaml:"base-url"` BaseURL string `yaml:"base-url"`
StartedAt time.Time `yaml:"-"` // used in custom css file
} `yaml:"server"` } `yaml:"server"`
Document struct { Document struct {
@@ -57,7 +56,7 @@ type config struct {
FaviconURL string `yaml:"favicon-url"` FaviconURL string `yaml:"favicon-url"`
AppName string `yaml:"app-name"` AppName string `yaml:"app-name"`
AppIconURL string `yaml:"app-icon-url"` AppIconURL string `yaml:"app-icon-url"`
AppBgColor string `yaml:"app-bg-color"` AppBackgroundColor string `yaml:"app-background-color"`
} `yaml:"branding"` } `yaml:"branding"`
Pages []page `yaml:"pages"` Pages []page `yaml:"pages"`

View File

@@ -18,15 +18,19 @@ var (
pageTemplate = mustParseTemplate("page.html", "document.html") pageTemplate = mustParseTemplate("page.html", "document.html")
pageContentTemplate = mustParseTemplate("page-content.html") pageContentTemplate = mustParseTemplate("page-content.html")
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl") pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
manifestTemplate = mustParseTemplate("manifest.json")
) )
const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour
type application struct { type application struct {
Version string Version string
CreatedAt time.Time
Config config Config config
ParsedThemeStyle template.HTML ParsedThemeStyle template.HTML
parsedManifest []byte
slugToPage map[string]*page slugToPage map[string]*page
widgetByID map[uint64]widget widgetByID map[uint64]widget
} }
@@ -34,6 +38,7 @@ type application struct {
func newApplication(config *config) (*application, error) { func newApplication(config *config) (*application, error) {
app := &application{ app := &application{
Version: buildVersion, Version: buildVersion,
CreatedAt: time.Now(),
Config: *config, Config: *config,
slugToPage: make(map[string]*page), slugToPage: make(map[string]*page),
widgetByID: make(map[uint64]widget), widgetByID: make(map[uint64]widget),
@@ -42,7 +47,7 @@ func newApplication(config *config) (*application, error) {
app.slugToPage[""] = &config.Pages[0] app.slugToPage[""] = &config.Pages[0]
providers := &widgetProviders{ providers := &widgetProviders{
assetResolver: app.AssetPath, assetResolver: app.StaticAssetPath,
} }
var err error var err error
@@ -89,9 +94,10 @@ func newApplication(config *config) (*application, error) {
config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/") config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/")
config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile) config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile)
config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL)
if config.Branding.FaviconURL == "" { if config.Branding.FaviconURL == "" {
config.Branding.FaviconURL = app.AssetPath("favicon.png") config.Branding.FaviconURL = app.StaticAssetPath("favicon.png")
} else { } else {
config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL)
} }
@@ -101,14 +107,22 @@ func newApplication(config *config) (*application, error) {
} }
if config.Branding.AppIconURL == "" { if config.Branding.AppIconURL == "" {
config.Branding.AppIconURL = app.AssetPath("app-icon.png") config.Branding.AppIconURL = app.StaticAssetPath("app-icon.png")
} }
if config.Branding.AppBgColor == "" { if config.Branding.AppBackgroundColor == "" {
config.Branding.AppBgColor = "#151519" 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 return app, nil
} }
@@ -232,10 +246,15 @@ func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request
widget.handleRequest(w, r) 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 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) { func (a *application) server() (func() error, func() error) {
// TODO: add gzip support, static files must have their gzipped contents cached // TODO: add gzip support, static files must have their gzipped contents cached
// TODO: add HTTPS support // 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", "public, max-age=%d",
int(STATIC_ASSETS_CACHE_DURATION.Seconds()), int(STATIC_ASSETS_CACHE_DURATION.Seconds()),
) )
mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) { 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.Header().Add("Content-Type", "text/css; charset=utf-8")
w.Write(bundledCSSContents) w.Write(bundledCSSContents)
}) })
mux.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", staticFSHash), func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("GET /manifest.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", cssBundleCacheControlValue) w.Header().Add("Cache-Control", assetCacheControlValue)
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.Write(a.parsedManifest)
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
}
}) })
var absAssetsPath string var absAssetsPath string
@@ -302,7 +307,6 @@ func (a *application) server() (func() error, func() error) {
} }
start := 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", log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n",
a.Config.Server.Host, a.Config.Server.Host,
a.Config.Server.Port, a.Config.Server.Port,

View File

@@ -12,11 +12,11 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Glance"> <meta name="apple-mobile-web-app-title" content="Glance">
<meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}"> <meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}">
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.AssetPath "app-icon.png" }}'> <link rel="apple-touch-icon" sizes="512x512" href='{{ .App.StaticAssetPath "app-icon.png" }}'>
<link rel="manifest" href='{{ .App.AssetPath "manifest.json" }}'> <link rel="manifest" href='{{ .App.VersionedAssetPath "manifest.json" }}'>
<link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" /> <link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" />
<link rel="stylesheet" href='{{ .App.AssetPath "css/bundle.css" }}'> <link rel="stylesheet" href='{{ .App.StaticAssetPath "css/bundle.css" }}'>
<script type="module" src='{{ .App.AssetPath "js/main.js" }}'></script> <script type="module" src='{{ .App.StaticAssetPath "js/main.js" }}'></script>
{{ block "document-head-after" . }}{{ end }} {{ block "document-head-after" . }}{{ end }}
</head> </head>
<body> <body>

View File

@@ -1,7 +1,7 @@
{ {
"name": "{{ .App.Config.Branding.AppName }}", "name": "{{ .App.Config.Branding.AppName }}",
"display": "standalone", "display": "standalone",
"background_color": "{{ .App.Config.Branding.AppBgColor }}", "background_color": "{{ .App.Config.Branding.AppBackgroundColor }}",
"scope": "/", "scope": "/",
"start_url": "/", "start_url": "/",
"icons": [ "icons": [

View File

@@ -17,7 +17,7 @@
{{ .App.ParsedThemeStyle }} {{ .App.ParsedThemeStyle }}
{{ if ne "" .App.Config.Theme.CustomCSSFile }} {{ if ne "" .App.Config.Theme.CustomCSSFile }}
<link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.Config.Server.StartedAt.Unix }}"> <link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.CreatedAt.Unix }}">
{{ end }} {{ end }}
{{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }} {{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}