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

Configure Feed

Select the types of activity you want to include in your feed.

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 {