Skip to content

Commit 317621c

Browse files
author
Philipp Heckel
committed
Styling
1 parent 39574c9 commit 317621c

10 files changed

+252
-135
lines changed

assets/favicon.xcf

9.68 KB
Binary file not shown.

examples/example_eventsource_sse.html

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<meta charset="UTF-8">
45
<title>ntfy.sh: EventSource Example</title>
56
<style>
67
body { font-size: 1.2em; line-height: 130%; }

server/index.html

+35-135
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<title>ntfy.sh</title>
5-
<style>
6-
body { font-size: 1.2em; line-height: 130%; }
7-
#error { color: darkred; font-style: italic; }
8-
#main { max-width: 900px; margin: 0 auto 50px auto; }
9-
</style>
4+
<meta charset="UTF-8">
5+
6+
<title>ntfy.sh | simple HTTP-based pub-sub</title>
7+
<link rel="stylesheet" href="static/css/app.css" type="text/css">
8+
9+
<!-- Mobile view -->
10+
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
11+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
12+
<meta name="HandheldFriendly" content="true">
13+
14+
<!-- Mobile browsers, background color -->
15+
<meta name="theme-color" content="#004c79">
16+
<meta name="msapplication-navbutton-color" content="#004c79">
17+
<meta name="apple-mobile-web-app-status-bar-style" content="#004c79">
18+
19+
<!-- Favicon, see favicon.io -->
20+
<link rel="icon" type="image/png" href="static/img/favicon.png">
21+
22+
<!-- Previews in Google, Slack, WhatsApp, etc. -->
23+
<meta property="og:type" content="website" />
24+
<meta property="og:locale" content="en_US" />
25+
<meta property="og:site_name" content="ntfy.sh" />
26+
<meta property="og:title" content="ntfy.sh | simple HTTP-based pub-sub" />
27+
<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." />
28+
<meta property="og:image" content="/static/img/ntfy.png" />
29+
<meta property="og:url" content="https://ntfy.sh" />
1030
</head>
1131
<body>
1232
<div id="main">
1333
<h1>ntfy.sh - simple HTTP-based pub-sub</h1>
1434
<p>
15-
<b>ntfy</b> (pronounce: <i>notify</i>) is a simple <b>HTTP-based pub-sub notification service and tool</b>.
16-
It allows you to send <b>desktop notifications via scripts</b>, entirely <b>without signup or cost</b>.
35+
<b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based pub-sub notification service and tool.
36+
It allows you to send <b>desktop notifications via scripts from any computer</b>, entirely <b>without signup or cost</b>.
1737
It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own.
1838
</p>
1939
<p id="error"></p>
@@ -37,151 +57,31 @@ <h3>Subscribe via web</h3>
3757
<p>
3858
<label for="topicField">Topic ID:</label>
3959
<input type="text" id="topicField" placeholder="Letters, numbers, _ and -" pattern="[-_A-Za-z]{1,64}" autofocus />
40-
<input type="submit" id="subscribeButton" value="Subscribe topic" />
60+
<input type="submit" id="subscribeButton" value="Subscribe" />
4161
</p>
4262
</form>
4363
<p id="topicsHeader">Subscribed topics:</p>
4464
<ul id="topicsList"></ul>
4565

4666
<h3>Subscribe via your app, or via the CLI</h3>
47-
<tt>
67+
<code>
4868
curl -s ntfy.sh/mytopic/raw # one message per line (\n are replaced with a space)<br/>
4969
curl -s ntfy.sh/mytopic/json # one JSON message per line<br/>
5070
curl -s ntfy.sh/mytopic/sse # server-sent events (SSE) stream
51-
</tt>
71+
</code>
5272

53-
<h3>Publishing messages</h3>
73+
<h2>Publishing messages</h2>
5474
<p>
5575
Publishing messages can be done via PUT or POST using. Here's an example using <tt>curl</tt>:
5676
</p>
57-
<tt>
77+
<code>
5878
curl -d "long process is done" ntfy.sh/mytopic
59-
</tt>
79+
</code>
6080
<p>
6181
Messages published to a non-existing topic or a topic without subscribers will not be delivered later.
6282
There is (currently) no buffering of any kind. If you're not listening, the message won't be delivered.
6383
</p>
6484
</div>
65-
66-
<script type="text/javascript">
67-
let topics = {};
68-
69-
const topicsHeader = document.getElementById("topicsHeader");
70-
const topicsList = document.getElementById("topicsList");
71-
const topicField = document.getElementById("topicField");
72-
const subscribeButton = document.getElementById("subscribeButton");
73-
const subscribeForm = document.getElementById("subscribeForm");
74-
const errorField = document.getElementById("error");
75-
76-
const subscribe = (topic) => {
77-
if (Notification.permission !== "granted") {
78-
Notification.requestPermission().then((permission) => {
79-
if (permission === "granted") {
80-
subscribeInternal(topic, 0);
81-
} else {
82-
showNotificationDeniedError();
83-
}
84-
});
85-
} else {
86-
subscribeInternal(topic, 0);
87-
}
88-
};
89-
90-
const subscribeInternal = (topic, delaySec) => {
91-
setTimeout(() => {
92-
// Render list entry
93-
let topicEntry = document.getElementById(`topic-${topic}`);
94-
if (!topicEntry) {
95-
topicEntry = document.createElement('li');
96-
topicEntry.id = `topic-${topic}`;
97-
topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
98-
topicsList.appendChild(topicEntry);
99-
}
100-
topicsHeader.style.display = '';
101-
102-
// Open event source
103-
let eventSource = new EventSource(`${topic}/sse`);
104-
eventSource.onopen = () => {
105-
topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
106-
delaySec = 0; // Reset on successful connection
107-
};
108-
eventSource.onerror = (e) => {
109-
const newDelaySec = (delaySec + 5 <= 15) ? delaySec + 5 : 15;
110-
topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button disabled="disabled">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
111-
eventSource.close()
112-
subscribeInternal(topic, newDelaySec);
113-
};
114-
eventSource.onmessage = (e) => {
115-
const event = JSON.parse(e.data);
116-
new Notification(event.message);
117-
};
118-
topics[topic] = eventSource;
119-
localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
120-
}, delaySec * 1000);
121-
};
122-
123-
const unsubscribe = (topic) => {
124-
topics[topic].close();
125-
delete topics[topic];
126-
localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
127-
document.getElementById(`topic-${topic}`).remove();
128-
if (Object.keys(topics).length === 0) {
129-
topicsHeader.style.display = 'none';
130-
}
131-
};
132-
133-
const test = (topic) => {
134-
fetch(`/${topic}`, {
135-
method: 'PUT',
136-
body: `This is a test notification for topic ${topic}!`
137-
})
138-
};
139-
140-
const showError = (msg) => {
141-
errorField.innerHTML = msg;
142-
topicField.disabled = true;
143-
subscribeButton.disabled = true;
144-
};
145-
146-
const showBrowserIncompatibleError = () => {
147-
showError("Your browser is not compatible to use the web-based desktop notifications.");
148-
};
149-
150-
const showNotificationDeniedError = () => {
151-
showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications.");
152-
};
153-
154-
subscribeForm.onsubmit = function () {
155-
if (!topicField.value) {
156-
return false;
157-
}
158-
subscribe(topicField.value);
159-
topicField.value = "";
160-
return false;
161-
};
162-
163-
// Disable Web UI if notifications of EventSource are not available
164-
if (!window["Notification"] || !window["EventSource"]) {
165-
showBrowserIncompatibleError();
166-
} else if (Notification.permission === "denied") {
167-
showNotificationDeniedError();
168-
}
169-
170-
// Reset UI
171-
topicField.value = "";
172-
173-
// Restore topics
174-
const storedTopics = localStorage.getItem('topics');
175-
if (storedTopics && Notification.permission === "granted") {
176-
const storedTopicsArray = JSON.parse(storedTopics)
177-
storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); });
178-
if (storedTopicsArray.length === 0) {
179-
topicsHeader.style.display = 'none';
180-
}
181-
} else {
182-
topicsHeader.style.display = 'none';
183-
}
184-
</script>
185-
85+
<script src="static/js/app.js"></script>
18686
</body>
18787
</html>

server/server.go

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"bytes"
5+
"embed"
56
_ "embed" // required for go:embed
67
"encoding/json"
78
"fmt"
@@ -51,10 +52,14 @@ var (
5152
jsonRegex = regexp.MustCompile(`^/[^/]+/json$`)
5253
sseRegex = regexp.MustCompile(`^/[^/]+/sse$`)
5354
rawRegex = regexp.MustCompile(`^/[^/]+/raw$`)
55+
staticRegex = regexp.MustCompile(`^/static/.+`)
5456

5557
//go:embed "index.html"
5658
indexSource string
5759

60+
//go:embed static
61+
webStaticFs embed.FS
62+
5863
errHTTPNotFound = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)}
5964
errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)}
6065
)
@@ -123,6 +128,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
123128
}
124129
if r.Method == http.MethodGet && r.URL.Path == "/" {
125130
return s.handleHome(w, r)
131+
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
132+
return s.handleStatic(w, r)
126133
} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
127134
return s.handleSubscribeJSON(w, r)
128135
} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
@@ -241,6 +248,11 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error {
241248
return nil
242249
}
243250

251+
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
252+
http.FileServer(http.FS(webStaticFs)).ServeHTTP(w, r)
253+
return nil
254+
}
255+
244256
func (s *Server) createTopic(id string) *topic {
245257
s.mu.Lock()
246258
defer s.mu.Unlock()

server/static/css/app.css

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* general styling */
2+
3+
html, body {
4+
font-family: 'Lato', sans-serif;
5+
color: #333;
6+
font-size: 1.1em;
7+
}
8+
9+
a {
10+
color: #39005a;
11+
}
12+
13+
a:hover {
14+
text-decoration: none;
15+
}
16+
17+
h1 {
18+
margin-top: 25px;
19+
margin-bottom: 18px;
20+
font-size: 2.5em;
21+
}
22+
23+
h2 {
24+
margin-top: 20px;
25+
margin-bottom: 5px;
26+
font-size: 1.8em;
27+
}
28+
29+
h3 {
30+
margin-top: 20px;
31+
margin-bottom: 5px;
32+
font-size: 1.3em;
33+
}
34+
35+
p {
36+
margin-top: 0;
37+
font-size: 1.1em;
38+
}
39+
40+
tt {
41+
background: #eee;
42+
padding: 2px 7px;
43+
border-radius: 3px;
44+
}
45+
46+
code {
47+
display: block;
48+
background: #eee;
49+
font-family: monospace;
50+
padding: 20px;
51+
border-radius: 3px;
52+
}
53+
54+
/* Lato font (OFL), https://fonts.google.com/specimen/Lato#about,
55+
embedded with the help of https://google-webfonts-helper.herokuapp.com/fonts/lato?subsets=latin */
56+
57+
@font-face {
58+
font-family: 'Lato';
59+
font-style: normal;
60+
font-weight: 400;
61+
src: local(''),
62+
url('../font/lato-v17-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
63+
url('../font/lato-v17-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
64+
}
65+
66+
/* Main page */
67+
68+
#main {
69+
max-width: 900px;
70+
margin: 0 auto 50px auto;
71+
}
72+
73+
#error {
74+
color: darkred;
75+
font-style: italic;
76+
}
Binary file not shown.
Binary file not shown.

server/static/img/favicon.png

1.23 KB
Loading

server/static/img/ntfy.png

1.23 KB
Loading

0 commit comments

Comments
 (0)