Remove old homepage
							
								
								
									
										19
									
								
								cmd/serve.go
									
										
									
									
									
								
							
							
						
						|  | @ -59,7 +59,7 @@ var flagsServe = append( | |||
| 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}), | ||||
| 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}), | ||||
| 	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}), | ||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}), | ||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}), | ||||
| 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}), | ||||
| 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}), | ||||
| 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}), | ||||
|  | @ -195,8 +195,6 @@ func execServe(c *cli.Context) error { | |||
| 		return errors.New("if set, base-url must start with http:// or https://") | ||||
| 	} else if baseURL != "" && strings.HasSuffix(baseURL, "/") { | ||||
| 		return errors.New("if set, base-url must not end with a slash (/)") | ||||
| 	} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) { | ||||
| 		return errors.New("if set, web-root must be 'home' or 'app'") | ||||
| 	} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") { | ||||
| 		return errors.New("if set, upstream-base-url must start with http:// or https://") | ||||
| 	} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") { | ||||
|  | @ -213,8 +211,16 @@ func execServe(c *cli.Context) error { | |||
| 		return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set") | ||||
| 	} | ||||
| 
 | ||||
| 	webRootIsApp := webRoot == "app" | ||||
| 	enableWeb := webRoot != "disable" | ||||
| 	// Backwards compatibility | ||||
| 	if webRoot == "app" { | ||||
| 		webRoot = "/" | ||||
| 	} else if webRoot == "home" { | ||||
| 		webRoot = "/app" | ||||
| 	} else if webRoot == "disable" { | ||||
| 		webRoot = "" | ||||
| 	} else if !strings.HasPrefix(webRoot, "/") { | ||||
| 		webRoot = "/" + webRoot | ||||
| 	} | ||||
| 
 | ||||
| 	// Default auth permissions | ||||
| 	authDefault, err := user.ParsePermission(authDefaultAccess) | ||||
|  | @ -293,7 +299,7 @@ func execServe(c *cli.Context) error { | |||
| 	conf.KeepaliveInterval = keepaliveInterval | ||||
| 	conf.ManagerInterval = managerInterval | ||||
| 	conf.DisallowedTopics = disallowedTopics | ||||
| 	conf.WebRootIsApp = webRootIsApp | ||||
| 	conf.WebRoot = webRoot | ||||
| 	conf.UpstreamBaseURL = upstreamBaseURL | ||||
| 	conf.SMTPSenderAddr = smtpSenderAddr | ||||
| 	conf.SMTPSenderUser = smtpSenderUser | ||||
|  | @ -317,7 +323,6 @@ func execServe(c *cli.Context) error { | |||
| 	conf.StripeSecretKey = stripeSecretKey | ||||
| 	conf.StripeWebhookKey = stripeWebhookKey | ||||
| 	conf.BillingContact = billingContact | ||||
| 	conf.EnableWeb = enableWeb | ||||
| 	conf.EnableSignup = enableSignup | ||||
| 	conf.EnableLogin = enableLogin | ||||
| 	conf.EnableReservations = enableReservations | ||||
|  |  | |||
|  | @ -1255,7 +1255,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | |||
| | `visitor-request-limit-exempt-hosts`       | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS`       | *comma-separated host/IP list*                      | -                 | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting                                                                                                                                                | | ||||
| | `visitor-subscription-limit`               | `NTFY_VISITOR_SUBSCRIPTION_LIMIT`               | *number*                                            | 30                | Rate limiting: Number of subscriptions per visitor (IP address)                                                                                                                                                                 | | ||||
| | `visitor-subscriber-rate-limiting`         | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING`         | *bool*                                              | `false`           | Rate limiting: Enables subscriber-based rate limiting                                                                                                                                                                           | | ||||
| | `web-root`                                 | `NTFY_WEB_ROOT`                                 | `app`, `home` or `disable`                          | `app`             | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable)                                                                                                                                  | | ||||
| | `web-root`                                 | `NTFY_WEB_ROOT`                                 | *path*, e.g. `/` or `/app`, or `disable`            | `/`               | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable)                                                                                                                                                   | | ||||
| | `enable-signup`                            | `NTFY_ENABLE_SIGNUP`                            | *boolean* (`true` or `false`)                       | `false`           | Allows users to sign up via the web app, or API                                                                                                                                                                                 | | ||||
| | `enable-login`                             | `NTFY_ENABLE_LOGIN`                             | *boolean* (`true` or `false`)                       | `false`           | Allows users to log in via the web app, or API                                                                                                                                                                                  | | ||||
| | `enable-reservations`                      | `NTFY_ENABLE_RESERVATIONS`                      | *boolean* (`true` or `false`)                       | `false`           | Allows users to reserve topics (if their tier allows it)                                                                                                                                                                        | | ||||
|  |  | |||
|  | @ -1178,6 +1178,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | |||
| 
 | ||||
| ## Not released yet | ||||
| 
 | ||||
| ## ntfy server v2.5.0 (UNRELEASED) | ||||
| 
 | ||||
| **Bug fixes + maintenance:** | ||||
| 
 | ||||
| * Removed old ntfy website from ntfy entirely (no ticket) | ||||
| 
 | ||||
| ### ntfy Android app v1.16.1 (UNRELEASED) | ||||
| 
 | ||||
| **Features:** | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ type Config struct { | |||
| 	KeepaliveInterval                    time.Duration | ||||
| 	ManagerInterval                      time.Duration | ||||
| 	DisallowedTopics                     []string | ||||
| 	WebRootIsApp                         bool | ||||
| 	WebRoot                              string // empty to disable | ||||
| 	DelayedSenderInterval                time.Duration | ||||
| 	FirebaseKeepaliveInterval            time.Duration | ||||
| 	FirebasePollInterval                 time.Duration | ||||
|  | @ -133,7 +133,6 @@ type Config struct { | |||
| 	StripeWebhookKey                     string | ||||
| 	StripePriceCacheDuration             time.Duration | ||||
| 	BillingContact                       string | ||||
| 	EnableWeb                            bool | ||||
| 	EnableSignup                         bool // Enable creation of accounts via API and UI | ||||
| 	EnableLogin                          bool | ||||
| 	EnableReservations                   bool // Allow users with role "user" to own/reserve topics | ||||
|  | @ -171,7 +170,7 @@ func NewConfig() *Config { | |||
| 		KeepaliveInterval:                    DefaultKeepaliveInterval, | ||||
| 		ManagerInterval:                      DefaultManagerInterval, | ||||
| 		DisallowedTopics:                     DefaultDisallowedTopics, | ||||
| 		WebRootIsApp:                         false, | ||||
| 		WebRoot:                              "/", | ||||
| 		DelayedSenderInterval:                DefaultDelayedSenderInterval, | ||||
| 		FirebaseKeepaliveInterval:            DefaultFirebaseKeepaliveInterval, | ||||
| 		FirebasePollInterval:                 DefaultFirebasePollInterval, | ||||
|  | @ -209,7 +208,6 @@ func NewConfig() *Config { | |||
| 		StripeWebhookKey:                     "", | ||||
| 		StripePriceCacheDuration:             DefaultStripePriceCacheDuration, | ||||
| 		BillingContact:                       "", | ||||
| 		EnableWeb:                            true, | ||||
| 		EnableSignup:                         false, | ||||
| 		EnableLogin:                          false, | ||||
| 		EnableReservations:                   false, | ||||
|  |  | |||
|  | @ -100,11 +100,10 @@ var ( | |||
| 	urlRegex                                             = regexp.MustCompile(`^https?://`) | ||||
| 
 | ||||
| 	//go:embed site | ||||
| 	webFs        embed.FS | ||||
| 	webFsCached  = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs} | ||||
| 	webSiteDir   = "/site" | ||||
| 	webHomeIndex = "/home.html" // Landing page, only if "web-root: home" | ||||
| 	webAppIndex  = "/app.html"  // React app | ||||
| 	webFs       embed.FS | ||||
| 	webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs} | ||||
| 	webSiteDir  = "/site" | ||||
| 	webAppIndex = "/app.html" // React app | ||||
| 
 | ||||
| 	//go:embed docs | ||||
| 	docsStaticFs     embed.FS | ||||
|  | @ -404,8 +403,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor, | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	if r.Method == http.MethodGet && r.URL.Path == "/" { | ||||
| 		return s.ensureWebEnabled(s.handleHome)(w, r, v) | ||||
| 	if r.Method == http.MethodGet && r.URL.Path == "/" && s.config.WebRoot == "/" { | ||||
| 		return s.ensureWebEnabled(s.handleRoot)(w, r, v) | ||||
| 	} else if r.Method == http.MethodHead && r.URL.Path == "/" { | ||||
| 		return s.ensureWebEnabled(s.handleEmpty)(w, r, v) | ||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath { | ||||
|  | @ -490,12 +489,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | |||
| 	return errHTTPNotFound | ||||
| } | ||||
| 
 | ||||
| func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	if s.config.WebRootIsApp { | ||||
| 		r.URL.Path = webAppIndex | ||||
| 	} else { | ||||
| 		r.URL.Path = webHomeIndex | ||||
| 	} | ||||
| func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	r.URL.Path = webAppIndex | ||||
| 	return s.handleStatic(w, r, v) | ||||
| } | ||||
| 
 | ||||
|  | @ -527,13 +522,9 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error { | ||||
| 	appRoot := "/" | ||||
| 	if !s.config.WebRootIsApp { | ||||
| 		appRoot = "/app" | ||||
| 	} | ||||
| 	response := &apiConfigResponse{ | ||||
| 		BaseURL:            "", // Will translate to window.location.origin | ||||
| 		AppRoot:            appRoot, | ||||
| 		AppRoot:            s.config.WebRoot, | ||||
| 		EnableLogin:        s.config.EnableLogin, | ||||
| 		EnableSignup:       s.config.EnableSignup, | ||||
| 		EnablePayments:     s.config.StripeSecretKey != "", | ||||
|  |  | |||
|  | @ -167,11 +167,13 @@ | |||
| # | ||||
| # disallowed-topics: | ||||
| 
 | ||||
| # Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the | ||||
| # web app. If you self-host, you don't want to change this. | ||||
| # Can be "app" (default), "home" or "disable" to disable the web app entirely. | ||||
| # Defines the root path of the web app, or disables the web app entirely. | ||||
| # | ||||
| # web-root: app | ||||
| # Can be any simple path, e.g. "/", "/app", or "/ntfy". For backwards-compatibility reasons, | ||||
| # the values "app" (maps to "/"), "home" (maps to "/app"), or "disable" (maps to "") to disable | ||||
| # the web app entirely. | ||||
| # | ||||
| # web-root: / | ||||
| 
 | ||||
| # Various feature flags used to control the web app, and API access, mainly around user and | ||||
| # account management. | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc { | |||
| 
 | ||||
| func (s *Server) ensureWebEnabled(next handleFunc) handleFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 		if !s.config.EnableWeb { | ||||
| 		if s.config.WebRoot == "" { | ||||
| 			return errHTTPNotFound | ||||
| 		} | ||||
| 		return next(w, r, v) | ||||
|  |  | |||
|  | @ -220,10 +220,6 @@ func TestServer_StaticSites(t *testing.T) { | |||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	require.Contains(t, rr.Body.String(), `<meta name="robots" content="noindex, nofollow"/>`) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/static/css/home.css", "", nil) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	require.Contains(t, rr.Body.String(), `/* general styling */`) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/docs", "", nil) | ||||
| 	require.Equal(t, 301, rr.Code) | ||||
| 
 | ||||
|  | @ -232,7 +228,7 @@ func TestServer_StaticSites(t *testing.T) { | |||
| 
 | ||||
| func TestServer_WebEnabled(t *testing.T) { | ||||
| 	conf := newTestConfig(t) | ||||
| 	conf.EnableWeb = false | ||||
| 	conf.WebRoot = "" // Disable web app | ||||
| 	s := newTestServer(t, conf) | ||||
| 
 | ||||
| 	rr := request(t, s, "GET", "/", "", nil) | ||||
|  | @ -245,7 +241,7 @@ func TestServer_WebEnabled(t *testing.T) { | |||
| 	require.Equal(t, 404, rr.Code) | ||||
| 
 | ||||
| 	conf2 := newTestConfig(t) | ||||
| 	conf2.EnableWeb = true | ||||
| 	conf2.WebRoot = "/" | ||||
| 	s2 := newTestServer(t, conf2) | ||||
| 
 | ||||
| 	rr = request(t, s2, "GET", "/", "", nil) | ||||
|  | @ -253,9 +249,6 @@ func TestServer_WebEnabled(t *testing.T) { | |||
| 
 | ||||
| 	rr = request(t, s2, "GET", "/config.js", "", nil) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s2, "GET", "/static/css/home.css", "", nil) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| } | ||||
| 
 | ||||
| func TestServer_PublishLargeMessage(t *testing.T) { | ||||
|  |  | |||
|  | @ -1,182 +0,0 @@ | |||
| <!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/home.css" type="text/css"> | ||||
| 
 | ||||
|     <!-- Mobile view --> | ||||
|     <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||||
|     <meta name="HandheldFriendly" content="true"> | ||||
| 
 | ||||
|     <!-- Mobile browsers, background color --> | ||||
|     <meta name="theme-color" content="#317f6f"> | ||||
|     <meta name="msapplication-navbutton-color" content="#317f6f"> | ||||
|     <meta name="apple-mobile-web-app-status-bar-style" content="#317f6f"> | ||||
| 
 | ||||
|     <!-- Favicon, see favicon.io --> | ||||
|     <link rel="icon" type="image/png" href="static/img/favicon.png"> | ||||
| 
 | ||||
|     <!-- 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 | 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:image" content="/static/img/ntfy.png" /> | ||||
|     <meta property="og:url" content="https://ntfy.sh" /> | ||||
| 
 | ||||
|     <!-- Fonts --> | ||||
|     <link rel="stylesheet" href="static/css/fonts.css" type="text/css"> | ||||
| </head> | ||||
| <body> | ||||
| 
 | ||||
| <nav id="header"> | ||||
|     <div id="headerBox"> | ||||
|         <img id="logo" src="static/img/ntfy.png" alt="logo"/> | ||||
|         <div id="name">ntfy</div> | ||||
|         <ol> | ||||
|             <li><a href="app">Web app</a></li> | ||||
|             <li><a href="docs/subscribe/phone/">Android/iOS</a></li> | ||||
|             <li><a href="docs/">Docs</a></li> | ||||
|             <li><a href="docs/publish/">API</a></li> | ||||
|             <li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li> | ||||
|         </ol> | ||||
|     </div> | ||||
| </nav> | ||||
| <div id="main"> | ||||
|     <h1>Send push notifications to your phone or desktop via PUT/POST</h1> | ||||
|     <p> | ||||
|         <b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service. | ||||
|         It allows you to send notifications to your phone or desktop via scripts from any computer, | ||||
|         entirely <b>without signup, cost or setup</b>. It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own. | ||||
|     </p> | ||||
|     <div id="screenshots"> | ||||
|         <a href="static/img/screenshot-curl.png"><img src="static/img/screenshot-curl.png"/></a> | ||||
|         <a href="static/img/screenshot-web-detail.png"><img src="static/img/screenshot-web-detail.png"/></a> | ||||
|         <span class="nowrap"> | ||||
|             <a href="static/img/screenshot-phone-main.jpg"><img src="static/img/screenshot-phone-main.jpg"/></a> | ||||
|             <a href="static/img/screenshot-phone-detail.jpg"><img src="static/img/screenshot-phone-detail.jpg"/></a> | ||||
|             <a href="static/img/screenshot-phone-notification.jpg"><img src="static/img/screenshot-phone-notification.jpg"/></a> | ||||
|         </span> | ||||
|     </div> | ||||
| 
 | ||||
|     <h2 id="publish" class="anchor">Publishing messages</h2> | ||||
|     <p> | ||||
|         <a href="docs/publish/">Publishing messages</a> can be done via PUT or POST. Topics are created on the fly by subscribing or publishing to them. | ||||
|         Because there is no sign-up, <b>the topic is essentially a password</b>, so pick something that's not easily guessable. | ||||
|     </p> | ||||
|     <p class="smallMarginBottom"> | ||||
|         Here's an example showing how to publish a message using a POST request (via <tt>curl -d</tt>): | ||||
|     </p> | ||||
|     <code> | ||||
|         curl -d "Backup successful 😀" <span class="ntfyUrl">ntfy.sh</span>/mytopic | ||||
|     </code> | ||||
|     <p class="smallMarginBottom"> | ||||
|         There are <a href="docs/publish/">more features</a> related to publishing messages: You can set a | ||||
|         <a href="docs/publish/#message-priority">notification priority</a>, a <a href="docs/publish/#message-title">title</a>, | ||||
|         and <a href="docs/publish/#tags-emojis">tag messages</a>. | ||||
|         Here's an example using some of them together: | ||||
|     </p> | ||||
|     <code> | ||||
|         curl \<br/> | ||||
|           -H "Title: Unauthorized access detected" \<br/> | ||||
|           -H "Priority: urgent" \<br/> | ||||
|           -H "Tags: warning,skull" \<br/> | ||||
|           -d "Remote access to $(hostname) detected. Act right away." \<br/> | ||||
|           <span class="ntfyUrl">ntfy.sh</span>/mytopic | ||||
|     </code> | ||||
|     <p> | ||||
|         Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>: | ||||
|     </p> | ||||
|     <figure> | ||||
|         <img src="static/img/screenshot-phone-popover.png" style="max-height: 200px"/> | ||||
|         <figcaption>Urgent notification with pop-over</figcaption> | ||||
|     </figure> | ||||
| 
 | ||||
|     <h2 id="subscribe" class="anchor">Subscribe to a topic</h2> | ||||
|     <p> | ||||
|         You can create and subscribe to a topic either <a href="docs/subscribe/phone/">using your phone</a>, | ||||
|         in <a href="docs/subscribe/web/">this web UI</a>, or in your own app by <a href="docs/subscribe/api/">subscribing via the API</a>. | ||||
|     </p> | ||||
| 
 | ||||
|     <h3 id="subscribe-phone" class="anchor">Subscribe from your phone</h3> | ||||
|     <p> | ||||
|         Simply get the app and start <a href="docs/publish/">publishing messages</a>. To learn more about the app, | ||||
|         <a href="docs/subscribe/phone/">check out the documentation</a>. | ||||
|     </p> | ||||
|     <p> | ||||
|         <a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="static/img/badge-googleplay.png"></a> | ||||
|         <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="static/img/badge-fdroid.png"></a> | ||||
|         <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="static/img/badge-appstore.png"></a> | ||||
|     </p> | ||||
|     <p> | ||||
|         Here's a video showing the app in action: | ||||
|     </p> | ||||
|     <figure> | ||||
|         <video controls muted autoplay loop src="static/img/android-video-overview.mp4" style="max-width: 650px"></video> | ||||
|         <figcaption>Sending push notifications to your Android phone</figcaption> | ||||
|     </figure> | ||||
| 
 | ||||
|     <h3 id="subscribe-web" class="anchor">Subscribe via web app</h3> | ||||
|     <p> | ||||
|         Subscribe to topics in the <a href="app">web app</a> and receive messages as <b>desktop notification</b>. | ||||
|         It is available at <b><a href="app"><span class="ntfyUrl">ntfy.sh</span>/app</a></b>. | ||||
|     </p> | ||||
|     <figure> | ||||
|         <a href="app"><img src="static/img/screenshot-web-detail.png" width="100%"/></a> | ||||
|         <figcaption>ntfy web app, available at <a href="app"><span class="ntfyUrl">ntfy.sh</span>/app</a></figcaption> | ||||
|     </figure> | ||||
| 
 | ||||
|     <h3 id="subscribe-api" class="anchor">Subscribe using the API</h3> | ||||
|     <p> | ||||
|         There's a super simple API that you can use to integrate your own app. You can consume | ||||
|         a <a href="docs/subscribe/api/#subscribe-as-json-stream">JSON stream</a>, | ||||
|         an <a href="docs/subscribe/api/#subscribe-as-sse-stream">SSE/EventSource stream</a>, | ||||
|         a <a href="docs/subscribe/api/#subscribe-as-raw-stream">plain text stream</a>, | ||||
|         or <a href="docs/subscribe/api/#websockets">via WebSockets</a>. | ||||
|     </p> | ||||
|     <p class="smallMarginBottom"> | ||||
|         Here's an example for JSON. The <b>connection stays open</b>, so you can retrieve messages as they come in: | ||||
|     </p> | ||||
|     <code> | ||||
|         $ curl -s <span class="ntfyUrl">ntfy.sh</span>/mytopic/json<br/> | ||||
|         {"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}<br/> | ||||
|         {"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Hi!"}<br/> | ||||
|         {"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"}<br/> | ||||
|         ... | ||||
|     </code> | ||||
|     <p> | ||||
|         Here's a short video demonstrating it in action: | ||||
|     </p> | ||||
|     <figure> | ||||
|         <video controls muted autoplay loop src="static/img/android-video-subscribe-api.mp4" style="max-width: 650px"></video> | ||||
|         <figcaption>Subscribing to the JSON stream with <tt>curl</tt></figcaption> | ||||
|     </figure> | ||||
| 
 | ||||
|     <h3 id="docs" class="anchor">Check out the docs!</h3> | ||||
|     <p> | ||||
|         ntfy has so many more features and you can learn about all of them <a href="docs/">in the documentation</a> | ||||
|         (I tried my very best to make it the best docs ever 😉, not sure if I succeeded, hehe). | ||||
|     </p> | ||||
|     <figure> | ||||
|         <a href="docs/"><img width="100%" src="static/img/screenshot-docs.png"/></a> | ||||
|         <figcaption>Check out the documentation</figcaption> | ||||
|     </figure> | ||||
| 
 | ||||
|     <h3 id="free-software" class="anchor">100% open source & forever free</h3> | ||||
|     <p> | ||||
|         I love free software, and I'm doing this because it's fun. I have no bad intentions, and I will | ||||
|         never monetize or sell your information. This service will always stay | ||||
|         <a href="https://github.com/binwiederhier/ntfy">free and open</a>. | ||||
|         You can read more in the <a href="docs/faq/">FAQs</a> and in the <a href="docs/privacy/">privacy policy</a>. | ||||
|     </p> | ||||
| 
 | ||||
|     <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/home.js"></script> | ||||
| </body> | ||||
| </html> | ||||
|  | @ -15,7 +15,7 @@ | |||
|   <meta name="apple-mobile-web-app-status-bar-style" content="#317f6f"> | ||||
| 
 | ||||
|   <!-- Favicon, see favicon.io --> | ||||
|   <link rel="icon" type="image/png" href="%PUBLIC_URL%/static/img/favicon.ico"> | ||||
|   <link rel="icon" type="image/png" href="%PUBLIC_URL%/static/images/favicon.ico"> | ||||
| 
 | ||||
|   <!-- Previews in Google, Slack, WhatsApp, etc. --> | ||||
|   <meta property="og:type" content="website" /> | ||||
|  | @ -23,7 +23,7 @@ | |||
|   <meta property="og:site_name" content="ntfy web" /> | ||||
|   <meta property="og:title" content="ntfy web" /> | ||||
|   <meta property="og:description" content="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." /> | ||||
|   <meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" /> | ||||
|   <meta property="og:image" content="%PUBLIC_URL%/static/images/ntfy.png" /> | ||||
|   <meta property="og:url" content="https://ntfy.sh" /> | ||||
| 
 | ||||
|   <!-- Never index --> | ||||
|  |  | |||
|  | @ -1,280 +0,0 @@ | |||
| /* general styling */ | ||||
| 
 | ||||
| html, body { | ||||
|     font-family: 'Roboto', sans-serif; | ||||
|     font-weight: 400; | ||||
|     font-size: 1.1em; | ||||
|     color: #444; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|     /* prevent scrollbar from repositioning website: | ||||
|      * https://www.w3docs.com/snippets/css/how-to-prevent-scrollbar-from-repositioning-web-page.html */ | ||||
|     overflow-y: scroll; | ||||
| } | ||||
| 
 | ||||
| a, a:visited { | ||||
|     color: #338574; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|     text-decoration: none; | ||||
|     color: #317f6f; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|     margin-top: 35px; | ||||
|     margin-bottom: 30px; | ||||
|     font-size: 2.5em; | ||||
|     word-wrap: break-word; /* For very long topics */ | ||||
|     padding-right: 40px; /* For the X on the detail page */ | ||||
|     font-weight: 300; | ||||
|     color: #666; | ||||
| } | ||||
| 
 | ||||
| h2 { | ||||
|     margin-top: 30px; | ||||
|     margin-bottom: 5px; | ||||
|     font-size: 1.8em; | ||||
|     font-weight: 300; | ||||
|     color: #333; | ||||
| } | ||||
| 
 | ||||
| h3 { | ||||
|     margin-top: 25px; | ||||
|     margin-bottom: 5px; | ||||
|     font-size: 1.3em; | ||||
|     font-weight: 300; | ||||
|     color: #333; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
|     margin-top: 10px; | ||||
|     margin-bottom: 20px; | ||||
|     line-height: 160%; | ||||
|     font-weight: 400; | ||||
| } | ||||
| 
 | ||||
| p.smallMarginBottom { | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| b { | ||||
|     font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| tt { | ||||
|     background: #eee; | ||||
|     padding: 2px 7px; | ||||
|     border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|     display: block; | ||||
|     background: #eee; | ||||
|     font-family: monospace; | ||||
|     padding: 20px; | ||||
|     border-radius: 3px; | ||||
|     margin-top: 10px; | ||||
|     margin-bottom: 20px; | ||||
|     overflow-x: auto; | ||||
|     white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| /* Main page */ | ||||
| 
 | ||||
| #main { | ||||
|     max-width: 900px; | ||||
|     margin: 0 auto 50px auto; | ||||
|     padding: 0 10px; | ||||
| } | ||||
| 
 | ||||
| #error { | ||||
|     color: darkred; | ||||
|     font-style: italic; | ||||
| } | ||||
| 
 | ||||
| #ironicCenterTagDontFreakOut { | ||||
|     color: #666; | ||||
| } | ||||
| 
 | ||||
| /* Anchors */ | ||||
| 
 | ||||
| .anchor .anchorLink { | ||||
|     color: #ccc; | ||||
|     text-decoration: none; | ||||
|     padding: 0 5px; | ||||
|     visibility: hidden; | ||||
| } | ||||
| 
 | ||||
| .anchor:hover .anchorLink { | ||||
|     visibility: visible; | ||||
| } | ||||
| 
 | ||||
| .anchor .anchorLink:hover { | ||||
|     color: #338574; | ||||
|     visibility: visible; | ||||
| } | ||||
| 
 | ||||
| /* Figures */ | ||||
| 
 | ||||
| figure { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| figure img, figure video { | ||||
|     filter: drop-shadow(3px 3px 3px #ccc); | ||||
|     border-radius: 7px; | ||||
|     max-width: 100%; | ||||
| } | ||||
| 
 | ||||
| figure video { | ||||
|     width: 100%; | ||||
|     max-height: 450px; | ||||
| } | ||||
| 
 | ||||
| figcaption { | ||||
|     text-align: center; | ||||
|     font-style: italic; | ||||
|     padding-top: 10px; | ||||
| } | ||||
| 
 | ||||
| /* Screenshots */ | ||||
| 
 | ||||
| #screenshots { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| #screenshots img { | ||||
|     height: 190px; | ||||
|     margin: 3px; | ||||
|     border-radius: 5px; | ||||
|     filter: drop-shadow(2px 2px 2px #ddd); | ||||
| } | ||||
| 
 | ||||
| #screenshots .nowrap { | ||||
|     white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| /* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */ | ||||
| 
 | ||||
| .lightbox { | ||||
|     opacity: 0; | ||||
|     visibility: hidden; | ||||
|     position: fixed; | ||||
|     left:0; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     z-index: -1; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     transition: all 0.15s ease-in; | ||||
| } | ||||
| 
 | ||||
| .lightbox.show { | ||||
|     background-color: rgba(0,0,0, 0.75); | ||||
|     opacity: 1; | ||||
|     visibility: visible; | ||||
|     z-index: 1000; | ||||
| } | ||||
| 
 | ||||
| .lightbox img { | ||||
|     max-width: 90%; | ||||
|     max-height: 90%; | ||||
|     filter: drop-shadow(5px 5px 10px #222); | ||||
|     border-radius: 5px; | ||||
| } | ||||
| 
 | ||||
| .lightbox .close-lightbox { | ||||
|     cursor: pointer; | ||||
|     position: absolute; | ||||
|     top: 30px; | ||||
|     right: 30px; | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
| 
 | ||||
| .lightbox .close-lightbox::after, | ||||
| .lightbox .close-lightbox::before { | ||||
|     content: ''; | ||||
|     width: 3px; | ||||
|     height: 20px; | ||||
|     background-color: #ddd; | ||||
|     position: absolute; | ||||
|     border-radius: 5px; | ||||
|     transform: rotate(45deg); | ||||
| } | ||||
| 
 | ||||
| .lightbox .close-lightbox::before { | ||||
|     transform: rotate(-45deg); | ||||
| } | ||||
| 
 | ||||
| .lightbox .close-lightbox:hover::after, | ||||
| .lightbox .close-lightbox:hover::before { | ||||
|     background-color: #fff; | ||||
| } | ||||
| 
 | ||||
| /* Header */ | ||||
| 
 | ||||
| #header { | ||||
|     background: #338574; | ||||
|     height: 130px; | ||||
| } | ||||
| 
 | ||||
| #header #headerBox { | ||||
|     max-width: 900px; | ||||
|     margin: 0 auto; | ||||
|     padding: 0 10px; | ||||
| } | ||||
| 
 | ||||
| #header #logo { | ||||
|     margin-top: 23px; | ||||
|     float: left; | ||||
| } | ||||
| 
 | ||||
| #header #name { | ||||
|     float: left; | ||||
|     color: white; | ||||
|     font-size: 2.6em; | ||||
|     font-weight: 300; | ||||
|     margin: 35px 0 0 20px; | ||||
| } | ||||
| 
 | ||||
| #header ol { | ||||
|     list-style-type: none; | ||||
|     float: right; | ||||
|     margin-top: 80px; | ||||
| } | ||||
| 
 | ||||
| #header ol li { | ||||
|     display: inline-block; | ||||
|     margin: 0 10px; | ||||
|     font-weight: 400; | ||||
| } | ||||
| 
 | ||||
| #header ol li a, nav ol li a:visited { | ||||
|     color: white; | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| #header ol li a:hover { | ||||
|     text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| li { | ||||
|     padding: 4px 0; | ||||
|     margin: 4px 0; | ||||
|     font-size: 0.9em; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* Hide top menu SMALL SCREEN */ | ||||
| @media only screen and (max-width: 780px) { | ||||
|     #header ol { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB | 
| Before Width: | Height: | Size: 5.8 KiB | 
| Before Width: | Height: | Size: 4.4 KiB | 
| Before Width: | Height: | Size: 3.7 KiB | 
| Before Width: | Height: | Size: 297 KiB | 
| Before Width: | Height: | Size: 113 KiB | 
| Before Width: | Height: | Size: 227 KiB | 
| Before Width: | Height: | Size: 225 KiB | 
| Before Width: | Height: | Size: 128 KiB | 
| Before Width: | Height: | Size: 224 KiB | 
| Before Width: | Height: | Size: 270 KiB | 
| Before Width: | Height: | Size: 473 KiB | 
|  | @ -1,84 +0,0 @@ | |||
| 
 | ||||
| /* All the things */ | ||||
| 
 | ||||
| let currentUrl = window.location.hostname; | ||||
| if (window.location.port) { | ||||
|     currentUrl += ':' + window.location.port | ||||
| } | ||||
| 
 | ||||
| /* Screenshots */ | ||||
| const lightbox = document.getElementById("lightbox"); | ||||
| 
 | ||||
| const showScreenshotOverlay = (e, el, index) => { | ||||
|     lightbox.classList.add('show'); | ||||
|     document.addEventListener('keydown', nextScreenshotKeyboardListener); | ||||
|     return showScreenshot(e, index); | ||||
| }; | ||||
| 
 | ||||
| const showScreenshot = (e, index) => { | ||||
|     const actualIndex = resolveScreenshotIndex(index); | ||||
|     lightbox.innerHTML = '<div class="close-lightbox"></div>' + screenshots[actualIndex].innerHTML; | ||||
|     lightbox.querySelector('img').onclick = (e) => { return showScreenshot(e,actualIndex+1); }; | ||||
|     currentScreenshotIndex = actualIndex; | ||||
|     e.stopPropagation(); | ||||
|     return false; | ||||
| }; | ||||
| 
 | ||||
| const nextScreenshot = (e) => { | ||||
|     return showScreenshot(e, currentScreenshotIndex+1); | ||||
| }; | ||||
| 
 | ||||
| const previousScreenshot = (e) => { | ||||
|     return showScreenshot(e, currentScreenshotIndex-1); | ||||
| }; | ||||
| 
 | ||||
| const resolveScreenshotIndex = (index) => { | ||||
|     if (index < 0) { | ||||
|         return screenshots.length - 1; | ||||
|     } else if (index > screenshots.length - 1) { | ||||
|         return 0; | ||||
|     } | ||||
|     return index; | ||||
| }; | ||||
| 
 | ||||
| const hideScreenshotOverlay = (e) => { | ||||
|     lightbox.classList.remove('show'); | ||||
|     document.removeEventListener('keydown', nextScreenshotKeyboardListener); | ||||
| }; | ||||
| 
 | ||||
| const nextScreenshotKeyboardListener = (e) => { | ||||
|     switch (e.keyCode) { | ||||
|         case 37: | ||||
|             previousScreenshot(e); | ||||
|             break; | ||||
|         case 39: | ||||
|             nextScreenshot(e); | ||||
|             break; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| let currentScreenshotIndex = 0; | ||||
| const screenshots = [...document.querySelectorAll("#screenshots a")]; | ||||
| screenshots.forEach((el, index) => { | ||||
|     el.onclick = (e) => { return showScreenshotOverlay(e, el, index); }; | ||||
| }); | ||||
| 
 | ||||
| lightbox.onclick = hideScreenshotOverlay; | ||||
| 
 | ||||
| // Add anchor links
 | ||||
| document.querySelectorAll('.anchor').forEach((el) => { | ||||
|     if (el.hasAttribute('id')) { | ||||
|         const id = el.getAttribute('id'); | ||||
|         const anchor = document.createElement('a'); | ||||
|         anchor.innerHTML = `<a href="#${id}" class="anchorLink">#</a>`; | ||||
|         el.appendChild(anchor); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| // Change ntfy.sh url and protocol to match self-hosted one
 | ||||
| document.querySelectorAll('.ntfyUrl').forEach((el) => { | ||||
|     el.innerHTML = currentUrl; | ||||
| }); | ||||
| document.querySelectorAll('.ntfyProtocol').forEach((el) => { | ||||
|     el.innerHTML = window.location.protocol + "//"; | ||||
| }); | ||||