madebydanny.uk written in html, css, and a lot of JavaScript I don't understand madebydanny.uk
html css javascript
1
fork

Configure Feed

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

added CDN API docs

+953 -9
+25 -9
cdn.html
··· 7 7 <meta name="description" content="A simple, fast, and free CDN powered by Cloudflare. Upload images, GIFs, videos and documents."> 8 8 <meta property="og:title" content="MBD CDN — madebydanny.uk"> 9 9 <meta property="og:description" content="A simple easy to use CDN, free for life"> 10 - <meta property="og:image" content="https://imrs.madebydanny.uk?url=https://public-cdn.madebydanny.uk/user-content/2026-01-30/cb09a559-ae35-4617-971c-9230521f7a9c.png"> 10 + <meta property="og:image" content="https://cdn.madebydanny.uk/user-content/2026-04-29/93bde54d-bc21-43b9-8e4e-d9d324e9607d.png"> 11 11 <meta property="og:type" content="website"> 12 12 <link rel="icon" href="https://public-cdn.madebydanny.uk/user-content/2026-01-30/33913bec-bc2f-4e6c-a474-2ef8f8c00197"> 13 13 ··· 531 531 532 532 .limit-card { 533 533 background: var(--bg-card); 534 - border: 1px solid var(--border); 535 - border-top: 2px solid var(--accent); 536 534 border-radius: var(--radius); 537 535 padding: 1.25rem; 538 536 text-align: center; ··· 572 570 .limits-note { 573 571 padding: 0.875rem 1.125rem; 574 572 background: var(--bg-card); 575 - border: 1px solid var(--border); 576 - border-left: 3px solid var(--accent); 577 573 border-radius: var(--radius); 578 574 font-size: 0.8375rem; 579 575 color: var(--text-muted); ··· 638 634 .how-grid, .limits-grid { grid-template-columns: 1fr; } 639 635 .nav-title { display: none; } 640 636 } 637 + 638 + .hero-badges { 639 + display: flex; 640 + flex-wrap: wrap; 641 + gap: 0.5rem; 642 + margin-top: 1.25rem; 643 + } 644 + .badge { 645 + display: inline-flex; 646 + align-items: center; 647 + gap: 0.35rem; 648 + padding: 0.3rem 0.75rem; 649 + border: 1px solid var(--border); 650 + border-radius: 999px; 651 + font-size: 0.75rem; 652 + color: var(--text-muted); 653 + background: var(--bg-card); 654 + } 655 + .badge i { font-size: 0.7rem; color: var(--accent); } 641 656 </style> 642 657 </head> 643 658 <body> ··· 655 670 <section class="page-hero"> 656 671 <p class="page-hero-eyebrow">madebydanny.uk</p> 657 672 <h1>MBD CDN</h1> 658 - <p>A simple, fast CDN powered by Cloudflare R2 — free to use, no account needed.</p> 673 + <p>A simple, fast CDN powered by Cloudflare R2. Free for Life.</p> 674 + <p>Are you a developer looking for a CDN with an API? <a href="/cdn/api.html"><b>Try the new API!</b></a></p> 659 675 </section> 660 676 661 677 <!-- Stats --> ··· 848 864 849 865 <div class="limits-note"> 850 866 All limits reset daily at <strong>midnight UTC</strong> and are enforced per IP to protect performance for all users. 851 - Need higher limits? Consider self-hosting the stack, or get in touch via Bluesky. 867 + <p><b>Need higher limits? <a href="/cdn/api.html">Try the new API!</b></a></p> 852 868 </div> 853 869 854 870 <div class="usage-section"> ··· 858 874 <div class="usage-item"> 859 875 <div class="usage-row"> 860 876 <span>Files Uploaded</span> 861 - <span id="usage-files-label">0 / 30</span> 877 + <span id="usage-files-label">0 / 25</span> 862 878 </div> 863 879 <div class="usage-track"> 864 880 <div class="usage-fill" id="usage-files-fill" style="width:0%"></div> ··· 867 883 <div class="usage-item"> 868 884 <div class="usage-row"> 869 885 <span>Storage Used</span> 870 - <span id="usage-bytes-label">0 B / 1 GB</span> 886 + <span id="usage-bytes-label">0 B / 200 MB</span> 871 887 </div> 872 888 <div class="usage-track"> 873 889 <div class="usage-fill" id="usage-bytes-fill" style="width:0%"></div>
+11
cdn/admin-cmds.md
··· 1 + # MBD CDN - Admin Key Management 2 + 3 + If you self-host MBD CDN, you can issue and revoke keys via the `/v1/keys` admin endpoints. These endpoints require a separate `ADMIN_SECRET` environment variable to be set in your Cloudflare Worker — **not** a regular API key. 4 + 5 + ## Issue a Key 6 + 7 + ```bash 8 + curl -X POST [https://cdn.madebydanny.uk/v1/keys](https://cdn.madebydanny.uk/v1/keys) \ 9 + -H "Authorization: Bearer your_admin_secret" \ 10 + -H "Content-Type: application/json" \ 11 + -d '{"name": "My App", "owner": "yourname"}'
+917
cdn/api.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>Developer API — MBD CDN</title> 7 + <meta name="description" content="Developer documentation for the MBD CDN API. Upload, manage, and serve files programmatically."> 8 + <meta property="og:title" content="Developer API — MBD CDN"> 9 + <meta property="og:description" content="Upload, manage, and serve files programmatically via the MBD CDN REST API."> 10 + <meta property="og:type" content="website"> 11 + <link rel="icon" href="https://public-cdn.madebydanny.uk/user-content/2026-01-30/33913bec-bc2f-4e6c-a474-2ef8f8c00197"> 12 + 13 + <link rel="preconnect" href="https://fonts.googleapis.com"> 14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 15 + <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"> 16 + <script src="https://kit.fontawesome.com/0ca27f8db1.js" crossorigin="anonymous"></script> 17 + 18 + <style> 19 + /* ── Design tokens (match main site) ── */ 20 + :root { 21 + --bg: #0e0d0c; 22 + --bg-raised: #171512; 23 + --bg-card: #1a1815; 24 + --bg-code: #131210; 25 + --border: #2d2926; 26 + --border-hover:#4a4238; 27 + 28 + --text: #e8e0d8; 29 + --text-muted: #8a7f74; 30 + --text-dim: #584f47; 31 + 32 + --accent: #c9a96e; 33 + --accent-dim: rgba(201,169,110,0.12); 34 + --accent-glow: rgba(201,169,110,0.06); 35 + 36 + --green: #4caf7d; 37 + --green-dim: rgba(76,175,125,0.12); 38 + --red: #e06c6c; 39 + --red-dim: rgba(224,108,108,0.12); 40 + --blue: #6ba3c9; 41 + --blue-dim: rgba(107,163,201,0.12); 42 + 43 + --font-serif: 'Lora', Georgia, serif; 44 + --font-sans: 'DM Sans', system-ui, sans-serif; 45 + --font-mono: 'Monaco', 'Menlo', 'Courier New', monospace; 46 + 47 + --radius: 10px; 48 + --max-w: 780px; 49 + --transition: 0.2s ease; 50 + } 51 + 52 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 53 + html { scroll-behavior: smooth; } 54 + 55 + body { 56 + font-family: var(--font-sans); 57 + font-size: 16px; 58 + line-height: 1.7; 59 + color: var(--text); 60 + background: var(--bg); 61 + -webkit-font-smoothing: antialiased; 62 + } 63 + 64 + a { color: var(--accent); text-decoration: none; transition: opacity var(--transition); } 65 + a:hover { opacity: 0.75; } 66 + 67 + code { 68 + font-family: var(--font-mono); 69 + font-size: 0.8em; 70 + background: var(--bg-raised); 71 + padding: 0.15em 0.45em; 72 + border-radius: 4px; 73 + color: var(--accent); 74 + } 75 + 76 + /* ── Header ── */ 77 + .site-header { 78 + position: sticky; 79 + top: 0; 80 + z-index: 100; 81 + background: rgba(14,13,12,0.92); 82 + backdrop-filter: blur(12px); 83 + border-bottom: 1px solid var(--border); 84 + } 85 + .nav-container { 86 + max-width: var(--max-w); 87 + margin: 0 auto; 88 + padding: 0.875rem 1.5rem; 89 + display: flex; 90 + justify-content: space-between; 91 + align-items: center; 92 + } 93 + .nav-logo { 94 + font-family: var(--font-serif); 95 + font-style: italic; 96 + font-size: 1.1rem; 97 + color: var(--text-muted); 98 + } 99 + .nav-logo:hover { color: var(--text); opacity: 1; } 100 + .nav-title { font-family: var(--font-serif); font-size: 1.125rem; color: var(--text); } 101 + .nav-back { 102 + display: inline-flex; 103 + align-items: center; 104 + gap: 0.35rem; 105 + font-size: 0.8125rem; 106 + color: var(--text-dim); 107 + transition: color var(--transition); 108 + } 109 + .nav-back:hover { color: var(--text-muted); opacity: 1; } 110 + 111 + /* ── Main ── */ 112 + .main-content { 113 + max-width: var(--max-w); 114 + margin: 0 auto; 115 + padding: 0 1.5rem; 116 + } 117 + 118 + /* ── Hero ── */ 119 + .page-hero { 120 + padding: 3rem 0 2rem; 121 + border-bottom: 1px solid var(--border); 122 + } 123 + .page-hero-eyebrow { 124 + font-size: 0.75rem; 125 + text-transform: uppercase; 126 + letter-spacing: 0.1em; 127 + color: var(--text-dim); 128 + margin-bottom: 0.5rem; 129 + } 130 + .page-hero h1 { 131 + font-family: var(--font-serif); 132 + font-size: clamp(1.75rem, 5vw, 2.5rem); 133 + font-weight: 400; 134 + color: var(--text); 135 + letter-spacing: -0.02em; 136 + margin-bottom: 0.625rem; 137 + } 138 + .page-hero p { 139 + font-size: 0.9375rem; 140 + color: var(--text-muted); 141 + max-width: 540px; 142 + } 143 + .hero-badges { 144 + display: flex; 145 + flex-wrap: wrap; 146 + gap: 0.5rem; 147 + margin-top: 1.25rem; 148 + } 149 + .badge { 150 + display: inline-flex; 151 + align-items: center; 152 + gap: 0.35rem; 153 + padding: 0.3rem 0.75rem; 154 + border: 1px solid var(--border); 155 + border-radius: 999px; 156 + font-size: 0.75rem; 157 + color: var(--text-muted); 158 + background: var(--bg-card); 159 + } 160 + .badge i { font-size: 0.7rem; color: var(--accent); } 161 + 162 + /* ── Tabs ── */ 163 + .tabs-section { padding-top: 1.75rem; } 164 + .tab-bar { 165 + display: flex; 166 + gap: 0; 167 + border-bottom: 1px solid var(--border); 168 + overflow-x: auto; 169 + scrollbar-width: none; 170 + margin-bottom: 2.5rem; 171 + } 172 + .tab-bar::-webkit-scrollbar { display: none; } 173 + .tab-btn { 174 + font-family: var(--font-sans); 175 + font-size: 0.875rem; 176 + font-weight: 500; 177 + color: var(--text-muted); 178 + background: none; 179 + border: none; 180 + border-bottom: 2px solid transparent; 181 + padding: 0.625rem 1.125rem; 182 + cursor: pointer; 183 + white-space: nowrap; 184 + margin-bottom: -1px; 185 + transition: color var(--transition), border-color var(--transition); 186 + } 187 + .tab-btn:hover { color: var(--text); } 188 + .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); } 189 + .tab-pane { display: none; padding-bottom: 4rem; } 190 + .tab-pane.active { display: block; } 191 + 192 + /* ── Section labels ── */ 193 + .section-label { 194 + font-size: 0.75rem; 195 + font-weight: 500; 196 + text-transform: uppercase; 197 + letter-spacing: 0.1em; 198 + color: var(--text-dim); 199 + margin-bottom: 1rem; 200 + } 201 + 202 + /* ── Prose ── */ 203 + .prose { 204 + font-size: 0.9375rem; 205 + color: var(--text-muted); 206 + line-height: 1.8; 207 + } 208 + .prose p + p { margin-top: 0.875rem; } 209 + .prose a { color: var(--accent); border-bottom: 1px solid var(--accent-dim); } 210 + .prose a:hover { border-bottom-color: var(--accent); opacity: 1; } 211 + .prose strong { color: var(--text); font-weight: 500; } 212 + 213 + /* ── Code blocks ── */ 214 + .code-block { 215 + position: relative; 216 + margin: 1rem 0; 217 + background: var(--bg-code); 218 + border: 1px solid var(--border); 219 + border-radius: var(--radius); 220 + overflow: hidden; 221 + } 222 + .code-block-header { 223 + display: flex; 224 + align-items: center; 225 + justify-content: space-between; 226 + padding: 0.5rem 0.875rem; 227 + background: var(--bg-raised); 228 + border-bottom: 1px solid var(--border); 229 + } 230 + .code-lang { 231 + font-size: 0.7rem; 232 + text-transform: uppercase; 233 + letter-spacing: 0.08em; 234 + color: var(--text-dim); 235 + font-family: var(--font-mono); 236 + } 237 + .code-copy { 238 + display: inline-flex; 239 + align-items: center; 240 + gap: 0.3rem; 241 + font-size: 0.7rem; 242 + color: var(--text-dim); 243 + background: none; 244 + border: none; 245 + cursor: pointer; 246 + font-family: var(--font-sans); 247 + padding: 0.2rem 0.5rem; 248 + border-radius: 4px; 249 + transition: color var(--transition), background var(--transition); 250 + } 251 + .code-copy:hover { color: var(--text-muted); background: var(--border); } 252 + .code-copy.copied { color: var(--green); } 253 + pre { 254 + font-family: var(--font-mono); 255 + font-size: 0.8125rem; 256 + line-height: 1.65; 257 + padding: 1rem 1.125rem; 258 + overflow-x: auto; 259 + color: var(--text-muted); 260 + scrollbar-width: thin; 261 + scrollbar-color: var(--border) transparent; 262 + } 263 + pre::-webkit-scrollbar { height: 4px; } 264 + pre::-webkit-scrollbar-track { background: transparent; } 265 + pre::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; } 266 + 267 + /* Syntax colouring */ 268 + .t-key { color: #b5a07a; } 269 + .t-str { color: #9cbf8a; } 270 + .t-num { color: #8ab3bf; } 271 + .t-bool { color: #bf8a8a; } 272 + .t-url { color: var(--accent); } 273 + .t-comm { color: var(--text-dim); font-style: italic; } 274 + .t-method { font-weight: 600; } 275 + .t-get { color: var(--green); } 276 + .t-post { color: var(--accent); } 277 + .t-del { color: var(--red); } 278 + 279 + /* ── Method badge ── */ 280 + .method { 281 + display: inline-block; 282 + padding: 0.15em 0.55em; 283 + border-radius: 4px; 284 + font-family: var(--font-mono); 285 + font-size: 0.7rem; 286 + font-weight: 600; 287 + letter-spacing: 0.05em; 288 + vertical-align: middle; 289 + margin-right: 0.4rem; 290 + } 291 + .method-get { background: var(--green-dim); color: var(--green); border: 1px solid rgba(76,175,125,0.25); } 292 + .method-post { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(201,169,110,0.25); } 293 + .method-delete { background: var(--red-dim); color: var(--red); border: 1px solid rgba(224,108,108,0.25); } 294 + 295 + /* ── Endpoint cards ── */ 296 + .endpoint { 297 + border: 1px solid var(--border); 298 + border-radius: var(--radius); 299 + margin-bottom: 1.25rem; 300 + overflow: hidden; 301 + transition: border-color var(--transition); 302 + } 303 + .endpoint:hover { border-color: var(--border-hover); } 304 + 305 + .endpoint-header { 306 + display: flex; 307 + align-items: flex-start; 308 + justify-content: space-between; 309 + gap: 1rem; 310 + padding: 1rem 1.125rem; 311 + cursor: pointer; 312 + user-select: none; 313 + background: var(--bg-card); 314 + } 315 + .endpoint-header:hover { background: var(--bg-raised); } 316 + 317 + .endpoint-title-row { 318 + display: flex; 319 + align-items: center; 320 + flex-wrap: wrap; 321 + gap: 0.5rem; 322 + } 323 + .endpoint-path { 324 + font-family: var(--font-mono); 325 + font-size: 0.875rem; 326 + color: var(--text); 327 + } 328 + .endpoint-desc { 329 + font-size: 0.8125rem; 330 + color: var(--text-muted); 331 + margin-top: 0.25rem; 332 + } 333 + 334 + .endpoint-toggle { 335 + flex-shrink: 0; 336 + color: var(--text-dim); 337 + font-size: 0.75rem; 338 + transition: transform var(--transition); 339 + margin-top: 0.125rem; 340 + } 341 + .endpoint.open .endpoint-toggle { transform: rotate(180deg); } 342 + 343 + .endpoint-body { 344 + display: none; 345 + border-top: 1px solid var(--border); 346 + padding: 1.125rem; 347 + background: var(--bg); 348 + } 349 + .endpoint.open .endpoint-body { display: block; } 350 + 351 + .param-table { 352 + width: 100%; 353 + border-collapse: collapse; 354 + font-size: 0.8125rem; 355 + margin-bottom: 1rem; 356 + } 357 + .param-table th { 358 + text-align: left; 359 + font-size: 0.7rem; 360 + text-transform: uppercase; 361 + letter-spacing: 0.07em; 362 + color: var(--text-dim); 363 + font-weight: 500; 364 + padding: 0 0.75rem 0.5rem; 365 + border-bottom: 1px solid var(--border); 366 + } 367 + .param-table th:first-child { padding-left: 0; } 368 + .param-table td { 369 + padding: 0.6rem 0.75rem; 370 + color: var(--text-muted); 371 + border-bottom: 1px solid var(--border); 372 + vertical-align: top; 373 + } 374 + .param-table td:first-child { padding-left: 0; } 375 + .param-table tr:last-child td { border-bottom: none; } 376 + .param-name { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text); } 377 + .param-type { color: var(--blue); font-family: var(--font-mono); font-size: 0.75rem; } 378 + .param-required { color: var(--red); font-size: 0.7rem; font-weight: 500; } 379 + .param-optional { color: var(--text-dim); font-size: 0.7rem; } 380 + 381 + .body-label { 382 + font-size: 0.75rem; 383 + font-weight: 500; 384 + text-transform: uppercase; 385 + letter-spacing: 0.07em; 386 + color: var(--text-dim); 387 + margin: 1rem 0 0.5rem; 388 + } 389 + .body-label:first-child { margin-top: 0; } 390 + 391 + /* ── Auth note ── */ 392 + .auth-note { 393 + display: inline-flex; 394 + align-items: center; 395 + gap: 0.4rem; 396 + font-size: 0.75rem; 397 + padding: 0.3rem 0.75rem; 398 + background: var(--accent-dim); 399 + border: 1px solid rgba(201,169,110,0.2); 400 + border-radius: 999px; 401 + color: var(--accent); 402 + } 403 + 404 + /* ── Info cards ── */ 405 + .info-card { 406 + background: var(--bg-card); 407 + border: 1px solid var(--border); 408 + border-radius: var(--radius); 409 + padding: 1.125rem 1.25rem; 410 + margin-bottom: 1rem; 411 + font-size: 0.875rem; 412 + color: var(--text-muted); 413 + } 414 + .info-card.highlight { 415 + border-left: 3px solid var(--accent); 416 + } 417 + .info-card.warning { 418 + border-left: 3px solid var(--red); 419 + } 420 + .info-card strong { color: var(--text); } 421 + 422 + /* ── Quick start steps ── */ 423 + .qs-steps { 424 + counter-reset: qs; 425 + display: flex; 426 + flex-direction: column; 427 + gap: 0; 428 + } 429 + .qs-step { 430 + display: grid; 431 + grid-template-columns: 2rem 1fr; 432 + gap: 0.875rem; 433 + padding: 1.25rem 0; 434 + border-bottom: 1px solid var(--border); 435 + counter-increment: qs; 436 + } 437 + .qs-step:last-child { border-bottom: none; } 438 + .qs-step-num { 439 + width: 2rem; 440 + height: 2rem; 441 + border-radius: 50%; 442 + background: var(--bg-card); 443 + border: 1px solid var(--accent-dim); 444 + display: flex; 445 + align-items: center; 446 + justify-content: center; 447 + font-size: 0.75rem; 448 + font-weight: 600; 449 + color: var(--accent); 450 + flex-shrink: 0; 451 + margin-top: 0.1rem; 452 + } 453 + .qs-step-content h3 { 454 + font-family: var(--font-serif); 455 + font-size: 1rem; 456 + font-weight: 400; 457 + color: var(--text); 458 + margin-bottom: 0.375rem; 459 + } 460 + .qs-step-content p { 461 + font-size: 0.875rem; 462 + color: var(--text-muted); 463 + margin-bottom: 0.625rem; 464 + } 465 + 466 + /* ── Limits table ── */ 467 + .limits-compare { 468 + width: 100%; 469 + border-collapse: collapse; 470 + font-size: 0.875rem; 471 + margin-bottom: 1.5rem; 472 + } 473 + .limits-compare th { 474 + text-align: left; 475 + font-size: 0.75rem; 476 + text-transform: uppercase; 477 + letter-spacing: 0.07em; 478 + color: var(--text-dim); 479 + font-weight: 500; 480 + padding: 0.625rem 1rem; 481 + border-bottom: 1px solid var(--border); 482 + background: var(--bg-card); 483 + } 484 + .limits-compare th:first-child { border-radius: var(--radius) 0 0 0; } 485 + .limits-compare th:last-child { border-radius: 0 var(--radius) 0 0; } 486 + .limits-compare td { 487 + padding: 0.75rem 1rem; 488 + border-bottom: 1px solid var(--border); 489 + color: var(--text-muted); 490 + } 491 + .limits-compare tr:last-child td { border-bottom: none; } 492 + .limits-compare td:first-child { color: var(--text); font-weight: 500; } 493 + .limits-compare .val-anon { color: var(--text-muted); } 494 + .limits-compare .val-api { color: var(--green); font-weight: 500; } 495 + .limits-table-wrap { 496 + border: 1px solid var(--border); 497 + border-radius: var(--radius); 498 + overflow: hidden; 499 + margin-bottom: 1.5rem; 500 + } 501 + 502 + /* ── Error codes ── */ 503 + .error-row { 504 + display: grid; 505 + grid-template-columns: 3.5rem 1fr; 506 + gap: 1rem; 507 + padding: 0.875rem 0; 508 + border-bottom: 1px solid var(--border); 509 + font-size: 0.875rem; 510 + } 511 + .error-row:last-child { border-bottom: none; } 512 + .error-code { 513 + font-family: var(--font-mono); 514 + font-weight: 600; 515 + color: var(--red); 516 + font-size: 0.8rem; 517 + } 518 + .error-code.ok { color: var(--green); } 519 + .error-code.warn { color: var(--accent); } 520 + .error-desc strong { color: var(--text); display: block; font-size: 0.875rem; margin-bottom: 0.125rem; } 521 + .error-desc { color: var(--text-muted); font-size: 0.8125rem; } 522 + 523 + /* ── Divider ── */ 524 + .divider { border: none; border-top: 1px solid var(--border); margin: 2rem 0; } 525 + 526 + /* ── Footer ── */ 527 + .site-footer { 528 + max-width: var(--max-w); 529 + margin: 0 auto; 530 + padding: 1.75rem 1.5rem 2.5rem; 531 + border-top: 1px solid var(--border); 532 + font-size: 0.8125rem; 533 + color: var(--text-dim); 534 + } 535 + .site-footer a { color: var(--text-muted); } 536 + .site-footer a:hover { color: var(--text); opacity: 1; } 537 + 538 + /* ── Responsive ── */ 539 + @media (max-width: 600px) { 540 + .nav-title { display: none; } 541 + .hero-badges { flex-wrap: wrap; } 542 + } 543 + </style> 544 + </head> 545 + <body> 546 + 547 + <header class="site-header"> 548 + <nav class="nav-container"> 549 + <a href="/" class="nav-logo">Daniel Morrisey <i>.com</i></a> 550 + <span class="nav-title">MBD CDN — Developer API</span> 551 + </nav> 552 + </header> 553 + 554 + <main class="main-content"> 555 + 556 + <section class="page-hero"> 557 + <p class="page-hero-eyebrow">madebydanny.uk / cdn</p> 558 + <h1>Developer API</h1> 559 + <p>A REST API for programmatic access to MBD CDN — upload files, manage your library, and check usage from your own apps and scripts.</p> 560 + <div class="hero-badges"> 561 + <span class="badge"><a href="/cdn.html" style="color:inherit; border:none; text-decoration:none;">← Back to CDN</a></span> 562 + </div> 563 + </section> 564 + 565 + <section class="tabs-section"> 566 + <div class="tab-bar"> 567 + <button class="tab-btn active" onclick="switchTab('quickstart', this)"> 568 + <i class="fa-solid fa-bolt"></i> Quick Start 569 + </button> 570 + <button class="tab-btn" onclick="switchTab('auth', this)"> 571 + <i class="fa-solid fa-key"></i> Authentication 572 + </button> 573 + <button class="tab-btn" onclick="switchTab('endpoints', this)"> 574 + <i class="fa-solid fa-circle-nodes"></i> Endpoints 575 + </button> 576 + <button class="tab-btn" onclick="switchTab('limits', this)"> 577 + <i class="fa-solid fa-gauge-high"></i> Rate Limits 578 + </button> 579 + <button class="tab-btn" onclick="switchTab('errors', this)"> 580 + <i class="fa-solid fa-triangle-exclamation"></i> Errors 581 + </button> 582 + </div> 583 + 584 + <div class="tab-pane active" id="tab-quickstart"> 585 + 586 + <p class="section-label">base url</p> 587 + <div class="code-block" style="margin-bottom:2rem"> 588 + <div class="code-block-header"> 589 + <span class="code-lang">text</span> 590 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 591 + </div> 592 + <pre>https://cdn.madebydanny.uk</pre> 593 + </div> 594 + 595 + <p class="section-label">make your first request in 60 seconds</p> 596 + <div class="qs-steps"> 597 + 598 + <div class="qs-step"> 599 + <div class="qs-step-num">1</div> 600 + <div class="qs-step-content"> 601 + <h3>Get an API key</h3> 602 + <p>Reach out via <a href="https://bsky.app/profile/danielmorrisey.com">Bluesky</a> DMs to request a key, with your <code>APP_NAME</code> and the <code>KEY_OWNER</code>. Keys look like <code>mbd_</code> followed by 64 hex characters.</p> 603 + </div> 604 + </div> 605 + 606 + <div class="qs-step"> 607 + <div class="qs-step-num">2</div> 608 + <div class="qs-step-content"> 609 + <h3>Upload a file</h3> 610 + <p>Send the raw file body as a <code>POST</code> with the correct <code>Content-Type</code>. Pass your key in the <code>Authorization</code> header.</p> 611 + <div class="code-block"> 612 + <div class="code-block-header"> 613 + <span class="code-lang">bash — curl</span> 614 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 615 + </div> 616 + <pre>curl -X POST https://cdn.madebydanny.uk/v1/upload \ 617 + -H <span class="t-str">"Authorization: Bearer mbd_your_key_here"</span> \ 618 + -H <span class="t-str">"Content-Type: image/png"</span> \ 619 + --data-binary @photo.png</pre> 620 + </div> 621 + </div> 622 + </div> 623 + 624 + <div class="qs-step"> 625 + <div class="qs-step-num">3</div> 626 + <div class="qs-step-content"> 627 + <h3>Get your public URL</h3> 628 + <p>A successful upload returns a JSON object. The <code>url</code> field is your permanent public link — ready to embed anywhere.</p> 629 + <div class="code-block"> 630 + <div class="code-block-header"> 631 + <span class="code-lang">json — response</span> 632 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 633 + </div> 634 + <pre>{ 635 + <span class="t-key">"success"</span>: <span class="t-bool">true</span>, 636 + <span class="t-key">"url"</span>: <span class="t-str">"https://cdn.madebydanny.uk/user-content/2026-04-29/abc123.png"</span>, 637 + <span class="t-key">"id"</span>: <span class="t-str">"abc12345-1234-1234-1234-abc123456789"</span>, 638 + <span class="t-key">"contentType"</span>: <span class="t-str">"image/png"</span>, 639 + <span class="t-key">"fileType"</span>: <span class="t-str">"image"</span>, 640 + <span class="t-key">"size"</span>: <span class="t-num">204800</span> 641 + }</pre> 642 + </div> 643 + </div> 644 + </div> 645 + 646 + <div class="qs-step"> 647 + <div class="qs-step-num">4</div> 648 + <div class="qs-step-content"> 649 + <h3>Use the URL anywhere</h3> 650 + <p>Files are served over Cloudflare's global edge with <code>Cache-Control: immutable</code>. Embed in HTML, markdown, social posts, or wherever you like.</p> 651 + <div class="code-block"> 652 + <div class="code-block-header"> 653 + <span class="code-lang">html</span> 654 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 655 + </div> 656 + <pre>&lt;img src=<span class="t-str">"https://cdn.madebydanny.uk/user-content/2026-04-29/abc123.png"</span> 657 + alt=<span class="t-str">"My photo"</span>&gt;</pre> 658 + </div> 659 + </div> 660 + </div> 661 + 662 + </div> 663 + 664 + <hr class="divider"> 665 + 666 + <p class="section-label">javascript example</p> 667 + <div class="code-block"> 668 + <div class="code-block-header"> 669 + <span class="code-lang">javascript — fetch</span> 670 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 671 + </div> 672 + <pre><span class="t-comm">// Upload a File object (e.g. from an &lt;input type="file"&gt;)</span> 673 + async function uploadToCDN(file, apiKey) { 674 + const res = await fetch(<span class="t-str">'https://cdn.madebydanny.uk/v1/upload'</span>, { 675 + method: <span class="t-str">'POST'</span>, 676 + headers: { 677 + <span class="t-str">'Authorization'</span>: <span class="t-str">`Bearer ${apiKey}`</span>, 678 + <span class="t-str">'Content-Type'</span>: file.type, 679 + }, 680 + body: file, 681 + }); 682 + 683 + const data = await res.json(); 684 + if (!data.success) throw new Error(data.error); 685 + return data.url; <span class="t-comm">// permanent public URL</span> 686 + } 687 + 688 + <span class="t-comm">// Usage</span> 689 + const url = await uploadToCDN(fileInputEl.files[0], <span class="t-str">'mbd_your_key_here'</span>); 690 + console.log(url); <span class="t-comm">// https://cdn.madebydanny.uk/user-content/...</span></pre> 691 + </div> 692 + 693 + </div> 694 + 695 + <div class="tab-pane" id="tab-auth"> 696 + 697 + <div class="prose" style="margin-bottom:1.5rem"> 698 + <p>All <code>/v1/</code> routes require an API key sent as a <strong>Bearer token</strong> in the <code>Authorization</code> header. Anonymous uploads via <code>POST /</code> continue to work without any key.</p> 699 + </div> 700 + 701 + <p class="section-label">sending your key</p> 702 + <div class="code-block" style="margin-bottom:1.5rem"> 703 + <div class="code-block-header"> 704 + <span class="code-lang">http header</span> 705 + <button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button> 706 + </div> 707 + <pre>Authorization: Bearer mbd_your_key_here</pre> 708 + </div> 709 + 710 + <div class="info-card highlight" style="margin-bottom:1.5rem"> 711 + <strong>Keep your key secret.</strong> Treat it like a password — don't commit it to public repos or include it in client-side code. Store it in environment variables or a secrets manager. 712 + </div> 713 + 714 + <p class="section-label">key format</p> 715 + <div class="info-card"> 716 + Keys are prefixed with <code>mbd_</code> followed by <strong>64 hex characters</strong> (32 random bytes). Example: <code>mbd_a3f1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2</code> 717 + </div> 718 + 719 + </div> 720 + 721 + <div class="tab-pane" id="tab-endpoints"> 722 + 723 + <p class="section-label" style="margin-bottom:1rem">file operations</p> 724 + 725 + <div class="endpoint" id="ep-upload"> 726 + <div class="endpoint-header" onclick="toggleEndpoint('ep-upload')"> 727 + <div> 728 + <div class="endpoint-title-row"> 729 + <span class="method method-post">POST</span> 730 + <span class="endpoint-path">/v1/upload</span> 731 + <span class="auth-note"><i class="fa-solid fa-key"></i> API key</span> 732 + </div> 733 + <div class="endpoint-desc">Upload a file and receive a permanent public URL.</div> 734 + </div> 735 + <i class="fa-solid fa-chevron-down endpoint-toggle"></i> 736 + </div> 737 + <div class="endpoint-body"> 738 + <div class="body-label">Headers</div> 739 + <table class="param-table"> 740 + <thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr></thead> 741 + <tbody> 742 + <tr> 743 + <td><span class="param-name">Authorization</span></td> 744 + <td><span class="param-type">string</span></td> 745 + <td><span class="param-required">required</span></td> 746 + <td>Bearer token — <code>Bearer mbd_…</code></td> 747 + </tr> 748 + <tr> 749 + <td><span class="param-name">Content-Type</span></td> 750 + <td><span class="param-type">string</span></td> 751 + <td><span class="param-required">required</span></td> 752 + <td>MIME type of the file, e.g. <code>image/png</code></td> 753 + </tr> 754 + <tr> 755 + <td><span class="param-name">Content-Length</span></td> 756 + <td><span class="param-type">integer</span></td> 757 + <td><span class="param-optional">optional</span></td> 758 + <td>File size in bytes. Recommended — allows accurate quota pre-checks.</td> 759 + </tr> 760 + </tbody> 761 + </table> 762 + <div class="body-label">Body</div> 763 + <div class="info-card" style="margin-bottom:1rem;font-size:0.8125rem;">Raw file bytes. Send the binary file body directly — no multipart encoding, no JSON wrapper.</div> 764 + <div class="body-label">Response — 200 OK</div> 765 + <div class="code-block"> 766 + <div class="code-block-header"><span class="code-lang">json</span><button class="code-copy" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button></div> 767 + <pre>{ 768 + <span class="t-key">"success"</span>: <span class="t-bool">true</span>, 769 + <span class="t-key">"url"</span>: <span class="t-str">"https://cdn.madebydanny.uk/user-content/2026-04-29/&lt;uuid&gt;.png"</span>, 770 + <span class="t-key">"id"</span>: <span class="t-str">"&lt;uuid&gt;"</span>, 771 + <span class="t-key">"path"</span>: <span class="t-str">"user-content/2026-04-29/&lt;uuid&gt;.png"</span>, 772 + <span class="t-key">"contentType"</span>: <span class="t-str">"image/png"</span>, 773 + <span class="t-key">"fileType"</span>: <span class="t-str">"image"</span>, 774 + <span class="t-key">"size"</span>: <span class="t-num">204800</span> 775 + }</pre> 776 + </div> 777 + <div class="body-label" style="margin-top:0.875rem">Rate-limit response headers</div> 778 + <table class="param-table"> 779 + <thead><tr><th>Header</th><th>Description</th></tr></thead> 780 + <tbody> 781 + <tr><td><span class="param-name">X-RateLimit-Limit-Files</span></td><td>Max files per day for this key</td></tr> 782 + <tr><td><span class="param-name">X-RateLimit-Remaining-Files</span></td><td>Files remaining today</td></tr> 783 + <tr><td><span class="param-name">X-RateLimit-Limit-Bytes</span></td><td>Max bytes per day for this key</td></tr> 784 + <tr><td><span class="param-name">X-RateLimit-Remaining-Bytes</span></td><td>Bytes remaining today</td></tr> 785 + <tr><td><span class="param-name">X-RateLimit-Reset</span></td><td>Unix timestamp when quota resets (midnight UTC)</td></tr> 786 + </tbody> 787 + </table> 788 + </div> 789 + </div> 790 + 791 + <div class="endpoint" id="ep-list"> 792 + <div class="endpoint-header" onclick="toggleEndpoint('ep-list')"> 793 + <div> 794 + <div class="endpoint-title-row"> 795 + <span class="method method-get">GET</span> 796 + <span class="endpoint-path">/v1/files</span> 797 + <span class="auth-note"><i class="fa-solid fa-key"></i> API key</span> 798 + </div> 799 + <div class="endpoint-desc">List files uploaded with this API key, newest first.</div> 800 + </div> 801 + <i class="fa-solid fa-chevron-down endpoint-toggle"></i> 802 + </div> 803 + <div class="endpoint-body"> 804 + <div class="body-label">Query Parameters</div> 805 + <table class="param-table"> 806 + <thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr></thead> 807 + <tbody> 808 + <tr> 809 + <td><span class="param-name">limit</span></td> 810 + <td><span class="param-type">integer</span></td> 811 + <td><span class="param-optional">optional</span></td> 812 + <td>Number of files to return (default 50, max 100).</td> 813 + </tr> 814 + <tr> 815 + <td><span class="param-name">cursor</span></td> 816 + <td><span class="param-type">string</span></td> 817 + <td><span class="param-optional">optional</span></td> 818 + <td>Pagination cursor from previous response.</td> 819 + </tr> 820 + </tbody> 821 + </table> 822 + </div> 823 + </div> 824 + 825 + </div> 826 + 827 + <div class="tab-pane" id="tab-limits"> 828 + <p class="section-label">tier comparison</p> 829 + <div class="limits-table-wrap"> 830 + <table class="limits-compare"> 831 + <thead> 832 + <tr> 833 + <th>Limit</th> 834 + <th>Anonymous (No Key)</th> 835 + <th>API Key</th> 836 + </tr> 837 + </thead> 838 + <tbody> 839 + <tr> 840 + <td>Max file size</td> 841 + <td class="val-anon">5 MB</td> 842 + <td class="val-api">25 MB</td> 843 + </tr> 844 + <tr> 845 + <td>Files per day</td> 846 + <td class="val-anon">20</td> 847 + <td class="val-api">1,000</td> 848 + </tr> 849 + <tr> 850 + <td>Bandwidth per day</td> 851 + <td class="val-anon">100 MB</td> 852 + <td class="val-api">5 GB</td> 853 + </tr> 854 + </tbody> 855 + </table> 856 + </div> 857 + <div class="info-card"> 858 + Rate limits reset at <strong>midnight UTC</strong>. If you need higher limits for a specific project, please reach out via Bluesky. You can check your remaining limits at any time by viewing the <code>X-RateLimit-*</code> response headers returned after a successful upload. 859 + </div> 860 + </div> 861 + 862 + <div class="tab-pane" id="tab-errors"> 863 + <p class="section-label">http status codes</p> 864 + <div style="border-top: 1px solid var(--border);"> 865 + <div class="error-row"> 866 + <span class="error-code ok">200</span> 867 + <span class="error-desc"><strong>OK</strong><br>Everything worked as expected.</span> 868 + </div> 869 + <div class="error-row"> 870 + <span class="error-code">400</span> 871 + <span class="error-desc"><strong>Bad Request</strong><br>Missing file, unsupported content type, or the payload is too large. Check the response body for details.</span> 872 + </div> 873 + <div class="error-row"> 874 + <span class="error-code">401</span> 875 + <span class="error-desc"><strong>Unauthorized</strong><br>Missing, malformed, or invalid API key. Ensure you are passing the key exactly as <code>Bearer mbd_...</code>.</span> 876 + </div> 877 + <div class="error-row"> 878 + <span class="error-code warn">429</span> 879 + <span class="error-desc"><strong>Too Many Requests</strong><br>You have hit your daily quota for either files or bytes. Check the <code>X-RateLimit</code> headers to see when your limit resets.</span> 880 + </div> 881 + <div class="error-row"> 882 + <span class="error-code">500</span> 883 + <span class="error-desc"><strong>Internal Server Error</strong><br>Something went wrong on our end (e.g., an issue connecting to the R2 bucket or D1 database). Try again later.</span> 884 + </div> 885 + </div> 886 + </div> 887 + 888 + </section> 889 + </main> 890 + 891 + <script> 892 + function switchTab(tabId, btn) { 893 + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); 894 + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 895 + document.getElementById('tab-' + tabId).classList.add('active'); 896 + btn.classList.add('active'); 897 + } 898 + 899 + function toggleEndpoint(epId) { 900 + document.getElementById(epId).classList.toggle('open'); 901 + } 902 + 903 + function copyCode(btn) { 904 + const codeBlock = btn.closest('.code-block').querySelector('pre'); 905 + navigator.clipboard.writeText(codeBlock.innerText).then(() => { 906 + const originalHtml = btn.innerHTML; 907 + btn.innerHTML = '<i class="fa-solid fa-check"></i> Copied'; 908 + btn.classList.add('copied'); 909 + setTimeout(() => { 910 + btn.innerHTML = originalHtml; 911 + btn.classList.remove('copied'); 912 + }, 2000); 913 + }); 914 + } 915 + </script> 916 + </body> 917 + </html>