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
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.

View File

@@ -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"`

View File

@@ -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,

View File

@@ -12,11 +12,11 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<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 }}">
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.AssetPath "app-icon.png" }}'>
<link rel="manifest" href='{{ .App.AssetPath "manifest.json" }}'>
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.StaticAssetPath "app-icon.png" }}'>
<link rel="manifest" href='{{ .App.VersionedAssetPath "manifest.json" }}'>
<link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" />
<link rel="stylesheet" href='{{ .App.AssetPath "css/bundle.css" }}'>
<script type="module" src='{{ .App.AssetPath "js/main.js" }}'></script>
<link rel="stylesheet" href='{{ .App.StaticAssetPath "css/bundle.css" }}'>
<script type="module" src='{{ .App.StaticAssetPath "js/main.js" }}'></script>
{{ block "document-head-after" . }}{{ end }}
</head>
<body>

View File

@@ -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"
}
]
}
}

View File

@@ -17,7 +17,7 @@
{{ .App.ParsedThemeStyle }}
{{ 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 }}
{{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}