Fix PWA for non-root web roots
parent
6615aea5dc
commit
d7aacb8b24
|
@ -79,6 +79,7 @@ var (
|
||||||
|
|
||||||
webConfigPath = "/config.js"
|
webConfigPath = "/config.js"
|
||||||
webManifestPath = "/manifest.webmanifest"
|
webManifestPath = "/manifest.webmanifest"
|
||||||
|
webRootHTMLPath = "/app.html"
|
||||||
webServiceWorkerPath = "/sw.js"
|
webServiceWorkerPath = "/sw.js"
|
||||||
accountPath = "/account"
|
accountPath = "/account"
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
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)
|
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
|
||||||
return s.ensureWebEnabled(s.handleWebManifest)(w, r, v)
|
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 {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
|
||||||
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
|
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
|
||||||
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
|
} 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)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
||||||
return s.handleMetrics(w, r, v)
|
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)
|
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
||||||
|
@ -590,9 +589,29 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
return err
|
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")
|
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,
|
// handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set,
|
||||||
|
|
|
@ -245,6 +245,9 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
rr = request(t, s, "GET", "/sw.js", "", nil)
|
rr = request(t, s, "GET", "/sw.js", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
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)
|
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
@ -264,6 +267,9 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
|
|
||||||
rr = request(t, s2, "GET", "/sw.js", "", nil)
|
rr = request(t, s2, "GET", "/sw.js", "", nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
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) {
|
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||||
|
|
|
@ -518,3 +518,22 @@ func (w *webPushSubscription) Context() log.Context {
|
||||||
"web_push_subscription_endpoint": w.Endpoint,
|
"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"`
|
||||||
|
}
|
||||||
|
|
|
@ -227,9 +227,28 @@ precacheAndRoute(
|
||||||
// Delete any cached old dist files from previous service worker versions
|
// Delete any cached old dist files from previous service worker versions
|
||||||
cleanupOutdatedCaches();
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
if (import.meta.env.MODE !== "development") {
|
if (!import.meta.env.DEV) {
|
||||||
// since the manifest only includes `/index.html`, this manually adds the root route `/`
|
// we need the app_root setting, so we import the config.js file from the go server
|
||||||
registerRoute(new NavigationRoute(createHandlerBoundToURL("/")));
|
// 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
|
// 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
|
// 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
|
// most recent config from the go server is cached, but the app still works if the network
|
||||||
|
|
|
@ -25,15 +25,18 @@ export default defineConfig(() => ({
|
||||||
navigateFallback: "index.html",
|
navigateFallback: "index.html",
|
||||||
},
|
},
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
globPatterns: ["**/*.{js,css,html,mp3,png,svg,json}"],
|
globPatterns: ["**/*.{js,css,html,mp3,ico,png,svg,json}"],
|
||||||
globIgnores: ["config.js"],
|
globIgnores: ["config.js"],
|
||||||
manifestTransforms: [
|
manifestTransforms: [
|
||||||
(entries) => ({
|
(entries) => ({
|
||||||
manifest: entries.map((entry) =>
|
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 === "index.html"
|
||||||
? {
|
? {
|
||||||
...entry,
|
...entry,
|
||||||
url: "/",
|
url: "app.html",
|
||||||
}
|
}
|
||||||
: entry
|
: entry
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue