diff --git a/docs/tutorial.md b/docs/tutorial.md
index d8b58a0..cffba96 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -592,7 +592,7 @@ GitRepublic implements NIP-34 for repository announcements. Key event types:
- **Kind 1621**: Issue
- **Kind 1641**: Ownership transfer
-See the [NIP-34 specification](https://github.com/nostr-protocol/nips/blob/master/34.md) for full details.
+See the [NIP-34 specification](/docs/nip34/spec) for full details.
### NIP-98 HTTP Authentication
diff --git a/src/app.css b/src/app.css
index 0a00a43..ef03d7d 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1170,8 +1170,10 @@ pre code {
background: var(--bg-secondary);
}
+label.filter-checkbox,
.filter-checkbox {
- display: flex;
+ display: flex !important;
+ flex-direction: row !important;
align-items: center;
gap: 0.5rem;
cursor: pointer;
@@ -1180,11 +1182,21 @@ pre code {
font-size: 0.9rem;
}
+label.filter-checkbox input[type="checkbox"],
.filter-checkbox input[type="checkbox"] {
cursor: pointer;
width: 1.1rem;
height: 1.1rem;
accent-color: var(--accent);
+ flex-shrink: 0;
+ margin: 0;
+ display: block;
+}
+
+label.filter-checkbox > span,
+.filter-checkbox > span {
+ display: inline;
+ line-height: 1.5;
}
.repos-list {
diff --git a/src/routes/docs/nip34/+page.svelte b/src/routes/docs/nip34/+page.svelte
index 30cbf65..cdd9c66 100644
--- a/src/routes/docs/nip34/+page.svelte
+++ b/src/routes/docs/nip34/+page.svelte
@@ -42,7 +42,8 @@
diff --git a/src/routes/docs/nip34/spec/+page.server.ts b/src/routes/docs/nip34/spec/+page.server.ts
new file mode 100644
index 0000000..80fc543
--- /dev/null
+++ b/src/routes/docs/nip34/spec/+page.server.ts
@@ -0,0 +1,20 @@
+/**
+ * Server-side loader for NIP-34 specification reference
+ */
+
+import { readFile } from 'fs/promises';
+import { join } from 'path';
+import type { PageServerLoad } from './$types';
+import logger from '$lib/services/logger.js';
+
+export const load: PageServerLoad = async () => {
+ try {
+ // Read NIP-34 specification from docs/34.md
+ const filePath = join(process.cwd(), 'docs', '34.md');
+ const content = await readFile(filePath, 'utf-8');
+ return { content };
+ } catch (error) {
+ logger.error({ error }, 'Error loading NIP-34 specification');
+ return { content: null, error: 'Failed to load NIP-34 specification' };
+ }
+};
diff --git a/src/routes/docs/nip34/spec/+page.svelte b/src/routes/docs/nip34/spec/+page.svelte
new file mode 100644
index 0000000..3b1a7cd
--- /dev/null
+++ b/src/routes/docs/nip34/spec/+page.svelte
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+ {#if loading}
+ Loading specification...
+ {:else if error}
+ {error}
+ {:else}
+
+ {@html content}
+
+ {/if}
+
+
+
+
diff --git a/src/routes/signup/+page.svelte b/src/routes/signup/+page.svelte
index 9797c00..bc01fc5 100644
--- a/src/routes/signup/+page.svelte
+++ b/src/routes/signup/+page.svelte
@@ -20,6 +20,8 @@
let cloneUrls = $state(['']);
let webUrls = $state(['']);
let maintainers = $state(['']);
+ let relays = $state(['']);
+ let blossoms = $state(['']);
let tags = $state(['']);
let documentation = $state(['']);
let alt = $state('');
@@ -96,6 +98,34 @@
maintainers = newMaintainers;
}
+ function addRelay() {
+ relays = [...relays, ''];
+ }
+
+ function removeRelay(index: number) {
+ relays = relays.filter((_, i) => i !== index);
+ }
+
+ function updateRelay(index: number, value: string) {
+ const newRelays = [...relays];
+ newRelays[index] = value;
+ relays = newRelays;
+ }
+
+ function addBlossom() {
+ blossoms = [...blossoms, ''];
+ }
+
+ function removeBlossom(index: number) {
+ blossoms = blossoms.filter((_, i) => i !== index);
+ }
+
+ function updateBlossom(index: number, value: string) {
+ const newBlossoms = [...blossoms];
+ newBlossoms[index] = value;
+ blossoms = newBlossoms;
+ }
+
function addTag() {
tags = [...tags, ''];
}
@@ -603,6 +633,34 @@
}
maintainers = maintainersList.length > 0 ? maintainersList : [''];
+ // Extract relays
+ const relaysList: string[] = [];
+ for (const tag of event.tags) {
+ if (tag[0] === 'relays') {
+ for (let i = 1; i < tag.length; i++) {
+ const relay = tag[i];
+ if (relay && typeof relay === 'string' && relay.trim()) {
+ relaysList.push(relay.trim());
+ }
+ }
+ }
+ }
+ relays = relaysList.length > 0 ? relaysList : [''];
+
+ // Extract blossoms
+ const blossomsList: string[] = [];
+ for (const tag of event.tags) {
+ if (tag[0] === 'blossoms') {
+ for (let i = 1; i < tag.length; i++) {
+ const blossom = tag[i];
+ if (blossom && typeof blossom === 'string' && blossom.trim()) {
+ blossomsList.push(blossom.trim());
+ }
+ }
+ }
+ }
+ blossoms = blossomsList.length > 0 ? blossomsList : [''];
+
// Extract tags/labels
const tagsList: string[] = [];
for (const tag of event.tags) {
@@ -612,15 +670,90 @@
}
tags = tagsList.length > 0 ? tagsList : [''];
- // Extract documentation - handle both formats
+ // Extract documentation - handle relay hints correctly
+ // Only treat values as multiple entries if they are in the same format
+ // If a value looks like a relay URL (wss:// or ws://), it's a relay hint for the previous value
const docsList: string[] = [];
+ const isRelayUrl = (value: string): boolean => {
+ return typeof value === 'string' && (value.startsWith('wss://') || value.startsWith('ws://'));
+ };
+
+ const getDocFormat = (value: string): string | null => {
+ // Check if it's naddr format (starts with naddr1)
+ if (value.startsWith('naddr1')) return 'naddr';
+ // Check if it's kind:pubkey:identifier format
+ if (/^\d+:[0-9a-f]{64}:[a-zA-Z0-9_-]+$/.test(value)) return 'kind:pubkey:identifier';
+ return null;
+ };
+
for (const tag of event.tags) {
if (tag[0] === 'documentation') {
- for (let i = 1; i < tag.length; i++) {
- const doc = tag[i];
- if (doc && typeof doc === 'string' && doc.trim()) {
- docsList.push(doc.trim());
+ let i = 1;
+
+ while (i < tag.length) {
+ const value = tag[i];
+ if (!value || typeof value !== 'string' || !value.trim()) {
+ i++;
+ continue;
+ }
+
+ const trimmed = value.trim();
+
+ // Skip relay URLs (they're hints, not entries)
+ if (isRelayUrl(trimmed)) {
+ i++;
+ continue;
+ }
+
+ // Check if this is a documentation reference
+ const format = getDocFormat(trimmed);
+ if (!format) {
+ i++;
+ continue; // Skip invalid formats
+ }
+
+ // Check if next value is a relay URL (hint for this entry)
+ const nextValue = i + 1 < tag.length ? tag[i + 1] : null;
+ if (nextValue && typeof nextValue === 'string' && isRelayUrl(nextValue.trim())) {
+ // Current value has a relay hint - store just the doc reference, skip the relay
+ docsList.push(trimmed);
+ i += 2; // Skip both the doc and the relay hint
+ continue;
}
+
+ // Check if we have multiple entries in the same format
+ // Collect all consecutive entries of the same format
+ const sameFormatEntries: string[] = [trimmed];
+ let j = i + 1;
+ while (j < tag.length) {
+ const nextVal = tag[j];
+ if (!nextVal || typeof nextVal !== 'string' || !nextVal.trim()) {
+ j++;
+ continue;
+ }
+
+ const nextTrimmed = nextVal.trim();
+
+ // Stop if we hit a relay URL (it's a hint for the previous entry)
+ if (isRelayUrl(nextTrimmed)) {
+ break;
+ }
+
+ // Check if it's the same format
+ const nextFormat = getDocFormat(nextTrimmed);
+ if (nextFormat === format) {
+ sameFormatEntries.push(nextTrimmed);
+ j++;
+ } else {
+ // Different format - stop collecting
+ break;
+ }
+ }
+
+ // If we have multiple entries in the same format, add them all
+ // Otherwise, just add the single entry
+ docsList.push(...sameFormatEntries);
+ i = j; // Move to the next unprocessed value
}
}
}
@@ -761,27 +894,37 @@
// Build maintainers list
const allMaintainers = maintainers.filter(m => m.trim());
+ // Build relays list - combine user relays with default relays
+ const allRelays = [
+ ...relays.filter(r => r.trim()),
+ ...DEFAULT_NOSTR_RELAYS.filter(r => !relays.includes(r))
+ ];
+
+ // Build blossoms list
+ const allBlossoms = blossoms.filter(b => b.trim());
+
// Build documentation list
const allDocumentation = documentation.filter(d => d.trim());
// Build tags/labels (excluding 'private' and 'fork' which are handled separately)
const allTags = tags.filter(t => t.trim() && t !== 'private' && t !== 'fork');
- // Build event tags - use separate tag for each value (correct format)
+ // Build event tags - use single tag with multiple values (NIP-34 format)
const eventTags: string[][] = [
['d', dTag],
['name', repoName],
...(description ? [['description', description]] : []),
- ...allCloneUrls.map(url => ['clone', url]), // Separate tag per URL
- ...allWebUrls.map(url => ['web', url]), // Separate tag per URL
- ...allMaintainers.map(m => ['maintainers', m]), // Separate tag per maintainer
- ...allDocumentation.map(d => ['documentation', d]), // Separate tag per documentation
+ ...(allCloneUrls.length > 0 ? [['clone', ...allCloneUrls]] : []), // Single tag with all clone URLs
+ ...(allWebUrls.length > 0 ? [['web', ...allWebUrls]] : []), // Single tag with all web URLs
+ ...(allMaintainers.length > 0 ? [['maintainers', ...allMaintainers]] : []), // Single tag with all maintainers
+ ...(allRelays.length > 0 ? [['relays', ...allRelays]] : []), // Single tag with all relays
+ ...(allBlossoms.length > 0 ? [['blossoms', ...allBlossoms]] : []), // Single tag with all blossoms
+ ...allDocumentation.map(d => ['documentation', d]), // Documentation can have relay hints, so keep separate
...allTags.map(t => ['t', t]),
...(imageUrl.trim() ? [['image', imageUrl.trim()]] : []),
...(bannerUrl.trim() ? [['banner', bannerUrl.trim()]] : []),
...(alt.trim() ? [['alt', alt.trim()]] : []),
- ...(earliestCommit.trim() ? [['r', earliestCommit.trim(), 'euc']] : []),
- ...DEFAULT_NOSTR_RELAYS.map(relay => ['relays', relay]) // Separate tag per relay (correct format)
+ ...(earliestCommit.trim() ? [['r', earliestCommit.trim(), 'euc']] : [])
];
// Add fork tags if this is a fork
@@ -1296,6 +1439,76 @@
+
+
+
+