Sfoglia il codice sorgente

Fix PWA for non-root web roots

nimbleghost 2 anni fa
parent
commit
d7aacb8b24
5 ha cambiato i file con 76 aggiunte e 10 eliminazioni
  1. 24 5
      server/server.go
  2. 6 0
      server/server_test.go
  3. 19 0
      server/types.go
  4. 22 3
      web/public/sw.js
  5. 5 2
      web/vite.config.js

+ 24 - 5
server/server.go

@@ -79,6 +79,7 @@ var (
 
 	webConfigPath                                        = "/config.js"
 	webManifestPath                                      = "/manifest.webmanifest"
+	webRootHTMLPath                                      = "/app.html"
 	webServiceWorkerPath                                 = "/sw.js"
 	accountPath                                          = "/account"
 	matrixPushPath                                       = "/_matrix/push/v1/notify"
@@ -434,8 +435,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
 		return s.ensureWebEnabled(s.handleWebManifest)(w, r, v)
-	} else if r.Method == http.MethodGet && r.URL.Path == webServiceWorkerPath {
-		return s.ensureWebEnabled(s.handleStatic)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
 		return s.ensureAdmin(s.handleUsersGet)(w, r, v)
 	} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
@@ -502,7 +501,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.handleMatrixDiscovery(w)
 	} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
 		return s.handleMetrics(w, r, v)
-	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
+	} else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) {
 		return s.ensureWebEnabled(s.handleStatic)(w, r, v)
 	} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
 		return s.ensureWebEnabled(s.handleDocs)(w, r, v)
@@ -590,9 +589,29 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
 	return err
 }
 
-func (s *Server) handleWebManifest(w http.ResponseWriter, r *http.Request, v *visitor) error {
+func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
+	response := &webManifestResponse{
+		Name:            "ntfy web",
+		Description:     "ntfy lets you send push notifications via scripts from any computer or phone. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy.",
+		ShortName:       "ntfy",
+		Scope:           "/",
+		StartURL:        s.config.WebRoot,
+		Display:         "standalone",
+		BackgroundColor: "#ffffff",
+		ThemeColor:      "#317f6f",
+		Icons: []webManifestIcon{
+			{SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"},
+			{SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"},
+		},
+	}
+
+	err := s.writeJSON(w, response)
+	if err != nil {
+		return err
+	}
+
 	w.Header().Set("Content-Type", "application/manifest+json")
-	return s.handleStatic(w, r, v)
+	return nil
 }
 
 // handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set,

+ 6 - 0
server/server_test.go

@@ -245,6 +245,9 @@ func TestServer_WebEnabled(t *testing.T) {
 	rr = request(t, s, "GET", "/sw.js", "", nil)
 	require.Equal(t, 404, rr.Code)
 
+	rr = request(t, s, "GET", "/app.html", "", nil)
+	require.Equal(t, 404, rr.Code)
+
 	rr = request(t, s, "GET", "/static/css/home.css", "", nil)
 	require.Equal(t, 404, rr.Code)
 
@@ -264,6 +267,9 @@ func TestServer_WebEnabled(t *testing.T) {
 
 	rr = request(t, s2, "GET", "/sw.js", "", nil)
 	require.Equal(t, 200, rr.Code)
+
+	rr = request(t, s2, "GET", "/app.html", "", nil)
+	require.Equal(t, 200, rr.Code)
 }
 
 func TestServer_PublishLargeMessage(t *testing.T) {

+ 19 - 0
server/types.go

@@ -518,3 +518,22 @@ func (w *webPushSubscription) Context() log.Context {
 		"web_push_subscription_endpoint": w.Endpoint,
 	}
 }
+
+// https://developer.mozilla.org/en-US/docs/Web/Manifest
+type webManifestResponse struct {
+	Name            string            `json:"name"`
+	Description     string            `json:"description"`
+	ShortName       string            `json:"short_name"`
+	Scope           string            `json:"scope"`
+	StartURL        string            `json:"start_url"`
+	Display         string            `json:"display"`
+	BackgroundColor string            `json:"background_color"`
+	ThemeColor      string            `json:"theme_color"`
+	Icons           []webManifestIcon `json:"icons"`
+}
+
+type webManifestIcon struct {
+	SRC   string `json:"src"`
+	Sizes string `json:"sizes"`
+	Type  string `json:"type"`
+}

+ 22 - 3
web/public/sw.js

@@ -227,9 +227,28 @@ precacheAndRoute(
 // Delete any cached old dist files from previous service worker versions
 cleanupOutdatedCaches();
 
-if (import.meta.env.MODE !== "development") {
-  // since the manifest only includes `/index.html`, this manually adds the root route `/`
-  registerRoute(new NavigationRoute(createHandlerBoundToURL("/")));
+if (!import.meta.env.DEV) {
+  // we need the app_root setting, so we import the config.js file from the go server
+  // this does NOT include the same base_url as the web app running in a window,
+  // since we don't have access to `window` like in `src/app/config.js`
+  self.importScripts("/config.js");
+
+  // this is the fallback single-page-app route, matching vite.config.js PWA config,
+  // and is served by the go web server. It is needed for the single-page-app to work.
+  // https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route
+  registerRoute(
+    new NavigationRoute(createHandlerBoundToURL("/app.html"), {
+      allowlist: [
+        // the app root itself, could be /, or not
+        new RegExp(`^${config.app_root}$`),
+        // any route starting with `/`, but not `/` itself.
+        // this is so we don't respond to `/` UNLESS it's the app root itself, defined above
+        /^\/.+$/,
+      ],
+      denylist: [/^\/docs\/?$/],
+    })
+  );
+
   // the manifest excludes config.js (see vite.config.js) since the dist-file differs from the
   // actual config served by the go server. this adds it back with `NetworkFirst`, so that the
   // most recent config from the go server is cached, but the app still works if the network

+ 5 - 2
web/vite.config.js

@@ -25,15 +25,18 @@ export default defineConfig(() => ({
         navigateFallback: "index.html",
       },
       injectManifest: {
-        globPatterns: ["**/*.{js,css,html,mp3,png,svg,json}"],
+        globPatterns: ["**/*.{js,css,html,mp3,ico,png,svg,json}"],
         globIgnores: ["config.js"],
         manifestTransforms: [
           (entries) => ({
             manifest: entries.map((entry) =>
+              // this matches the build step in the Makefile.
+              // since ntfy needs the ability to serve another page on /index.html,
+              // it's renamed and served from server.go as app.html as well.
               entry.url === "index.html"
                 ? {
                     ...entry,
-                    url: "/",
+                    url: "app.html",
                   }
                 : entry
             ),