Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 12 kB view raw
1/** 2 * HTML page generation utilities for hosting service 3 * Generates 404 pages, directory listings, and updating pages 4 */ 5 6/** 7 * Generate 404 page HTML 8 */ 9export function generate404Page(): string { 10 const html = `<!DOCTYPE html> 11<html> 12<head> 13 <meta charset="utf-8"> 14 <meta name="viewport" content="width=device-width, initial-scale=1"> 15 <title>404 - Not Found</title> 16 <style> 17 @media (prefers-color-scheme: light) { 18 :root { 19 /* Warm beige background */ 20 --background: oklch(0.90 0.012 35); 21 /* Very dark brown text */ 22 --foreground: oklch(0.18 0.01 30); 23 --border: oklch(0.75 0.015 30); 24 /* Bright pink accent for links */ 25 --accent: oklch(0.78 0.15 345); 26 } 27 } 28 @media (prefers-color-scheme: dark) { 29 :root { 30 /* Slate violet background */ 31 --background: oklch(0.23 0.015 285); 32 /* Light gray text */ 33 --foreground: oklch(0.90 0.005 285); 34 /* Subtle borders */ 35 --border: oklch(0.38 0.02 285); 36 /* Soft pink accent */ 37 --accent: oklch(0.85 0.08 5); 38 } 39 } 40 body { 41 font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 42 background: var(--background); 43 color: var(--foreground); 44 padding: 2rem; 45 max-width: 800px; 46 margin: 0 auto; 47 display: flex; 48 flex-direction: column; 49 min-height: 100vh; 50 justify-content: center; 51 align-items: center; 52 text-align: center; 53 } 54 h1 { 55 font-size: 6rem; 56 margin: 0; 57 font-weight: 700; 58 line-height: 1; 59 } 60 h2 { 61 font-size: 1.5rem; 62 margin: 1rem 0 2rem; 63 font-weight: 400; 64 opacity: 0.8; 65 } 66 p { 67 font-size: 1rem; 68 opacity: 0.7; 69 margin-bottom: 2rem; 70 } 71 a { 72 color: var(--accent); 73 text-decoration: none; 74 font-size: 1rem; 75 } 76 a:hover { 77 text-decoration: underline; 78 } 79 footer { 80 margin-top: 2rem; 81 padding-top: 1.5rem; 82 border-top: 1px solid var(--border); 83 text-align: center; 84 font-size: 0.875rem; 85 opacity: 0.7; 86 color: var(--foreground); 87 } 88 footer a { 89 color: var(--accent); 90 text-decoration: none; 91 display: inline; 92 } 93 footer a:hover { 94 text-decoration: underline; 95 } 96 </style> 97</head> 98<body> 99 <div> 100 <h1>404</h1> 101 <h2>Page not found</h2> 102 <p>The page you're looking for doesn't exist.</p> 103 <a href="/">← Back to home</a> 104 </div> 105 <footer> 106 Hosted on <a href="https://wisp.place" target="_blank" rel="noopener">wisp.place</a> - Made by <a href="https://bsky.app/profile/nekomimi.pet" target="_blank" rel="noopener">@nekomimi.pet</a> 107 </footer> 108</body> 109</html>`; 110 return html; 111} 112 113/** 114 * Generate directory listing HTML 115 */ 116export function generateDirectoryListing(path: string, entries: Array<{name: string, isDirectory: boolean}>): string { 117 const title = path || 'Index'; 118 119 // Sort: directories first, then files, alphabetically within each group 120 const sortedEntries = [...entries].sort((a, b) => { 121 if (a.isDirectory && !b.isDirectory) return -1; 122 if (!a.isDirectory && b.isDirectory) return 1; 123 return a.name.localeCompare(b.name); 124 }); 125 126 const html = `<!DOCTYPE html> 127<html> 128<head> 129 <meta charset="utf-8"> 130 <meta name="viewport" content="width=device-width, initial-scale=1"> 131 <title>Index of /${path}</title> 132 <style> 133 @media (prefers-color-scheme: light) { 134 :root { 135 /* Warm beige background */ 136 --background: oklch(0.90 0.012 35); 137 /* Very dark brown text */ 138 --foreground: oklch(0.18 0.01 30); 139 --border: oklch(0.75 0.015 30); 140 /* Bright pink accent for links */ 141 --accent: oklch(0.78 0.15 345); 142 /* Lavender for folders */ 143 --folder: oklch(0.60 0.12 295); 144 --icon: oklch(0.28 0.01 30); 145 } 146 } 147 @media (prefers-color-scheme: dark) { 148 :root { 149 /* Slate violet background */ 150 --background: oklch(0.23 0.015 285); 151 /* Light gray text */ 152 --foreground: oklch(0.90 0.005 285); 153 /* Subtle borders */ 154 --border: oklch(0.38 0.02 285); 155 /* Soft pink accent */ 156 --accent: oklch(0.85 0.08 5); 157 /* Lavender for folders */ 158 --folder: oklch(0.70 0.10 295); 159 --icon: oklch(0.85 0.005 285); 160 } 161 } 162 body { 163 font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 164 background: var(--background); 165 color: var(--foreground); 166 padding: 2rem; 167 max-width: 800px; 168 margin: 0 auto; 169 } 170 h1 { 171 font-size: 1.5rem; 172 margin-bottom: 2rem; 173 padding-bottom: 0.5rem; 174 border-bottom: 1px solid var(--border); 175 } 176 ul { 177 list-style: none; 178 padding: 0; 179 } 180 li { 181 padding: 0.5rem 0; 182 border-bottom: 1px solid var(--border); 183 } 184 li:last-child { 185 border-bottom: none; 186 } 187 li a { 188 color: var(--accent); 189 text-decoration: none; 190 display: flex; 191 align-items: center; 192 gap: 0.75rem; 193 } 194 li a:hover { 195 text-decoration: underline; 196 } 197 .folder { 198 color: var(--folder); 199 font-weight: 600; 200 } 201 .file { 202 color: var(--accent); 203 } 204 .folder::before, 205 .file::before, 206 .parent::before { 207 content: ""; 208 display: inline-block; 209 width: 1.25em; 210 height: 1.25em; 211 background-color: var(--icon); 212 flex-shrink: 0; 213 -webkit-mask-size: contain; 214 mask-size: contain; 215 -webkit-mask-repeat: no-repeat; 216 mask-repeat: no-repeat; 217 -webkit-mask-position: center; 218 mask-position: center; 219 } 220 .folder::before { 221 -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M64 15v37a5.006 5.006 0 0 1-5 5H5a5.006 5.006 0 0 1-5-5V12a5.006 5.006 0 0 1 5-5h14.116a6.966 6.966 0 0 1 5.466 2.627l5 6.247A2.983 2.983 0 0 0 31.922 17H59a1 1 0 0 1 0 2H31.922a4.979 4.979 0 0 1-3.9-1.876l-5-6.247A4.976 4.976 0 0 0 19.116 9H5a3 3 0 0 0-3 3v40a3 3 0 0 0 3 3h54a3 3 0 0 0 3-3V15a3 3 0 0 0-3-3H30a1 1 0 0 1 0-2h29a5.006 5.006 0 0 1 5 5z"/></svg>'); 222 mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M64 15v37a5.006 5.006 0 0 1-5 5H5a5.006 5.006 0 0 1-5-5V12a5.006 5.006 0 0 1 5-5h14.116a6.966 6.966 0 0 1 5.466 2.627l5 6.247A2.983 2.983 0 0 0 31.922 17H59a1 1 0 0 1 0 2H31.922a4.979 4.979 0 0 1-3.9-1.876l-5-6.247A4.976 4.976 0 0 0 19.116 9H5a3 3 0 0 0-3 3v40a3 3 0 0 0 3 3h54a3 3 0 0 0 3-3V15a3 3 0 0 0-3-3H30a1 1 0 0 1 0-2h29a5.006 5.006 0 0 1 5 5z"/></svg>'); 223 } 224 .file::before { 225 -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25"><g><path d="M18 8.28a.59.59 0 0 0-.13-.18l-4-3.9h-.05a.41.41 0 0 0-.15-.2.41.41 0 0 0-.19 0h-9a.5.5 0 0 0-.5.5v19a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5V8.43a.58.58 0 0 0 .02-.15zM16.3 8H14V5.69zM5 23V5h8v3.5a.49.49 0 0 0 .15.36.5.5 0 0 0 .35.14l3.5-.06V23z"/><path d="M20.5 1h-13a.5.5 0 0 0-.5.5V3a.5.5 0 0 0 1 0V2h12v18h-1a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .5-.5v-19a.5.5 0 0 0-.5-.5z"/><path d="M7.5 8h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0 0 1zM7.5 11h4a.5.5 0 0 0 0-1h-4a.5.5 0 0 0 0 1zM13.5 13h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zM13.5 16h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zM13.5 19h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1z"/></g></svg>'); 226 mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25"><g><path d="M18 8.28a.59.59 0 0 0-.13-.18l-4-3.9h-.05a.41.41 0 0 0-.15-.2.41.41 0 0 0-.19 0h-9a.5.5 0 0 0-.5.5v19a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5V8.43a.58.58 0 0 0 .02-.15zM16.3 8H14V5.69zM5 23V5h8v3.5a.49.49 0 0 0 .15.36.5.5 0 0 0 .35.14l3.5-.06V23z"/><path d="M20.5 1h-13a.5.5 0 0 0-.5.5V3a.5.5 0 0 0 1 0V2h12v18h-1a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .5-.5v-19a.5.5 0 0 0-.5-.5z"/><path d="M7.5 8h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0 0 1zM7.5 11h4a.5.5 0 0 0 0-1h-4a.5.5 0 0 0 0 1zM13.5 13h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zM13.5 16h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zM13.5 19h-6a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1z"/></g></svg>'); 227 } 228 .parent::before { 229 -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>'); 230 mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>'); 231 } 232 footer { 233 margin-top: 2rem; 234 padding-top: 1.5rem; 235 border-top: 1px solid var(--border); 236 text-align: center; 237 font-size: 0.875rem; 238 opacity: 0.7; 239 color: var(--foreground); 240 } 241 footer a { 242 color: var(--accent); 243 text-decoration: none; 244 display: inline; 245 } 246 footer a:hover { 247 text-decoration: underline; 248 } 249 </style> 250</head> 251<body> 252 <h1>Index of /${path}</h1> 253 <ul> 254 ${path ? '<li><a href="../" class="parent">../</a></li>' : ''} 255 ${sortedEntries.map(e => 256 `<li><a href="${e.name}${e.isDirectory ? '/' : ''}" class="${e.isDirectory ? 'folder' : 'file'}">${e.name}${e.isDirectory ? '/' : ''}</a></li>` 257 ).join('\n ')} 258 </ul> 259 <footer> 260 Hosted on <a href="https://wisp.place" target="_blank" rel="noopener">wisp.place</a> - Made by <a href="https://bsky.app/profile/nekomimi.pet" target="_blank" rel="noopener">@nekomimi.pet</a> 261 </footer> 262</body> 263</html>`; 264 return html; 265} 266 267/** 268 * Return a response indicating the site is being updated 269 */ 270export function generateSiteUpdatingPage(): string { 271 const html = `<!DOCTYPE html> 272<html> 273<head> 274 <meta charset="utf-8"> 275 <meta name="viewport" content="width=device-width, initial-scale=1"> 276 <title>Site Updating</title> 277 <style> 278 @media (prefers-color-scheme: light) { 279 :root { 280 --background: oklch(0.90 0.012 35); 281 --foreground: oklch(0.18 0.01 30); 282 --primary: oklch(0.35 0.02 35); 283 --accent: oklch(0.78 0.15 345); 284 } 285 } 286 @media (prefers-color-scheme: dark) { 287 :root { 288 --background: oklch(0.23 0.015 285); 289 --foreground: oklch(0.90 0.005 285); 290 --primary: oklch(0.70 0.10 295); 291 --accent: oklch(0.85 0.08 5); 292 } 293 } 294 body { 295 font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 296 display: flex; 297 align-items: center; 298 justify-content: center; 299 min-height: 100vh; 300 margin: 0; 301 background: var(--background); 302 color: var(--foreground); 303 } 304 .container { 305 text-align: center; 306 padding: 2rem; 307 max-width: 500px; 308 } 309 h1 { 310 font-size: 2.5rem; 311 margin-bottom: 1rem; 312 font-weight: 600; 313 color: var(--primary); 314 } 315 p { 316 font-size: 1.25rem; 317 opacity: 0.8; 318 margin-bottom: 2rem; 319 color: var(--foreground); 320 } 321 .spinner { 322 border: 4px solid var(--accent); 323 border-radius: 50%; 324 border-top: 4px solid var(--primary); 325 width: 40px; 326 height: 40px; 327 animation: spin 1s linear infinite; 328 margin: 0 auto; 329 } 330 @keyframes spin { 331 0% { transform: rotate(0deg); } 332 100% { transform: rotate(360deg); } 333 } 334 </style> 335 <meta http-equiv="refresh" content="3"> 336</head> 337<body> 338 <div class="container"> 339 <h1>Site Updating</h1> 340 <p>This site is undergoing an update right now. Check back in a moment...</p> 341 <div class="spinner"></div> 342 </div> 343</body> 344</html>`; 345 346 return html; 347} 348 349/** 350 * Create a Response for site updating 351 */ 352export function siteUpdatingResponse(): Response { 353 return new Response(generateSiteUpdatingPage(), { 354 status: 503, 355 headers: { 356 'Content-Type': 'text/html; charset=utf-8', 357 'Cache-Control': 'no-store, no-cache, must-revalidate', 358 'Retry-After': '3', 359 }, 360 }); 361} 362