Installable as an app (PWA): manifests, offline-only service worker, icons

This commit is contained in:
Astra 2026-10-03 12:22:33 +01:00
parent b26360e8f3
commit 44708d5f3c
16 changed files with 246 additions and 0 deletions

71
internal/web/pwa.go Normal file
View file

@ -0,0 +1,71 @@
package web
import (
"encoding/json"
"io/fs"
"net/http"
)
// The site installs as an app (a PWA): a web app manifest, plus the service worker in
// assets/sw.js. The public site and the staff panel are separate apps, so each opens on its own
// page; both cover the whole site, since signing in goes through /auth.
type manifest struct {
ID string `json:"id"`
Name string `json:"name"`
ShortName string `json:"short_name"`
Description string `json:"description"`
StartURL string `json:"start_url"`
Scope string `json:"scope"`
Display string `json:"display"`
BackgroundColor string `json:"background_color"`
ThemeColor string `json:"theme_color"`
Icons []manifestIcon `json:"icons"`
}
type manifestIcon struct {
Src string `json:"src"`
Sizes string `json:"sizes"`
Type string `json:"type"`
Purpose string `json:"purpose,omitempty"`
}
func (s *Server) manifest(staff bool) http.HandlerFunc {
m := manifest{
ID: "/",
Name: s.site.Title,
ShortName: s.site.Title,
Description: "Live server, bans and staff.",
StartURL: "/",
Scope: "/",
Display: "standalone",
BackgroundColor: "#14161b",
ThemeColor: "#1b1e25",
Icons: []manifestIcon{
{Src: "/static/icon-192.png", Sizes: "192x192", Type: "image/png"},
{Src: "/static/icon-512.png", Sizes: "512x512", Type: "image/png"},
{Src: "/static/icon-maskable-512.png", Sizes: "512x512", Type: "image/png", Purpose: "maskable"},
},
}
if staff {
m.ID = "/admin"
m.Name = s.site.Title + " staff panel"
m.ShortName = s.site.Title + " staff"
m.Description = "Bans, mutes, ranks and the live server, for staff."
m.StartURL = "/admin"
}
body, _ := json.Marshal(m)
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
w.Header().Set("Cache-Control", "no-cache")
w.Write(body)
}
}
// serviceWorker serves sw.js from the root, since a worker only controls pages under its own path.
func serviceWorker(static fs.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
http.ServeFileFS(w, r, static, "sw.js")
}
}