Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.

test

+304 -189
+59 -2
apps/main-app/public/editor/tabs/DomainsTab.tsx
··· 23 23 CheckCircle2, 24 24 XCircle, 25 25 Loader2, 26 - Trash2 26 + Trash2, 27 + AlertCircle 27 28 } from 'lucide-react' 28 29 import type { WispDomain, CustomDomain } from '../hooks/useDomainData' 29 30 import type { UserInfo } from '../hooks/useUserInfo' 31 + 32 + // Hosting node IP addresses for A record fallback 33 + const HOSTING_NODES = [ 34 + { region: 'US East (Virginia)', ip: '192.0.2.1' }, 35 + { region: 'US West (California)', ip: '192.0.2.2' }, 36 + { region: 'Europe (Frankfurt)', ip: '192.0.2.3' }, 37 + ] as const 30 38 31 39 interface DomainsTabProps { 32 40 wispDomains: WispDomain[] ··· 511 519 <div className="p-3 bg-background rounded border border-border"> 512 520 <div className="flex justify-between items-start mb-2"> 513 521 <span className="text-xs font-semibold text-muted-foreground"> 514 - CNAME Record (Pointing) 522 + CNAME Record (Pointing) — Recommended 515 523 </span> 516 524 </div> 517 525 <div className="font-mono text-xs space-y-2"> ··· 534 542 </div> 535 543 <p className="text-xs text-muted-foreground mt-2"> 536 544 Note: Some DNS providers (like Cloudflare) flatten CNAMEs to A records - this is fine and won't affect verification. 545 + </p> 546 + </div> 547 + 548 + <div className="p-3 bg-background rounded border border-border"> 549 + <div className="flex items-start gap-2 mb-2"> 550 + <span className="text-xs font-semibold text-muted-foreground"> 551 + A Records (Fallback Option) 552 + </span> 553 + </div> 554 + <div className="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded mb-3 flex gap-2"> 555 + <AlertCircle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" /> 556 + <p className="text-xs text-yellow-700 dark:text-yellow-500"> 557 + <strong>Warning:</strong> Using A records instead of CNAME means you lose GeoDNS capabilities. 558 + Your site will always be served from the specific node you choose below, regardless of visitor location. 559 + </p> 560 + </div> 561 + <div className="space-y-3"> 562 + {HOSTING_NODES.map((node) => ( 563 + <div key={node.ip} className="font-mono text-xs space-y-1 pl-3 border-l-2 border-muted"> 564 + <div className="font-semibold text-muted-foreground mb-1"> 565 + {node.region} 566 + </div> 567 + <div> 568 + <span className="text-muted-foreground"> 569 + Name: 570 + </span>{' '} 571 + <span className="select-all"> 572 + {domain.domain} 573 + </span> 574 + </div> 575 + <div> 576 + <span className="text-muted-foreground"> 577 + Type: 578 + </span>{' '} 579 + <span>A</span> 580 + </div> 581 + <div> 582 + <span className="text-muted-foreground"> 583 + Value: 584 + </span>{' '} 585 + <span className="select-all"> 586 + {node.ip} 587 + </span> 588 + </div> 589 + </div> 590 + ))} 591 + </div> 592 + <p className="text-xs text-muted-foreground mt-3"> 593 + Choose one region that best matches your primary audience location. 537 594 </p> 538 595 </div> 539 596 </div>
+245 -187
apps/main-app/src/index.ts
··· 132 132 133 133 set.headers['Content-Type'] = 'text/html; charset=utf-8' 134 134 135 - return `<!doctype html> 135 + return `<!DOCTYPE html> 136 136 <html lang="en"> 137 - <head> 138 - <meta charset="UTF-8" /> 139 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 140 - <title>wisp.place</title> 141 - <meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." /> 137 + <head> 138 + <meta charset="UTF-8"> 139 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 140 + <title>wisp.place</title> 141 + <meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." /> 142 142 143 - <!-- Open Graph / Facebook --> 144 - <meta property="og:type" content="website" /> 145 - <meta property="og:url" content="https://wisp.place/" /> 146 - <meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" /> 147 - <meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 148 - <meta property="og:site_name" content="wisp.place" /> 143 + <!-- Open Graph / Facebook --> 144 + <meta property="og:type" content="website" /> 145 + <meta property="og:url" content="https://wisp.place/" /> 146 + <meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" /> 147 + <meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 148 + <meta property="og:site_name" content="wisp.place" /> 149 149 150 - <!-- Twitter --> 151 - <meta name="twitter:card" content="summary_large_image" /> 152 - <meta name="twitter:url" content="https://wisp.place/" /> 153 - <meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" /> 154 - <meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 150 + <!-- Twitter --> 151 + <meta name="twitter:card" content="summary_large_image" /> 152 + <meta name="twitter:url" content="https://wisp.place/" /> 153 + <meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" /> 154 + <meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 155 155 156 - <!-- Theme --> 157 - <meta name="theme-color" content="#7c3aed" /> 156 + <!-- Theme --> 157 + <meta name="theme-color" content="#000000" /> 158 158 159 - <link rel="icon" type="image/x-icon" href="./favicon.ico"> 160 - <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> 161 - <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> 162 - <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> 163 - <link rel="manifest" href="./site.webmanifest"> 159 + <link rel="icon" type="image/x-icon" href="./favicon.ico"> 160 + <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> 161 + <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> 162 + <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> 163 + <link rel="manifest" href="./site.webmanifest"> 164 164 165 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 166 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 167 - <link 168 - href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" 169 - rel="stylesheet" 170 - /> 171 - <style> 172 - * { 173 - margin: 0; 174 - padding: 0; 175 - box-sizing: border-box; 176 - } 165 + <link rel="preconnect" href="https://fonts.googleapis.com"> 166 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 167 + <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet"> 168 + 169 + <style> 170 + * { 171 + margin: 0; 172 + padding: 0; 173 + box-sizing: border-box; 174 + } 177 175 176 + :root { 177 + --bg: #fafafa; 178 + --text: #000; 179 + --text-muted: #666; 180 + --text-subtle: #999; 181 + --border: #ddd; 182 + --cta-bg: #000; 183 + --cta-text: #fff; 184 + --cta-hover-bg: #fff; 185 + --cta-hover-text: #000; 186 + --code-bg: #000; 187 + --code-text: #0f0; 188 + --link: #000; 189 + } 190 + 191 + @media (prefers-color-scheme: dark) { 178 192 :root { 179 - --bg: #ffffff; 180 - --text: #1a1a1a; 181 - --text-muted: #666; 182 - --link: #0066cc; 183 - --link-hover: #0052a3; 184 - --terminal-bg: #1a1a1a; 185 - --terminal-text: #e0e0e0; 186 - --terminal-cyan: #5fdfdf; 193 + --bg: #0a0a0a; 194 + --text: #fafafa; 195 + --text-muted: #999; 196 + --text-subtle: #666; 197 + --border: #333; 198 + --cta-bg: #fff; 199 + --cta-text: #000; 200 + --cta-hover-bg: #0a0a0a; 201 + --cta-hover-text: #fff; 202 + --code-bg: #111; 203 + --code-text: #0f0; 204 + --link: #fff; 187 205 } 206 + } 188 207 189 - @media (prefers-color-scheme: dark) { 190 - :root { 191 - --bg: #121212; 192 - --text: #e0e0e0; 193 - --text-muted: #888; 194 - --link: #5fdfdf; 195 - --link-hover: #7fffff; 196 - --terminal-bg: #0a0a0a; 197 - --terminal-text: #e0e0e0; 198 - } 199 - } 208 + body { 209 + font-family: "Fira Mono", monospace; 210 + font-weight: 400; 211 + background: var(--bg); 212 + color: var(--text); 213 + min-height: 100vh; 214 + display: flex; 215 + flex-direction: column; 216 + padding-top: 6rem; 217 + } 200 218 201 - body { 202 - font-family: "Fira Mono", monospace; 203 - font-weight: 400; 204 - font-style: normal; 205 - font-size: 18px; 206 - line-height: 1.6; 207 - padding: 60px 40px; 208 - max-width: 80%; 209 - color: var(--text); 210 - background: var(--bg); 211 - transition: 212 - background 0.2s, 213 - color 0.2s; 214 - } 219 + .container { 220 + max-width: 800px; 221 + margin: 0 auto; 222 + padding: 0 2rem; 223 + width: 100%; 224 + } 225 + 226 + main { 227 + flex: 1; 228 + display: flex; 229 + align-items: center; 230 + justify-content: center; 231 + } 232 + 233 + .hero { 234 + text-align: center; 235 + padding: 4rem 0; 236 + } 215 237 238 + h1 { 239 + font-size: 5rem; 240 + font-weight: 700; 241 + margin-bottom: 4rem; 242 + letter-spacing: -0.02em; 243 + color: #4a4a4a; 244 + text-shadow: 245 + 1px 1px 0 #fff, 246 + -1px -1px 0 #2a2a2a, 247 + 2px 2px 3px rgba(0, 0, 0, 0.3); 248 + } 249 + 250 + @media (prefers-color-scheme: dark) { 216 251 h1 { 217 - font-size: 1.1em; 218 - font-weight: normal; 219 - margin-bottom: 2em; 252 + color: #888; 253 + text-shadow: 254 + 1px 1px 0 #222, 255 + -1px -1px 0 #000, 256 + 2px 2px 3px rgba(0, 0, 0, 0.5); 220 257 } 258 + } 221 259 222 - .cursor { 223 - display: inline-block; 224 - width: 2px; 225 - height: 1.1em; 226 - background: var(--text); 227 - margin-left: 2px; 228 - vertical-align: text-bottom; 229 - animation: blink 1s step-end infinite; 230 - } 260 + h1::after { 261 + content: '_'; 262 + animation: blink 1s infinite; 263 + } 231 264 232 - @keyframes blink { 233 - 0%, 234 - 100% { 235 - opacity: 1; 236 - } 237 - 50% { 238 - opacity: 0; 239 - } 240 - } 265 + @keyframes blink { 266 + 0%, 50% { opacity: 1; } 267 + 51%, 100% { opacity: 0; } 268 + } 241 269 242 - p { 243 - margin-bottom: 0.3em; 244 - } 270 + .cta { 271 + display: inline-block; 272 + background: var(--cta-bg); 273 + color: var(--cta-text); 274 + padding: 2rem 4rem; 275 + font-size: 1.5rem; 276 + text-decoration: none; 277 + border: 3px solid var(--cta-bg); 278 + transition: all 0.1s; 279 + font-weight: 700; 280 + margin-bottom: 3rem; 281 + } 245 282 246 - section { 247 - margin-bottom: 2.5em; 248 - } 283 + .cta:hover { 284 + background: var(--cta-hover-bg); 285 + color: var(--cta-hover-text); 286 + border-color: var(--cta-bg); 287 + } 249 288 250 - a { 251 - color: var(--link); 252 - text-decoration: underline; 253 - text-underline-offset: 2px; 254 - } 289 + .tagline { 290 + font-size: 1.2rem; 291 + color: var(--text-muted); 292 + margin-bottom: 6rem; 293 + } 255 294 256 - a:hover { 257 - color: var(--link-hover); 258 - } 295 + .secondary { 296 + border-top: 1px solid var(--border); 297 + padding-top: 3rem; 298 + margin-top: 4rem; 299 + } 259 300 260 - .click-hint { 261 - color: var(--link); 262 - margin-left: 0.5em; 263 - display: inline-flex; 264 - align-items: center; 265 - } 301 + .secondary h2 { 302 + font-size: 1rem; 303 + margin-bottom: 1.5rem; 304 + font-weight: 700; 305 + text-transform: lowercase; 306 + } 266 307 267 - .click-hint .arrow { 268 - display: inline-block; 269 - width: 1.2em; 270 - text-align: center; 271 - animation: nudge 1.2s ease-in-out infinite; 272 - } 308 + .code-block { 309 + background: var(--code-bg); 310 + color: var(--code-text); 311 + padding: 1.5rem; 312 + margin: 1rem 0; 313 + font-size: 0.9rem; 314 + overflow-x: auto; 315 + } 273 316 274 - @keyframes nudge { 275 - 0%, 276 - 100% { 277 - transform: translateX(0); 278 - } 279 - 50% { 280 - transform: translateX(-4px); 281 - } 282 - } 317 + .code-block code { 318 + font-family: "Fira Mono", monospace; 319 + } 283 320 284 - .terminal-section { 285 - margin-top: 2em; 286 - } 321 + .secondary p { 322 + color: var(--text-muted); 323 + margin-bottom: 1rem; 324 + font-size: 0.95rem; 325 + } 287 326 288 - .terminal-label { 289 - margin-bottom: 0.8em; 290 - } 327 + .secondary a { 328 + color: var(--link); 329 + text-decoration: none; 330 + border-bottom: 1px solid var(--link); 331 + } 291 332 292 - .cmd { 293 - font-family: 294 - ui-monospace, "SF Mono", "Cascadia Code", "Source Code Pro", 295 - Menlo, Consolas, monospace; 296 - font-size: 0.85em; 297 - background: var(--terminal-bg); 298 - color: var(--terminal-text); 299 - border-radius: 4px; 300 - padding: 12px 16px; 301 - display: table; 302 - white-space: nowrap; 303 - margin-bottom: 0.5em; 304 - } 333 + .secondary a:hover { 334 + border-bottom: 2px solid var(--link); 335 + } 305 336 306 - .cmd .highlight { 307 - color: var(--terminal-cyan); 337 + footer { 338 + border-top: 1px solid var(--border); 339 + padding: 3rem 0; 340 + text-align: center; 341 + margin-top: 6rem; 342 + } 343 + 344 + .quote { 345 + font-size: 0.85rem; 346 + color: var(--text-subtle); 347 + font-style: italic; 348 + } 349 + 350 + .links { 351 + margin-top: 2rem; 352 + font-size: 0.85rem; 353 + } 354 + 355 + .links a { 356 + color: var(--text-muted); 357 + text-decoration: none; 358 + margin: 0 1rem; 359 + } 360 + 361 + .links a:hover { 362 + color: var(--text); 363 + } 364 + 365 + @media (max-width: 768px) { 366 + h1 { 367 + font-size: 3rem; 308 368 } 309 369 310 - .hosting-options { 311 - margin-top: 2.5em; 370 + .cta { 371 + padding: 1.5rem 3rem; 372 + font-size: 1.2rem; 312 373 } 313 374 314 - .hosting-options p { 315 - margin-bottom: 0.2em; 375 + .tagline { 376 + font-size: 1rem; 316 377 } 317 - </style> 318 - </head> 319 - <body> 320 - <h1>wisp.place<span class="cursor"></span></h1> 378 + } 379 + </style> 380 + </head> 381 + <body> 382 + <main> 383 + <div class="container"> 384 + <div class="hero"> 385 + <h1>wisp.place</h1> 321 386 322 - <section> 323 - <p>the easiest way to get static html going</p> 324 - <p> 325 - just drag n' drop into the dashboard with your 326 - <a href="${atprotoLoginUrl}">AT Protocol account</a>. 327 - <span class="click-hint" 328 - ><span class="arrow">←</span> click me!</span 329 - > 330 - </p> 331 - </section> 387 + <a href="${atprotoLoginUrl}" class="cta">SIGN IN WITH AT PROTOCOL</a> 332 388 333 - <section class="terminal-section"> 334 - <p class="terminal-label">are you a terminal nerd?</p> 335 - <code class="cmd" 336 - >curl 337 - <span class="highlight" 338 - >https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux</span 339 - > 340 - -o wisp-cli</code 341 - > 342 - <code class="cmd" 343 - >wisp-cli 344 - <span class="highlight">alice.bsky.social</span> --site 345 - MyBlog</code 346 - > 347 - </section> 389 + <p class="tagline">Drop files. They're live.</p> 348 390 349 - <div class="hosting-options"> 350 - <p>host on our infrastructure for free</p> 351 - <p> 352 - or use wisp-cli to host on your own infra with seamless 353 - deployments 354 - </p> 355 - <p>need docs? <a href="https://docs.wisp.place">docs.wisp.place</a></p> 391 + <div class="secondary"> 392 + <h2>are you a terminal nerd?</h2> 393 + <div class="code-block"> 394 + <code>curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli</code> 395 + </div> 396 + <div class="code-block"> 397 + <code>wisp-cli alice.bsky.social --site MyBlog</code> 398 + </div> 399 + <p>host on our infrastructure for free<br> 400 + or use wisp-cli to host on your own infra with seamless deployments</p> 401 + <p>need docs? <a href="https://docs.wisp.place">docs.wisp.place</a></p> 402 + </div> 403 + </div> 404 + </div> 405 + </main> 406 + 407 + <footer> 408 + <div class="container"> 409 + <p class="quote">"The easiest way to get static HTML going."</p> 410 + <div class="links"> 411 + <a href="https://docs.wisp.place">docs</a> 412 + </div> 356 413 </div> 357 - </body> 414 + </footer> 415 + </body> 358 416 </html>` 359 417 }) 360 418 .use(authRoutes(client, cookieSecret))