diff options
| author | bndw <ben@bdw.to> | 2026-02-13 20:50:27 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 20:50:27 -0800 |
| commit | 97e6c9a0c2c32bf514d3a4218d239741f1dc26c8 (patch) | |
| tree | 259cc3a149c9521adb9d212d0f4e5261a1ed08c6 /internal/handler/websocket/index.go | |
| parent | dfa19ff0776be0850ad7b86ca579601431349593 (diff) | |
feat: add HTML index page for browser viewing
Add a beautiful HTML landing page when visiting relay in browser:
- Shows all three protocol endpoints (gRPC, Connect, WebSocket)
- Lists supported NIPs (01, 09, 11)
- Displays relay features and info
- Responsive design with gradient styling
- Serves on GET requests (regular Accept header)
- NIP-11 still served for Accept: application/nostr+json
Diffstat (limited to 'internal/handler/websocket/index.go')
| -rw-r--r-- | internal/handler/websocket/index.go | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/internal/handler/websocket/index.go b/internal/handler/websocket/index.go new file mode 100644 index 0000000..96bcfd4 --- /dev/null +++ b/internal/handler/websocket/index.go | |||
| @@ -0,0 +1,194 @@ | |||
| 1 | package websocket | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "html/template" | ||
| 5 | "net/http" | ||
| 6 | ) | ||
| 7 | |||
| 8 | var indexTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html> | ||
| 9 | <html lang="en"> | ||
| 10 | <head> | ||
| 11 | <meta charset="UTF-8"> | ||
| 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 13 | <title>Nostr gRPC Relay</title> | ||
| 14 | <style> | ||
| 15 | * { | ||
| 16 | margin: 0; | ||
| 17 | padding: 0; | ||
| 18 | box-sizing: border-box; | ||
| 19 | } | ||
| 20 | body { | ||
| 21 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | ||
| 22 | line-height: 1.6; | ||
| 23 | color: #333; | ||
| 24 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 25 | min-height: 100vh; | ||
| 26 | padding: 2rem; | ||
| 27 | } | ||
| 28 | .container { | ||
| 29 | max-width: 800px; | ||
| 30 | margin: 0 auto; | ||
| 31 | background: white; | ||
| 32 | border-radius: 12px; | ||
| 33 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | ||
| 34 | overflow: hidden; | ||
| 35 | } | ||
| 36 | .header { | ||
| 37 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 38 | color: white; | ||
| 39 | padding: 2rem; | ||
| 40 | text-align: center; | ||
| 41 | } | ||
| 42 | .header h1 { | ||
| 43 | font-size: 2.5rem; | ||
| 44 | margin-bottom: 0.5rem; | ||
| 45 | } | ||
| 46 | .header p { | ||
| 47 | font-size: 1.1rem; | ||
| 48 | opacity: 0.9; | ||
| 49 | } | ||
| 50 | .content { | ||
| 51 | padding: 2rem; | ||
| 52 | } | ||
| 53 | .section { | ||
| 54 | margin-bottom: 2rem; | ||
| 55 | } | ||
| 56 | .section h2 { | ||
| 57 | color: #667eea; | ||
| 58 | margin-bottom: 1rem; | ||
| 59 | font-size: 1.5rem; | ||
| 60 | border-bottom: 2px solid #f0f0f0; | ||
| 61 | padding-bottom: 0.5rem; | ||
| 62 | } | ||
| 63 | .protocol { | ||
| 64 | background: #f8f9fa; | ||
| 65 | padding: 1rem; | ||
| 66 | border-radius: 8px; | ||
| 67 | margin-bottom: 1rem; | ||
| 68 | border-left: 4px solid #667eea; | ||
| 69 | } | ||
| 70 | .protocol h3 { | ||
| 71 | color: #333; | ||
| 72 | margin-bottom: 0.5rem; | ||
| 73 | font-size: 1.2rem; | ||
| 74 | } | ||
| 75 | .protocol code { | ||
| 76 | background: #e9ecef; | ||
| 77 | padding: 0.2rem 0.5rem; | ||
| 78 | border-radius: 4px; | ||
| 79 | font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; | ||
| 80 | font-size: 0.9rem; | ||
| 81 | color: #764ba2; | ||
| 82 | } | ||
| 83 | .protocol p { | ||
| 84 | margin: 0.5rem 0; | ||
| 85 | color: #666; | ||
| 86 | } | ||
| 87 | .nips { | ||
| 88 | display: flex; | ||
| 89 | flex-wrap: wrap; | ||
| 90 | gap: 0.5rem; | ||
| 91 | } | ||
| 92 | .nip { | ||
| 93 | background: #667eea; | ||
| 94 | color: white; | ||
| 95 | padding: 0.4rem 0.8rem; | ||
| 96 | border-radius: 20px; | ||
| 97 | font-size: 0.9rem; | ||
| 98 | font-weight: 500; | ||
| 99 | } | ||
| 100 | .footer { | ||
| 101 | background: #f8f9fa; | ||
| 102 | padding: 1.5rem; | ||
| 103 | text-align: center; | ||
| 104 | color: #666; | ||
| 105 | font-size: 0.9rem; | ||
| 106 | } | ||
| 107 | .footer a { | ||
| 108 | color: #667eea; | ||
| 109 | text-decoration: none; | ||
| 110 | } | ||
| 111 | .footer a:hover { | ||
| 112 | text-decoration: underline; | ||
| 113 | } | ||
| 114 | @media (max-width: 600px) { | ||
| 115 | body { | ||
| 116 | padding: 1rem; | ||
| 117 | } | ||
| 118 | .header h1 { | ||
| 119 | font-size: 2rem; | ||
| 120 | } | ||
| 121 | .content { | ||
| 122 | padding: 1.5rem; | ||
| 123 | } | ||
| 124 | } | ||
| 125 | </style> | ||
| 126 | </head> | ||
| 127 | <body> | ||
| 128 | <div class="container"> | ||
| 129 | <div class="header"> | ||
| 130 | <h1>⚡ Nostr gRPC Relay</h1> | ||
| 131 | <p>High-performance relay with multi-protocol support</p> | ||
| 132 | </div> | ||
| 133 | |||
| 134 | <div class="content"> | ||
| 135 | <div class="section"> | ||
| 136 | <h2>Protocols</h2> | ||
| 137 | |||
| 138 | <div class="protocol"> | ||
| 139 | <h3>🔌 gRPC (Native Binary)</h3> | ||
| 140 | <p><code>{{.GrpcAddr}}</code></p> | ||
| 141 | <p>High-performance binary protocol for applications</p> | ||
| 142 | </div> | ||
| 143 | |||
| 144 | <div class="protocol"> | ||
| 145 | <h3>🌐 Connect (HTTP/JSON)</h3> | ||
| 146 | <p><code>{{.HttpAddr}}/nostr.v1.NostrRelay/*</code></p> | ||
| 147 | <p>Browser-compatible gRPC over HTTP with JSON</p> | ||
| 148 | </div> | ||
| 149 | |||
| 150 | <div class="protocol"> | ||
| 151 | <h3>🔗 WebSocket (Nostr Protocol)</h3> | ||
| 152 | <p><code>ws://{{.WsAddr}}/</code></p> | ||
| 153 | <p>Standard Nostr protocol (NIP-01) for all clients</p> | ||
| 154 | </div> | ||
| 155 | </div> | ||
| 156 | |||
| 157 | <div class="section"> | ||
| 158 | <h2>Supported NIPs</h2> | ||
| 159 | <div class="nips"> | ||
| 160 | <span class="nip">NIP-01</span> | ||
| 161 | <span class="nip">NIP-09</span> | ||
| 162 | <span class="nip">NIP-11</span> | ||
| 163 | </div> | ||
| 164 | </div> | ||
| 165 | |||
| 166 | <div class="section"> | ||
| 167 | <h2>Features</h2> | ||
| 168 | <ul style="list-style-position: inside; color: #666;"> | ||
| 169 | <li>Binary-first storage (Protocol Buffers)</li> | ||
| 170 | <li>SQLite with WAL mode</li> | ||
| 171 | <li>Event validation (ID & signature)</li> | ||
| 172 | <li>Real-time subscriptions</li> | ||
| 173 | <li>Event deletion (NIP-09)</li> | ||
| 174 | </ul> | ||
| 175 | </div> | ||
| 176 | </div> | ||
| 177 | |||
| 178 | <div class="footer"> | ||
| 179 | <p>Built with <a href="https://github.com/anthropics/claude-code" target="_blank">Claude Code</a></p> | ||
| 180 | </div> | ||
| 181 | </div> | ||
| 182 | </body> | ||
| 183 | </html>`)) | ||
| 184 | |||
| 185 | type IndexData struct { | ||
| 186 | GrpcAddr string | ||
| 187 | HttpAddr string | ||
| 188 | WsAddr string | ||
| 189 | } | ||
| 190 | |||
| 191 | func (h *Handler) ServeIndex(w http.ResponseWriter, r *http.Request, data IndexData) { | ||
| 192 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| 193 | indexTemplate.Execute(w, data) | ||
| 194 | } | ||
