Skip to content

Commit ab747d9

Browse files
authored
feat(config): Add PWA manifest.json endpoint for web app installation (#990)
* feat(config): Add PWA manifest.json endpoint for web app installation * fix: Update comment to English in manifest handler * fix: fix EOL * fix: Remove unused fmt import from manifest handler * feat: use site settings for manifest name and icon * fix(manifest): Move manifest.json route to static handler for proper CDN handling * feat: move manifest.json handler to static package and improve path handling * feat: Add custom static file handler to prevent manifest.json conflicts * fix: Integrate manifest.json handling into static file serving routes * fix: Simplify PWA manifest scope handling and static file serving - Remove CDN-specific logic for PWA manifest scope and start_url - Always use base path for PWA scope regardless of CDN configuration - Replace manual file serving logic with http.FileServer for static assets * fix: Ensure consistent base path handling in site configuration and manifest path construction * fix: Refactor trailing slash handling in site configuration * feat(static): update manifest path handling and add route for manifest.json
1 parent 93c0621 commit ab747d9

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed

server/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func Init(e *gin.Engine) {
3232
})
3333
g.GET("/favicon.ico", handles.Favicon)
3434
g.GET("/robots.txt", handles.Robots)
35+
g.GET("/manifest.json", static.ManifestJSON)
3536
g.GET("/i/:link_name", handles.Plist)
3637
common.SecretKey = []byte(conf.Conf.JwtSecret)
3738
g.Use(middlewares.StoragesLoaded)

server/static/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ func getSiteConfig() SiteConfig {
1919
}
2020
if siteConfig.BasePath != "" {
2121
siteConfig.BasePath = utils.FixAndCleanPath(siteConfig.BasePath)
22+
// Keep consistent with frontend: trim trailing slash unless it's root
23+
if siteConfig.BasePath != "/" && strings.HasSuffix(siteConfig.BasePath, "/") {
24+
siteConfig.BasePath = strings.TrimSuffix(siteConfig.BasePath, "/")
25+
}
26+
}
27+
if siteConfig.BasePath == "" {
28+
siteConfig.BasePath = "/"
2229
}
2330
if siteConfig.Cdn == "" {
2431
siteConfig.Cdn = strings.TrimSuffix(siteConfig.BasePath, "/")

server/static/static.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package static
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"io"
@@ -17,6 +18,20 @@ import (
1718
"github.com/gin-gonic/gin"
1819
)
1920

21+
type ManifestIcon struct {
22+
Src string `json:"src"`
23+
Sizes string `json:"sizes"`
24+
Type string `json:"type"`
25+
}
26+
27+
type Manifest struct {
28+
Display string `json:"display"`
29+
Scope string `json:"scope"`
30+
StartURL string `json:"start_url"`
31+
Name string `json:"name"`
32+
Icons []ManifestIcon `json:"icons"`
33+
}
34+
2035
var static fs.FS
2136

2237
func initStatic() {
@@ -77,9 +92,15 @@ func initIndex(siteConfig SiteConfig) {
7792
utils.Log.Debug("Successfully read index.html from static files system")
7893
}
7994
utils.Log.Debug("Replacing placeholders in index.html...")
95+
// Construct the correct manifest path based on basePath
96+
manifestPath := "/manifest.json"
97+
if siteConfig.BasePath != "/" {
98+
manifestPath = siteConfig.BasePath + "/manifest.json"
99+
}
80100
replaceMap := map[string]string{
81-
"cdn: undefined": fmt.Sprintf("cdn: '%s'", siteConfig.Cdn),
82-
"base_path: undefined": fmt.Sprintf("base_path: '%s'", siteConfig.BasePath),
101+
"cdn: undefined": fmt.Sprintf("cdn: '%s'", siteConfig.Cdn),
102+
"base_path: undefined": fmt.Sprintf("base_path: '%s'", siteConfig.BasePath),
103+
`href="/manifest.json"`: fmt.Sprintf(`href="%s"`, manifestPath),
83104
}
84105
conf.RawIndexHtml = replaceStrings(conf.RawIndexHtml, replaceMap)
85106
UpdateIndex()
@@ -110,12 +131,57 @@ func UpdateIndex() {
110131
utils.Log.Debug("Index.html update completed")
111132
}
112133

134+
func ManifestJSON(c *gin.Context) {
135+
// Get site configuration to ensure consistent base path handling
136+
siteConfig := getSiteConfig()
137+
138+
// Get site title from settings
139+
siteTitle := setting.GetStr(conf.SiteTitle)
140+
141+
// Get logo from settings, use the first line (light theme logo)
142+
logoSetting := setting.GetStr(conf.Logo)
143+
logoUrl := strings.Split(logoSetting, "\n")[0]
144+
145+
// Use base path from site config for consistency
146+
basePath := siteConfig.BasePath
147+
148+
// Determine scope and start_url
149+
// PWA scope and start_url should always point to our application's base path
150+
// regardless of whether static resources come from CDN or local server
151+
scope := basePath
152+
startURL := basePath
153+
154+
manifest := Manifest{
155+
Display: "standalone",
156+
Scope: scope,
157+
StartURL: startURL,
158+
Name: siteTitle,
159+
Icons: []ManifestIcon{
160+
{
161+
Src: logoUrl,
162+
Sizes: "512x512",
163+
Type: "image/png",
164+
},
165+
},
166+
}
167+
168+
c.Header("Content-Type", "application/json")
169+
c.Header("Cache-Control", "public, max-age=3600") // cache for 1 hour
170+
171+
if err := json.NewEncoder(c.Writer).Encode(manifest); err != nil {
172+
utils.Log.Errorf("Failed to encode manifest.json: %v", err)
173+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate manifest"})
174+
return
175+
}
176+
}
177+
113178
func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
114179
utils.Log.Debug("Setting up static routes...")
115180
siteConfig := getSiteConfig()
116181
initStatic()
117182
initIndex(siteConfig)
118183
folders := []string{"assets", "images", "streamer", "static"}
184+
119185
if conf.Conf.Cdn == "" {
120186
utils.Log.Debug("Setting up static file serving...")
121187
r.Use(func(c *gin.Context) {

0 commit comments

Comments
 (0)