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

landing page redesign

+265 -35
-35
apps/main-app/public/index.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>wisp.place</title> 7 - <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." /> 8 - 9 - <!-- Open Graph / Facebook --> 10 - <meta property="og:type" content="website" /> 11 - <meta property="og:url" content="https://wisp.place/" /> 12 - <meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" /> 13 - <meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 14 - <meta property="og:site_name" content="wisp.place" /> 15 - 16 - <!-- Twitter --> 17 - <meta name="twitter:card" content="summary_large_image" /> 18 - <meta name="twitter:url" content="https://wisp.place/" /> 19 - <meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" /> 20 - <meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 21 - 22 - <!-- Theme --> 23 - <meta name="theme-color" content="#7c3aed" /> 24 - 25 - <link rel="icon" type="image/x-icon" href="./favicon.ico"> 26 - <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> 27 - <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> 28 - <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> 29 - <link rel="manifest" href="./site.webmanifest"> 30 - </head> 31 - <body> 32 - <div id="elysia"></div> 33 - <script type="module" src="./index.tsx"></script> 34 - </body> 35 - </html>
+236
apps/main-app/src/index.ts
··· 121 121 }) 122 122 .onError(observabilityMiddleware('main-app').onError) 123 123 .use(csrfProtection()) 124 + .get('/', ({ set }) => { 125 + // Build dynamic login URL for AT Protocol OAuth entryway 126 + // atproto.wisp.place will redirect to this endpoint with the saved handle 127 + const isLocalDev = Bun.env.LOCAL_DEV === 'true' 128 + const loginUrl = isLocalDev 129 + ? 'http://127.0.0.1:8000/api/auth/login' 130 + : `${config.domain}/api/auth/login` 131 + const atprotoLoginUrl = `https://atproto.wisp.place/?next=${encodeURIComponent(loginUrl)}` 132 + 133 + set.headers['Content-Type'] = 'text/html; charset=utf-8' 134 + 135 + return `<!doctype html> 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." /> 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" /> 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." /> 155 + 156 + <!-- Theme --> 157 + <meta name="theme-color" content="#7c3aed" /> 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"> 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 + } 177 + 178 + :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; 187 + } 188 + 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 + } 200 + 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 + } 215 + 216 + h1 { 217 + font-size: 1.1em; 218 + font-weight: normal; 219 + margin-bottom: 2em; 220 + } 221 + 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 + } 231 + 232 + @keyframes blink { 233 + 0%, 234 + 100% { 235 + opacity: 1; 236 + } 237 + 50% { 238 + opacity: 0; 239 + } 240 + } 241 + 242 + p { 243 + margin-bottom: 0.3em; 244 + } 245 + 246 + section { 247 + margin-bottom: 2.5em; 248 + } 249 + 250 + a { 251 + color: var(--link); 252 + text-decoration: underline; 253 + text-underline-offset: 2px; 254 + } 255 + 256 + a:hover { 257 + color: var(--link-hover); 258 + } 259 + 260 + .click-hint { 261 + color: var(--link); 262 + margin-left: 0.5em; 263 + display: inline-flex; 264 + align-items: center; 265 + } 266 + 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 + } 273 + 274 + @keyframes nudge { 275 + 0%, 276 + 100% { 277 + transform: translateX(0); 278 + } 279 + 50% { 280 + transform: translateX(-4px); 281 + } 282 + } 283 + 284 + .terminal-section { 285 + margin-top: 2em; 286 + } 287 + 288 + .terminal-label { 289 + margin-bottom: 0.8em; 290 + } 291 + 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 + } 305 + 306 + .cmd .highlight { 307 + color: var(--terminal-cyan); 308 + } 309 + 310 + .hosting-options { 311 + margin-top: 2.5em; 312 + } 313 + 314 + .hosting-options p { 315 + margin-bottom: 0.2em; 316 + } 317 + </style> 318 + </head> 319 + <body> 320 + <h1>wisp.place<span class="cursor"></span></h1> 321 + 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> 332 + 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> 348 + 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> 356 + </div> 357 + </body> 358 + </html>` 359 + }) 124 360 .use(authRoutes(client, cookieSecret)) 125 361 .use(wispRoutes(client, cookieSecret)) 126 362 .use(domainRoutes(client, cookieSecret))
+29
apps/main-app/src/routes/auth.ts
··· 13 13 sign: ['did'] 14 14 } 15 15 }) 16 + .get('/api/auth/login', async (c) => { 17 + // GET endpoint for initiating OAuth via atproto.wisp.place entryway 18 + // Accepts: login_hint (handle) or pds (server) 19 + try { 20 + const query = c.query as { login_hint?: string; pds?: string } 21 + const handle = query.login_hint || '' 22 + const pds = query.pds || '' 23 + 24 + // Use login_hint if provided, otherwise use PDS URL 25 + const identifier = handle || (pds ? `https://${pds}` : '') 26 + 27 + if (!identifier) { 28 + logger.error('Login attempt with no login_hint or pds') 29 + return c.redirect('/?error=missing_handle') 30 + } 31 + 32 + logger.info('Login attempt via entryway', { identifier }) 33 + const state = crypto.randomUUID() 34 + const url = await client.authorize(identifier, { state }) 35 + logger.info('Authorization URL generated', { identifier }) 36 + 37 + // Redirect to the OAuth authorization URL 38 + return c.redirect(url.toString()) 39 + } catch (err) { 40 + logger.error('Login error', err) 41 + console.error('[Auth] Full error:', err) 42 + return c.redirect('/?error=auth_failed') 43 + } 44 + }) 16 45 .post('/api/auth/signin', async (c) => { 17 46 let handle = 'unknown' 18 47 try {