From e27d5719f037aa9e9dca27c4eff23d4622b5475d Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 5 Mar 2022 20:24:10 -0500
Subject: [PATCH] Embed new web UI into server

---
 .gitignore                                    |   1 +
 Makefile                                      |  29 +++++++++-
 server/server.go                              |  53 ++++++++----------
 server/static/img/close.svg                   |   1 -
 server/static/img/favicon.png                 | Bin 4701 -> 0 bytes
 server/static/img/ntfy.png                    | Bin 3627 -> 0 bytes
 web/README.md                                 |  44 ---------------
 web/public/config.js                          |   3 -
 web/public/favicon.ico                        | Bin 3870 -> 0 bytes
 server/index.gohtml => web/public/home.html   |  10 ++--
 web/public/index.html                         |   9 +--
 web/public/manifest.json                      |  15 -----
 .../app.css => web/public/static/css/home.css |   0
 .../static/font/roboto-v29-latin-300.woff     | Bin
 .../static/font/roboto-v29-latin-300.woff2    | Bin
 .../static/font/roboto-v29-latin-500.woff     | Bin
 .../static/font/roboto-v29-latin-500.woff2    | Bin
 .../static/font/roboto-v29-latin-regular.woff | Bin
 .../font/roboto-v29-latin-regular.woff2       | Bin
 .../static/img/android-video-overview.mp4     | Bin
 .../img/android-video-subscribe-api.mp4       | Bin
 .../public}/static/img/badge-appstore.png     | Bin
 .../public}/static/img/badge-fdroid.png       | Bin
 .../public}/static/img/badge-googleplay.png   | Bin
 .../public}/static/img/basic-notification.png | Bin
 .../public}/static/img/screenshot-curl.png    | Bin
 .../public}/static/img/screenshot-docs.png    | Bin
 .../static/img/screenshot-phone-add.jpg       | Bin
 .../static/img/screenshot-phone-detail.jpg    | Bin
 .../static/img/screenshot-phone-main.jpg      | Bin
 .../img/screenshot-phone-notification.jpg     | Bin
 .../static/img/screenshot-phone-popover.png   | Bin
 .../static/img/screenshot-web-detail.png      | Bin
 .../js/app.js => web/public/static/js/home.js |   0
 web/src/app/config.js                         |   5 +-
 web/src/components/App.js                     |   1 -
 web/src/components/Notifications.js           |   2 +-
 web/src/components/SubscribeDialog.js         |   1 +
 38 files changed, 64 insertions(+), 110 deletions(-)
 delete mode 100644 server/static/img/close.svg
 delete mode 100644 server/static/img/favicon.png
 delete mode 100644 server/static/img/ntfy.png
 delete mode 100644 web/README.md
 delete mode 100644 web/public/config.js
 delete mode 100644 web/public/favicon.ico
 rename server/index.gohtml => web/public/home.html (96%)
 delete mode 100644 web/public/manifest.json
 rename server/static/css/app.css => web/public/static/css/home.css (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-300.woff (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-300.woff2 (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-500.woff (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-500.woff2 (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-regular.woff (100%)
 rename {server => web/public}/static/font/roboto-v29-latin-regular.woff2 (100%)
 rename {server => web/public}/static/img/android-video-overview.mp4 (100%)
 rename {server => web/public}/static/img/android-video-subscribe-api.mp4 (100%)
 rename {server => web/public}/static/img/badge-appstore.png (100%)
 rename {server => web/public}/static/img/badge-fdroid.png (100%)
 rename {server => web/public}/static/img/badge-googleplay.png (100%)
 rename {server => web/public}/static/img/basic-notification.png (100%)
 rename {server => web/public}/static/img/screenshot-curl.png (100%)
 rename {server => web/public}/static/img/screenshot-docs.png (100%)
 rename {server => web/public}/static/img/screenshot-phone-add.jpg (100%)
 rename {server => web/public}/static/img/screenshot-phone-detail.jpg (100%)
 rename {server => web/public}/static/img/screenshot-phone-main.jpg (100%)
 rename {server => web/public}/static/img/screenshot-phone-notification.jpg (100%)
 rename server/static/img/priority-notification.png => web/public/static/img/screenshot-phone-popover.png (100%)
 rename {server => web/public}/static/img/screenshot-web-detail.png (100%)
 rename server/static/js/app.js => web/public/static/js/home.js (100%)

diff --git a/.gitignore b/.gitignore
index 932e0fcd..9f514857 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ dist/
 build/
 .idea/
 server/docs/
+server/site/
 tools/fbsend/fbsend
 playground/
 *.iml
diff --git a/Makefile b/Makefile
index a9a9a201..06b4c745 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,28 @@ docs-deps: .PHONY
 docs: docs-deps
 	mkdocs build
 
+
+# Web app
+
+web-deps:
+	cd web && npm install
+
+web-build:
+	cd web \
+		&& npm run build \
+		&& mv build/index.html build/app.html \
+		&& rm -rf ../server/site \
+		&& mv build ../server/site \
+		&& rm \
+			../server/site/precache* \
+			../server/site/service-worker.js \
+			../server/site/asset-manifest.json \
+			../server/site/static/js/*.js.map \
+			../server/site/static/js/*.js.LICENSE.txt
+
+web: web-deps web-build
+
+
 # Test/check targets
 
 check: test fmt-check vet lint staticcheck
@@ -94,7 +116,7 @@ staticcheck: .PHONY
 
 # Building targets
 
-build-deps: docs
+build-deps: docs web
 	which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
 	which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
 
@@ -105,8 +127,9 @@ build-snapshot: build-deps
 	goreleaser build --snapshot --rm-dist --debug
 
 build-simple: clean
-	mkdir -p dist/ntfy_linux_amd64 server/docs
-	touch server/docs/dummy
+	mkdir -p dist/ntfy_linux_amd64 server/docs server/site
+	touch server/docs/index.html
+	touch server/site/app.html
 	export CGO_ENABLED=1
 	go build \
 		-o dist/ntfy_linux_amd64/ntfy \
diff --git a/server/server.go b/server/server.go
index 7e4c551a..9edcc86c 100644
--- a/server/server.go
+++ b/server/server.go
@@ -13,7 +13,6 @@ import (
 	"golang.org/x/sync/errgroup"
 	"heckel.io/ntfy/auth"
 	"heckel.io/ntfy/util"
-	"html/template"
 	"io"
 	"log"
 	"net"
@@ -61,35 +60,31 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
 
 var (
 	// If changed, don't forget to update Android App and auth_sqlite.go
-	topicRegex       = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)  // No /!
-	topicPathRegex   = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
-	jsonPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
-	ssePathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
-	rawPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
-	wsPathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
-	authPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
-	publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
+	topicRegex        = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)               // No /!
+	topicPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`)              // Regex must match JS & Android app!
+	extTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
+	jsonPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
+	ssePathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
+	rawPathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
+	wsPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
+	authPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
+	publishPathRegex  = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
 
 	staticRegex      = regexp.MustCompile(`^/static/.+`)
 	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`)
 	fileRegex        = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
-	disallowedTopics = []string{"docs", "static", "file"} // If updated, also update in Android app
+	disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
 	attachURLRegex   = regexp.MustCompile(`^https?://`)
 
-	templateFnMap = template.FuncMap{
-		"durationToHuman": util.DurationToHuman,
-	}
-
-	//go:embed "index.gohtml"
-	indexSource   string
-	indexTemplate = template.Must(template.New("index").Funcs(templateFnMap).Parse(indexSource))
-
 	//go:embed "example.html"
 	exampleSource string
 
-	//go:embed static
-	webStaticFs       embed.FS
-	webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
+	//go:embed site
+	webFs        embed.FS
+	webFsCached  = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
+	webSiteDir   = "/site"
+	webHomeIndex = "/home.html" // Landing page, only if "web-index: home"
+	webAppIndex  = "/app.html"  // React app
 
 	//go:embed docs
 	docsStaticFs     embed.FS
@@ -284,8 +279,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.limitRequests(s.handleFile)(w, r, v)
 	} else if r.Method == http.MethodOptions {
 		return s.handleOptions(w, r)
-	} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
-		return s.handleTopic(w, r)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
 	} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
@@ -300,15 +293,15 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
 	} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
+	} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || extTopicPathRegex.MatchString(r.URL.Path)) {
+		return s.handleTopic(w, r)
 	}
 	return errHTTPNotFound
 }
 
 func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
-	return indexTemplate.Execute(w, &indexPage{
-		Topic:         r.URL.Path[1:],
-		CacheDuration: s.config.CacheDuration,
-	})
+	r.URL.Path = webHomeIndex
+	return s.handleStatic(w, r)
 }
 
 func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
@@ -319,7 +312,8 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
 		_, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n")
 		return err
 	}
-	return s.handleHome(w, r)
+	r.URL.Path = webAppIndex
+	return s.handleStatic(w, r)
 }
 
 func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
@@ -339,7 +333,8 @@ func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
 }
 
 func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
-	http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r)
+	r.URL.Path = webSiteDir + r.URL.Path
+	http.FileServer(http.FS(webFsCached)).ServeHTTP(w, r)
 	return nil
 }
 
diff --git a/server/static/img/close.svg b/server/static/img/close.svg
deleted file mode 100644
index 5f1267d7..00000000
--- a/server/static/img/close.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
\ No newline at end of file
diff --git a/server/static/img/favicon.png b/server/static/img/favicon.png
deleted file mode 100644
index 92312feac6a3c9effe8323e76a6d30f2ce88d724..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4701
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
Zdu`HC4VjGyX*2{dxMQmGUfU`5e*gp9@R9%k

diff --git a/server/static/img/ntfy.png b/server/static/img/ntfy.png
deleted file mode 100644
index 6b969a846efc6c3dac70510793c72253e04389c9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3627
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

diff --git a/web/README.md b/web/README.md
deleted file mode 100644
index 953f65b2..00000000
--- a/web/README.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# Create React App example
-
-## How to use
-
-Download the example [or clone the repo](https://github.com/mui/material-ui):
-
-<!-- #default-branch-switch -->
-
-```sh
-curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/create-react-app
-cd create-react-app
-```
-
-Install it and run:
-
-```sh
-npm install
-npm start
-```
-
-or:
-
-<!-- #default-branch-switch -->
-
-[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/create-react-app)
-
-<!-- #default-branch-switch -->
-
-[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/create-react-app)
-
-## The idea behind the example
-
-<!-- #default-branch-switch -->
-
-This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app).
-It includes `@mui/material` and its peer dependencies, including `emotion`, the default style engine in MUI v5.
-If you prefer, you can [use styled-components instead](https://mui.com/guides/interoperability/#styled-components).
-
-## What's next?
-
-<!-- #default-branch-switch -->
-
-You now have a working example project.
-You can head back to the documentation, continuing browsing it from the [templates](https://mui.com/getting-started/templates/) section.
diff --git a/web/public/config.js b/web/public/config.js
deleted file mode 100644
index 3283db2e..00000000
--- a/web/public/config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var config = {
-    defaultBaseUrl: 'https://ntfy.sh'
-};
diff --git a/web/public/favicon.ico b/web/public/favicon.ico
deleted file mode 100644
index a11777cc471a4344702741ab1c8a588998b1311a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3870
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
baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@

diff --git a/server/index.gohtml b/web/public/home.html
similarity index 96%
rename from server/index.gohtml
rename to web/public/home.html
index 9fec14cf..f054f83a 100644
--- a/server/index.gohtml
+++ b/web/public/home.html
@@ -1,11 +1,10 @@
-{{- /*gotype: heckel.io/ntfy/server.indexPage*/ -}}
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
 
     <title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
-    <link rel="stylesheet" href="static/css/app.css" type="text/css">
+    <link rel="stylesheet" href="static/css/home.css" type="text/css">
 
     <!-- Mobile view -->
     <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
@@ -37,9 +36,9 @@
         <div id="name">ntfy</div>
         <ol>
             <li><a href="docs/">Getting started</a></li>
+            <li><a href="app">Web app</a></li>
             <li><a href="docs/subscribe/phone/">Android/iOS</a></li>
             <li><a href="docs/publish/">API</a></li>
-            <li><a href="docs/install/">Self-hosting</a></li>
             <li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
         </ol>
     </div>
@@ -90,7 +89,7 @@
         Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
     </p>
     <figure>
-        <img src="static/img/priority-notification.png" style="max-height: 200px"/>
+        <img src="static/img/screenshot-phone-popover.png" style="max-height: 200px"/>
         <figcaption>Urgent notification with pop-over</figcaption>
     </figure>
 
@@ -170,7 +169,6 @@
     <center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
 </div>
 <div id="lightbox" class="lightbox"></div>
-<script src="static/js/emoji.js"></script>
-<script src="static/js/app.js"></script>
+<script src="static/js/home.js"></script>
 </body>
 </html>
diff --git a/web/public/index.html b/web/public/index.html
index 3512b548..19e14506 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -20,18 +20,15 @@
   <!-- Previews in Google, Slack, WhatsApp, etc. -->
   <meta property="og:type" content="website" />
   <meta property="og:locale" content="en_US" />
-  <meta property="og:site_name" content="ntfy.sh" />
-  <meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" />
-  <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
+  <meta property="og:site_name" content="ntfy web" />
+  <meta property="og:title" content="ntfy web | Web app to receive push notifications from scripts via PUT/POST" />
+  <meta property="og:description" content="ntfy lets you send push notifications via scripts from any computer or phone, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
   <meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
   <meta property="og:url" content="https://ntfy.sh" />
 
   <!-- Never index -->
   <meta name="robots" content="noindex, nofollow" />
 
-  <!-- Server configuration -->
-  <script src="%PUBLIC_URL%/config.js"></script>
-
   <!-- FIXME Roboto -->
   <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
 </head>
diff --git a/web/public/manifest.json b/web/public/manifest.json
deleted file mode 100644
index f99717a5..00000000
--- a/web/public/manifest.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "short_name": "Your Orders",
-  "name": "Your Orders",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}
diff --git a/server/static/css/app.css b/web/public/static/css/home.css
similarity index 100%
rename from server/static/css/app.css
rename to web/public/static/css/home.css
diff --git a/server/static/font/roboto-v29-latin-300.woff b/web/public/static/font/roboto-v29-latin-300.woff
similarity index 100%
rename from server/static/font/roboto-v29-latin-300.woff
rename to web/public/static/font/roboto-v29-latin-300.woff
diff --git a/server/static/font/roboto-v29-latin-300.woff2 b/web/public/static/font/roboto-v29-latin-300.woff2
similarity index 100%
rename from server/static/font/roboto-v29-latin-300.woff2
rename to web/public/static/font/roboto-v29-latin-300.woff2
diff --git a/server/static/font/roboto-v29-latin-500.woff b/web/public/static/font/roboto-v29-latin-500.woff
similarity index 100%
rename from server/static/font/roboto-v29-latin-500.woff
rename to web/public/static/font/roboto-v29-latin-500.woff
diff --git a/server/static/font/roboto-v29-latin-500.woff2 b/web/public/static/font/roboto-v29-latin-500.woff2
similarity index 100%
rename from server/static/font/roboto-v29-latin-500.woff2
rename to web/public/static/font/roboto-v29-latin-500.woff2
diff --git a/server/static/font/roboto-v29-latin-regular.woff b/web/public/static/font/roboto-v29-latin-regular.woff
similarity index 100%
rename from server/static/font/roboto-v29-latin-regular.woff
rename to web/public/static/font/roboto-v29-latin-regular.woff
diff --git a/server/static/font/roboto-v29-latin-regular.woff2 b/web/public/static/font/roboto-v29-latin-regular.woff2
similarity index 100%
rename from server/static/font/roboto-v29-latin-regular.woff2
rename to web/public/static/font/roboto-v29-latin-regular.woff2
diff --git a/server/static/img/android-video-overview.mp4 b/web/public/static/img/android-video-overview.mp4
similarity index 100%
rename from server/static/img/android-video-overview.mp4
rename to web/public/static/img/android-video-overview.mp4
diff --git a/server/static/img/android-video-subscribe-api.mp4 b/web/public/static/img/android-video-subscribe-api.mp4
similarity index 100%
rename from server/static/img/android-video-subscribe-api.mp4
rename to web/public/static/img/android-video-subscribe-api.mp4
diff --git a/server/static/img/badge-appstore.png b/web/public/static/img/badge-appstore.png
similarity index 100%
rename from server/static/img/badge-appstore.png
rename to web/public/static/img/badge-appstore.png
diff --git a/server/static/img/badge-fdroid.png b/web/public/static/img/badge-fdroid.png
similarity index 100%
rename from server/static/img/badge-fdroid.png
rename to web/public/static/img/badge-fdroid.png
diff --git a/server/static/img/badge-googleplay.png b/web/public/static/img/badge-googleplay.png
similarity index 100%
rename from server/static/img/badge-googleplay.png
rename to web/public/static/img/badge-googleplay.png
diff --git a/server/static/img/basic-notification.png b/web/public/static/img/basic-notification.png
similarity index 100%
rename from server/static/img/basic-notification.png
rename to web/public/static/img/basic-notification.png
diff --git a/server/static/img/screenshot-curl.png b/web/public/static/img/screenshot-curl.png
similarity index 100%
rename from server/static/img/screenshot-curl.png
rename to web/public/static/img/screenshot-curl.png
diff --git a/server/static/img/screenshot-docs.png b/web/public/static/img/screenshot-docs.png
similarity index 100%
rename from server/static/img/screenshot-docs.png
rename to web/public/static/img/screenshot-docs.png
diff --git a/server/static/img/screenshot-phone-add.jpg b/web/public/static/img/screenshot-phone-add.jpg
similarity index 100%
rename from server/static/img/screenshot-phone-add.jpg
rename to web/public/static/img/screenshot-phone-add.jpg
diff --git a/server/static/img/screenshot-phone-detail.jpg b/web/public/static/img/screenshot-phone-detail.jpg
similarity index 100%
rename from server/static/img/screenshot-phone-detail.jpg
rename to web/public/static/img/screenshot-phone-detail.jpg
diff --git a/server/static/img/screenshot-phone-main.jpg b/web/public/static/img/screenshot-phone-main.jpg
similarity index 100%
rename from server/static/img/screenshot-phone-main.jpg
rename to web/public/static/img/screenshot-phone-main.jpg
diff --git a/server/static/img/screenshot-phone-notification.jpg b/web/public/static/img/screenshot-phone-notification.jpg
similarity index 100%
rename from server/static/img/screenshot-phone-notification.jpg
rename to web/public/static/img/screenshot-phone-notification.jpg
diff --git a/server/static/img/priority-notification.png b/web/public/static/img/screenshot-phone-popover.png
similarity index 100%
rename from server/static/img/priority-notification.png
rename to web/public/static/img/screenshot-phone-popover.png
diff --git a/server/static/img/screenshot-web-detail.png b/web/public/static/img/screenshot-web-detail.png
similarity index 100%
rename from server/static/img/screenshot-web-detail.png
rename to web/public/static/img/screenshot-web-detail.png
diff --git a/server/static/js/app.js b/web/public/static/js/home.js
similarity index 100%
rename from server/static/js/app.js
rename to web/public/static/js/home.js
diff --git a/web/src/app/config.js b/web/src/app/config.js
index 71a9ece3..1976d79e 100644
--- a/web/src/app/config.js
+++ b/web/src/app/config.js
@@ -1,2 +1,5 @@
-const config = window.config;
+//const config = window.config;
+const config = {
+    defaultBaseUrl: "https://ntfy.sh"
+};
 export default config;
diff --git a/web/src/components/App.js b/web/src/components/App.js
index c39fd412..7834de47 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -21,7 +21,6 @@ import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-rout
 import {subscriptionRoute} from "../app/utils";
 
 // TODO support unsubscribed routes
-// TODO embed into ntfy server
 // TODO googlefonts
 // TODO new notification indicator
 // TODO sound
diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js
index d64ef0a6..38e8c9c4 100644
--- a/web/src/components/Notifications.js
+++ b/web/src/components/Notifications.js
@@ -251,7 +251,7 @@ const NothingHereYet = (props) => {
     return (
         <VerticallyCenteredContainer maxWidth="xs">
             <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
-                <img src="static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
+                <img src="/static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
                 You haven't received any notifications for this topic yet.
             </Typography>
             <Paragraph>
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index c3a362d5..aa2de679 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -109,6 +109,7 @@ const SubscribePage = (props) => {
                     margin="dense"
                     id="topic"
                     placeholder="Topic name, e.g. phil_alerts"
+                    inputProps={{ maxLength: 64 }}
                     value={props.topic}
                     onChange={ev => props.setTopic(ev.target.value)}
                     type="text"