diff --git a/.env.example b/.env.example
index 6c96c15..471a3c4 100644
--- a/.env.example
+++ b/.env.example
@@ -14,6 +14,7 @@ PUBLIC_SEARCH=no # yes | no — enable the search box (req
PUBLIC_LABELS= # comma-separated discussion labels (e.g., bug,feature,question)
PUBLIC_NOSTRCONNECT_RELAYS= # comma-separated relays for nostrconnect:// (bunker) login; defaults to bucket.coracle.social,relay.primal.net
PUBLIC_BLOSSOM_URL= # Blossom server URL for uploads (e.g., https://blossom.primal.net)
+PUBLIC_EXTERNAL_CLIENT= # external client for profiles/events, {e} is the bech32 entity; default https://njump.me/{e}
# Theme color overrides — quote the value, a leading # is read as a comment otherwise
PUBLIC_ACCENT_COLOR= # override the accent color (default #e32a6d), e.g. "#00ff00"; hover shade derived
PUBLIC_SECONDARY_COLOR= # override the secondary color (default #ffaf25), e.g. "#0000ff"; hover shade derived
diff --git a/README.md b/README.md
index bb279b2..99566c5 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Squalk is configured entirely through environment variables (all prefixed `PUBLI
| `PUBLIC_SEARCH` | no | `no` | `yes` to show a search box at the top of the homepage. Requires a relay with NIP-50 search support. |
| `PUBLIC_LABELS` | no | — | Comma-separated discussion labels offered when composing, e.g. `bug,feature,question`. |
| `PUBLIC_BLOSSOM_URL` | no | — | Blossom server URL used for media uploads, e.g. `https://blossom.primal.net`. Uploads are disabled when unset. |
+| `PUBLIC_EXTERNAL_CLIENT` | no | `https://njump.me/{e}` | URL template of the external Nostr client used to open profiles and events outside the forum. `{e}` is replaced with the bech32 entity (npub, nevent, naddr), e.g. `https://nostr.at/{e}`. |
| `PUBLIC_ACCENT_COLOR` | no | `#e32a6d` | Override the accent (primary) color. Quote the value (`"#00ff00"`) — an unquoted leading `#` is read as a comment. The hover shade is derived automatically. |
| `PUBLIC_SECONDARY_COLOR` | no | `#ffaf25` | Override the secondary color. Same quoting rule and derived hover shade as above. |
diff --git a/src/lib/components/PostContent.svelte b/src/lib/components/PostContent.svelte
index 2dbd18b..66a0db1 100644
--- a/src/lib/components/PostContent.svelte
+++ b/src/lib/components/PostContent.svelte
@@ -2,6 +2,7 @@
import { loadNostrUser, type NostrUser } from "$lib/gadgets";
import * as nip19 from "@nostr/tools/nip19";
import { profilePath } from "$lib/forum/profiles";
+ import { externalLink } from "$lib/config";
import {
resolveThreadRef,
threadRefHref,
@@ -311,7 +312,7 @@
const entity = dest.slice(6).toLowerCase();
try {
nip19.decode(entity);
- return { type: "link", href: `https://njump.me/${entity}`, label };
+ return { type: "link", href: externalLink(entity), label };
} catch {
return { type: "text", value: label };
}
@@ -642,7 +643,7 @@
>
{:else}
{inline.label}`,
);
+
+// External Nostr client used to open events and profiles that live outside
+// the forum. {e} is replaced with the bech32 entity (npub, nevent, naddr...).
+const DEFAULT_EXTERNAL_CLIENT = "https://njump.me/{e}";
+export const EXTERNAL_CLIENT = env.PUBLIC_EXTERNAL_CLIENT?.includes("{e}")
+ ? env.PUBLIC_EXTERNAL_CLIENT
+ : DEFAULT_EXTERNAL_CLIENT;
+export const EXTERNAL_CLIENT_NAME = (() => {
+ try {
+ return new URL(EXTERNAL_CLIENT.replace("{e}", "x")).hostname;
+ } catch {
+ return "external client";
+ }
+})();
+export function externalLink(entity: string): string {
+ return EXTERNAL_CLIENT.replace("{e}", entity);
+}
diff --git a/src/lib/linkify.ts b/src/lib/linkify.ts
index abc3117..4ffbe36 100644
--- a/src/lib/linkify.ts
+++ b/src/lib/linkify.ts
@@ -1,5 +1,5 @@
import * as nip19 from "@nostr/tools/nip19";
-import { RELAY_URL } from "./config";
+import { RELAY_URL, externalLink } from "./config";
// Curated TLD list: gTLDs, popular new gTLDs, common ccTLDs
const TLDS = [
@@ -157,7 +157,7 @@ function decodeEntity(entity: string): ChatToken | null {
return {
type: "entity",
entity,
- href: `https://njump.me/${entity}`,
+ href: externalLink(entity),
label: shortNostrEntity(entity),
id: decoded.type === "note" ? decoded.data : decoded.data.id,
};
@@ -165,7 +165,7 @@ function decodeEntity(entity: string): ChatToken | null {
return {
type: "entity",
entity,
- href: `https://njump.me/${entity}`,
+ href: externalLink(entity),
label: shortNostrEntity(entity),
};
} catch {
diff --git a/src/routes/profile/[id]/+page.svelte b/src/routes/profile/[id]/+page.svelte
index 42b7ebc..b23f953 100644
--- a/src/routes/profile/[id]/+page.svelte
+++ b/src/routes/profile/[id]/+page.svelte
@@ -1,6 +1,6 @@