mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
Docs: add topic generator
This commit is contained in:
+39
-1
@@ -1,7 +1,7 @@
|
|||||||
# Publishing
|
# Publishing
|
||||||
Publishing messages can be done via HTTP PUT/POST or via the [ntfy CLI](subscribe/cli.md#publish-messages) ([install instructions](install.md)).
|
Publishing messages can be done via HTTP PUT/POST or via the [ntfy CLI](subscribe/cli.md#publish-messages) ([install instructions](install.md)).
|
||||||
Topics are created on the fly by subscribing or publishing to them. Because there is no sign-up, **the topic is essentially a password**, so pick
|
Topics are created on the fly by subscribing or publishing to them. Because there is no sign-up, **the topic is essentially a password**, so pick
|
||||||
something that's not easily guessable.
|
something that's not easily guessable (see [picking a topic](#picking-a-topic) for a handy topic name generator).
|
||||||
|
|
||||||
Here's an example showing how to publish a simple message using a POST request:
|
Here's an example showing how to publish a simple message using a POST request:
|
||||||
|
|
||||||
@@ -308,6 +308,44 @@ an [external image attachment](#attach-file-from-a-url) and [email publishing](#
|
|||||||
<figcaption>Notification using a click action, a user action, with an external image attachment and forwarded via email</figcaption>
|
<figcaption>Notification using a click action, a user action, with an external image attachment and forwarded via email</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
## Picking a topic
|
||||||
|
Since there is no sign-up, **the topic is essentially a password**, so pick something that's not easily guessable. Topic names may
|
||||||
|
only contain letters, numbers, underscores and dashes (`[-_A-Za-z0-9]`), and may be up to 64 characters long.
|
||||||
|
|
||||||
|
Not sure what to pick? Type a name below and the generator will add a random, hard-to-guess suffix for you. Everything happens locally in your browser:
|
||||||
|
|
||||||
|
<div id="tg-widget" class="tg-generator">
|
||||||
|
<div class="tg-header">
|
||||||
|
<span class="tg-title">Topic name generator</span>
|
||||||
|
<button type="button" id="tg-reroll" class="tg-reset" title="Generate a new random suffix">Regenerate suffix</button>
|
||||||
|
</div>
|
||||||
|
<div class="tg-body">
|
||||||
|
<div class="tg-left">
|
||||||
|
<div class="tg-field">
|
||||||
|
<label for="tg-input">Type a topic name</label>
|
||||||
|
<input type="text" id="tg-input" placeholder="e.g. backups, alerts, phil-home" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="tg-note">Spaces and characters other than letters, numbers, <code>-</code> and <code>_</code> are removed automatically as you type. Names are capped at 64 characters.</div>
|
||||||
|
</div>
|
||||||
|
<div class="tg-right">
|
||||||
|
<div class="tg-output-row">
|
||||||
|
<span class="tg-output-label">Your topic:</span>
|
||||||
|
<div class="tg-output-line">
|
||||||
|
<pre class="tg-output" id="tg-output-name"></pre>
|
||||||
|
<button type="button" class="tg-btn-copy" data-copy="tg-output-name" title="Copy to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tg-output-row">
|
||||||
|
<span class="tg-output-label">Your topic URL:</span>
|
||||||
|
<div class="tg-output-line">
|
||||||
|
<pre class="tg-output" id="tg-output-url">https://ntfy.sh/</pre>
|
||||||
|
<button type="button" class="tg-btn-copy" data-copy="tg-output-url" title="Copy to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Message title
|
## Message title
|
||||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
|
|||||||
Vendored
+235
@@ -0,0 +1,235 @@
|
|||||||
|
/* Topic name generator (Publishing page) */
|
||||||
|
/* Styled to mirror the config generator (header + left form / right output panels). */
|
||||||
|
|
||||||
|
.tg-generator {
|
||||||
|
margin: 16px 0 24px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header (matches .cg-modal-header) */
|
||||||
|
.tg-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-reset {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #777;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-reset:hover {
|
||||||
|
color: #333;
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body: left (form) + right (output), matches .cg-modal-body */
|
||||||
|
.tg-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-left {
|
||||||
|
flex: 1;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
padding: 16px 18px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One output per block: label on its own line, then value field + copy button */
|
||||||
|
.tg-output-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-output-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form field (matches .cg-field) */
|
||||||
|
.tg-field > label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-field input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-field input[type="text"]:focus {
|
||||||
|
border-color: var(--md-primary-fg-color);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-note code {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-output-label {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button (matches .cg-btn-copy) */
|
||||||
|
.tg-btn-copy {
|
||||||
|
background: none;
|
||||||
|
color: #777;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-btn-copy:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Output block (matches .cg-output-wrap pre). Scoped under .tg-generator so the margin
|
||||||
|
reset beats the theme's .md-typeset pre rule, which otherwise adds a stray top margin. */
|
||||||
|
.tg-generator .tg-output {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 9px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: var(--md-default-fg-color);
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: var(--md-code-font-family, monospace);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
body[data-md-color-scheme="slate"] .tg-generator {
|
||||||
|
background: #1e1e2e;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-header {
|
||||||
|
border-bottom-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-title {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-reset {
|
||||||
|
border-color: #555;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-reset:hover {
|
||||||
|
border-color: #888;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-left {
|
||||||
|
border-right-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-field > label,
|
||||||
|
body[data-md-color-scheme="slate"] .tg-output-label {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-btn-copy {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-btn-copy:hover {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-field input[type="text"] {
|
||||||
|
background: #2a2a3a;
|
||||||
|
border-color: #555;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-note {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-output {
|
||||||
|
background: #161620;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: stack panels like the config generator does on mobile */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.tg-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-left {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-md-color-scheme="slate"] .tg-left {
|
||||||
|
border-bottom-color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+121
@@ -0,0 +1,121 @@
|
|||||||
|
// Topic name generator for the ntfy docs
|
||||||
|
//
|
||||||
|
// A tiny helper that lives on the "Publishing" page. The user types a memorable
|
||||||
|
// prefix (e.g. "backups"), and the widget appends a random, hard-to-guess suffix
|
||||||
|
// (e.g. "backups-x7Kp2mQ9"). The result is a valid, unguessable topic name.
|
||||||
|
//
|
||||||
|
// Topic names on the server must match ^[-_A-Za-z0-9]{1,64}$ (see server.go), so as
|
||||||
|
// the user types we strip anything that isn't allowed (spaces, slashes, punctuation,
|
||||||
|
// emoji, ...) live and cap the whole thing at 64 characters. The random suffix is
|
||||||
|
// generated once on load and can be re-rolled with the "Regenerate suffix" button.
|
||||||
|
(function () {
|
||||||
|
// Allowed topic characters per the server regex ^[-_A-Za-z0-9]{1,64}$
|
||||||
|
const ALLOWED = /[^-_A-Za-z0-9]/g;
|
||||||
|
const MAX_LEN = 64;
|
||||||
|
|
||||||
|
// Suffix alphabet: full base62 (letters + digits). We deliberately keep look-alikes
|
||||||
|
// (0/O, l/1) for maximum entropy -- this is a generated suffix, not something typed by
|
||||||
|
// hand. Hyphen/underscore are excluded so the "-" separator stays visually clear.
|
||||||
|
const SUFFIX_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const SUFFIX_LEN = 10;
|
||||||
|
|
||||||
|
// randomSuffix returns a cryptographically random string from SUFFIX_ALPHABET.
|
||||||
|
// It uses rejection sampling to avoid the modulo bias that a plain `byte % 62` would
|
||||||
|
// introduce (256 is not a multiple of 62), keeping every character equally likely.
|
||||||
|
function randomSuffix() {
|
||||||
|
const n = SUFFIX_ALPHABET.length;
|
||||||
|
const limit = Math.floor(256 / n) * n; // largest multiple of n that fits in a byte
|
||||||
|
const buf = new Uint8Array(1);
|
||||||
|
let out = "";
|
||||||
|
while (out.length < SUFFIX_LEN) {
|
||||||
|
crypto.getRandomValues(buf);
|
||||||
|
if (buf[0] < limit) {
|
||||||
|
out += SUFFIX_ALPHABET[buf[0] % n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitize strips everything that isn't a valid topic character.
|
||||||
|
function sanitize(value) {
|
||||||
|
return value.replace(ALLOWED, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTopicGenerator() {
|
||||||
|
const root = document.getElementById("tg-widget");
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const input = root.querySelector("#tg-input");
|
||||||
|
const outputName = root.querySelector("#tg-output-name");
|
||||||
|
const outputUrl = root.querySelector("#tg-output-url");
|
||||||
|
const reroll = root.querySelector("#tg-reroll");
|
||||||
|
|
||||||
|
let suffix = randomSuffix();
|
||||||
|
|
||||||
|
// update recomputes the live preview from the (sanitized) input + current suffix.
|
||||||
|
function update() {
|
||||||
|
// Sanitize in place so the user sees disallowed characters disappear as they type.
|
||||||
|
const cleaned = sanitize(input.value);
|
||||||
|
if (cleaned !== input.value) {
|
||||||
|
const pos = input.selectionStart - (input.value.length - cleaned.length);
|
||||||
|
// Reassigning .value and setSelectionRange make the browser scroll the field into
|
||||||
|
// view (there is no preventScroll option for setSelectionRange), which jumps the
|
||||||
|
// whole page. Capture the scroll position and restore it afterwards.
|
||||||
|
const scrollX = window.scrollX;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
input.value = cleaned;
|
||||||
|
// Best-effort caret restore so removing a bad char doesn't jump the cursor to the end.
|
||||||
|
try { input.setSelectionRange(pos, pos); } catch { /* ignore */ }
|
||||||
|
window.scrollTo(scrollX, scrollY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose "<prefix>-<suffix>", capped at the 64-char topic limit. With no prefix,
|
||||||
|
// fall back to just the random suffix so the output is always a valid topic.
|
||||||
|
let topic;
|
||||||
|
if (cleaned === "") {
|
||||||
|
topic = suffix;
|
||||||
|
} else {
|
||||||
|
const maxPrefix = MAX_LEN - suffix.length - 1; // room for "-" + suffix
|
||||||
|
const prefix = cleaned.slice(0, Math.max(0, maxPrefix));
|
||||||
|
topic = prefix === "" ? suffix : prefix + "-" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputName.textContent = topic;
|
||||||
|
outputUrl.textContent = "https://ntfy.sh/" + topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", update);
|
||||||
|
reroll.addEventListener("click", function () {
|
||||||
|
suffix = randomSuffix();
|
||||||
|
update();
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy buttons: copy the target output and briefly swap the clipboard icon for a checkmark,
|
||||||
|
// mirroring the config generator's copy button behavior.
|
||||||
|
const copyIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>";
|
||||||
|
const checkIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>";
|
||||||
|
root.querySelectorAll(".tg-btn-copy").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
const target = root.querySelector("#" + btn.dataset.copy);
|
||||||
|
if (!target || !target.textContent) return;
|
||||||
|
navigator.clipboard.writeText(target.textContent).then(function () {
|
||||||
|
btn.innerHTML = checkIcon;
|
||||||
|
btn.style.color = "var(--md-primary-fg-color)";
|
||||||
|
setTimeout(function () {
|
||||||
|
btn.innerHTML = copyIcon;
|
||||||
|
btn.style.color = "";
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", initTopicGenerator);
|
||||||
|
} else {
|
||||||
|
initTopicGenerator();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -44,9 +44,11 @@ extra_javascript:
|
|||||||
- static/js/extra.js
|
- static/js/extra.js
|
||||||
- static/js/bcrypt.js
|
- static/js/bcrypt.js
|
||||||
- static/js/config-generator.js
|
- static/js/config-generator.js
|
||||||
|
- static/js/topic-generator.js
|
||||||
extra_css:
|
extra_css:
|
||||||
- static/css/extra.css
|
- static/css/extra.css
|
||||||
- static/css/config-generator.css
|
- static/css/config-generator.css
|
||||||
|
- static/css/topic-generator.css
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
|
|||||||
Reference in New Issue
Block a user