1<!doctype html> 2<html lang="en" translate="no"> 3 <head> 4 <link rel="canonical" href="https://onefap.ai/collection" /> 5 <!-- First, ahead of every preload. Until a phone has read this it lays the 6 page out 980px wide, and the preload scanner answers the hero preloads' 7 media queries for that width: a phone fetched the desktop 2x picture at 8 high priority, ahead of the stylesheet, and never preloaded the one it 9 paints. --> 10 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> 11 <!-- Block browser-side auto-translation: Chromium wraps text nodes in 12 <font> tags when the OS locale differs from the page lang, which makes 13 React's reconciler throw on the next render. --> 14 <meta name="google" content="notranslate" /> 15 16 <!-- Nothing may put this page inside a frame. 17 18 The proper way to say so is a header the browser enforces before a 19 single byte is drawn â `X-Frame-Options: SAMEORIGIN`, or the 20 `frame-ancestors` line of a Content-Security-Policy â and neither can 21 be written from here: a header is the host's to send, and a `<meta>` 22 carries neither. Both are in the README, in the section about the 23 static site's settings, and adding them there is what actually closes 24 this. A framed storefront is a decoy button over a real one, on a site 25 with a payment on nearly every screen. 26 27 Until the header exists, this â and NOT the frame-buster it started 28 as. Navigating the parent out (`top.location = â¦`) is refused by every 29 current browser when the framer is a different origin and nobody 30 clicked anything: measured here, the escape ran and nothing moved. So 31 the page does the one thing a framed document can always do â it 32 declines to draw itself, and offers the address instead. There is then 33 no interface inside the frame to dress up, which is the whole of what 34 clickjacking needs. 35 36 The escape is still attempted, because where it is allowed it is the 37 better outcome: the visitor lands on the real site rather than on a 38 notice. First script in the head, before the theme script and long before the 39 bundle, so this is settled while there is nothing on screen. --> 40 <style> 41 /* Declared here rather than in globals.css: the stylesheet is a separate 42 request, and a frame must be answered during the parse of this file. */ 43 #framed-note { display: none; } 44 html.is-framed #root { display: none !important; } 45 html.is-framed #framed-note { 46 display: block; 47 margin: 0; 48 padding: 2.5rem 1.5rem; 49 text-align: center; 50 font: 600 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; 51 color: #f4f2f8; 52 background: #0a0b0c; 53 min-height: 100vh; 54 } 55 html.is-framed #framed-note a { color: #b49bff; } 56 </style> 57
57<script> 58 (function () { 59 var framed; 60 try { 61 framed = window.top !== window.self; 62 } catch (e) { 63 // Reading across origins throws in some browsers, and being unable to 64 // look is itself the answer: somebody else's document is on top. 65 framed = true; 66 } 67 if (!framed) return; 68 document.documentElement.classList.add('is-framed'); 69 try { 70 window.top.location = window.self.location.href; 71 } catch (e) { 72 /* refused, which is the ordinary case â the notice below is the answer */ 73 } 74 })(); 75 </script>
75 76 77 <!-- The colour iOS paints the bar above the page and the one Android puts 78 behind the status bar. It sits here, before the bootstrap below rather 79 than with the other metas further down, because that script is what 80 fills it in: it has just decided which palette the page is wearing and 81 is the only thing that knows before the first paint. Left as a literal 82 it was #0a0b0c â the stock dark theme's ground â while the same script 83 was applying lux, whose ground is #12091F, so the bar and the page 84 under it were two different colours on every phone. 85 86 The content here is only what shows if that script cannot run at all. --> 87 <meta name="theme-color" content="#0a0b0c" /> 88 89 <!-- No-FOUC theme bootstrap: runs synchronously before any paint. Dark 90 unconditionally, because the header's day/night toggle is gone: reading 91 prefers-color-scheme without it would drop a visitor whose system is 92 set to light into the light theme with nothing to switch back, and an 93 old stored 'light' would strand them there for good. The light palette 94 is still in globals.css â restoring that choice means restoring the 95 toggle and a read of the stored theme alongside the palette read 96 below. --> 97
97<script> 98 (function () { 99 document.documentElement.classList.add('dark'); 100 101 // The colour theme (see PaletteSwitcher, which is where a visitor 102 // changes it). Applied here rather than in React so it is on the 103 // element before the first paint â mounted, it would repaint the whole 104 // page a frame late. 105 // 106 // The default is applied first and outside the try, so a browser with 107 // storage blocked still gets it. 'lux' is the second palette in the 108 // switcher's row and DEFAULT_PALETTE there; the two are written out 109 // separately because this script runs before any module and cannot 110 // import one, so they have to be changed together. 111 // 112 // The ground of each palette, beside its id, because the browser bar 113 // has to be painted in the same breath: the stylesheet that holds 114 // `--background` is linked further down this head and has not been 115 // parsed yet, so there is nothing to read it off. Mirrors the 116 // `:root[data-palette=â¦]` blocks in globals.css, and only has to be 117 // right for the first frame â utils/themeColor reconciles the meta 118 // against the real computed value once the stylesheet lands, so a 119 // palette repainted in CSS corrects itself rather than drifting. 120 var GROUND = { '': '#0a0b0c', lux: '#12091F', neon: '#0A0713', minimal: '#111111' }; 121 122 function wear(id) { 123 if (id) document.documentElement.setAttribute('data-palette', id); 124 else document.documentElement.removeAttribute('data-palette'); 125 var bar = document.querySelector('meta[name="theme-color"]'); 126 if (bar && GROUND[id]) bar.setAttribute('content', GROUND[id]); 127 } 128 129 wear('lux'); 130 try { 131 // `=== null` rather than a truth test: the stock theme is stored as 132 // the empty string, and treating that as "nothing stored" would put 133 // the default back over a choice somebody made. 134 var p = localStorage.getItem('onef_palette'); 135 if (p !== null) wear(p); 136 137 // Second alphabet, warmed here rather than in the markup below. 138 // The latin cut is preloaded unconditionally further down because 139 // every visitor spells something in it; the cyrillic and arabic cuts 140 // are needed by two languages out of seven, and a static <link> for 141 // them would cost every other visitor a download they never render. 142 // The stored language is the only thing that knows which, and it is 143 // readable here â one paint earlier than React could ask. 144 // Somebody who has already confirmed their age must not be shown the 145 // gate again, and the prerendered pages carry it in their markup â 146 // they are rendered for a first-time visitor, which is who a crawler 147 // is. Without this the returning visitor watches it sit there for the 148 // second or two the bundle takes to arrive and re-render without it. 149 // A class rather than a node removal: this runs in <head>, before the 150 // body it would have to reach into exists. globals.css does the rest. 151 if (localStorage.getItem('onef_age_ok') === '1') { 152 document.documentElement.classList.add('age-ok'); 153 } 154 155 var lang = localStorage.getItem('onef_lang'); 156 var cut = { ru: 'inter-cyrillic-v20', ar: 'noto-sans-arabic-v33', th: 'noto-sans-thai-v29', vi: 'inter-vietnamese-v20' }[lang]; 157 if (cut) { 158 var link = document.createElement('link'); 159 link.rel = 'preload'; 160 link.as = 'font'; 161 link.type = 'font/woff2'; 162 // Fonts are fetched anonymously even from our own origin; without 163 // this the preload lands in a different cache partition than the 164 // CSS-driven request and the file is downloaded twice. 165 link.crossOrigin = 'anonymous'; 166 link.href = '/fonts/' + cut + '.woff2'; 167 document.head.appendChild(link); 168 } 169 170 // Japanese and Korean are not one file but a hundred and twenty, so 171 // what gets warmed here is the stylesheet that declares them rather 172 // than any one cut â which of them a screen of text lands in is the 173 // browser's to work out from the ranges, and it cannot start until it 174 // has them. Bundling those declarations instead would put 53 KB 175 // gzipped in front of the first paint for the twelve languages that 176 // never render a glyph out of either table. 177 // 178 // A real stylesheet link rather than a preload: it has to apply, not 179 // just arrive, and this is the earliest point at which anything knows 180 // the language. The switcher does the same for a change made 181 // mid-session â see utils/cjkFonts, which will not add a second one. 182 var table = { ja: 'noto-sans-jp', ko: 'noto-sans-kr' }[lang]; 183 if (table) { 184 var sheet = document.createElement('link'); 185 sheet.rel = 'stylesheet'; 186 sheet.href = '/fonts/' + table + '.css'; 187 document.head.appendChild(sheet); 188 } 189 } catch (e) { 190 /* storage blocked â the default palette above is already on */ 191 } 192 })(); 193 </script>
193 194 195 <meta charset="UTF-8" /> 196 197 <!-- A phone code, taken out of the address before anything reads it. 198 199 The desktop's "Open on phone" code carries a one-time sign-in in the 200 fragment: `#ho=<code>`, and `&back=<the page's own fragment>` when it 201 had one. A fragment is never sent in a request, so the code is in no 202 access log; this lifts it out of the address as well, during the 203 parse, before any module, referrer or history entry of ours can see 204 it, puts the page's own fragment back, and parks the code for 205 AuthContext to spend. Only a fragment that starts with `ho=` is ours â 206 an ordinary anchor is never read as parameters. --> 207
207<script> 208 (function () { 209 var hash = location.hash.slice(1); 210 if (hash.indexOf('ho=') !== 0) return; 211 var parts = hash.split('&'); 212 var code = null; 213 var back = ''; 214 for (var i = 0; i < parts.length; i++) { 215 var eq = parts[i].indexOf('='); 216 var key = parts[i].slice(0, eq); 217 var value = parts[i].slice(eq + 1); 218 try { 219 if (key === 'ho') code = decodeURIComponent(value); 220 else if (key === 'back') back = decodeURIComponent(value); 221 } catch (e) { 222 // Malformed: dropped from the address all the same. 223 } 224 } 225 if (code) window.__phoneHandoff = code; 226 try { 227 history.replaceState(history.state, '', location.pathname + location.search + (back ? '#' + back : '')); 228 } catch (e2) { 229 // History refused (a sandboxed view). AuthContext tries again. 230 } 231 })(); 232 </script>
232 233 234 <!-- The Telegram sign-in, started before the bundle that would otherwise 235 start it. 236 237 Telegram sends the browser back here with a code, and turning that code 238 into a session is a round trip our own API answers in about six tenths 239 of a second before it has even called Telegram. Left to the app, that 240 wait begins only once 340 KB has downloaded, parsed and mounted â two 241 waits in series, with a page of ours on screen for the sum of them, and 242 no page of ours is what a redirect is supposed to look like. 243 244 So the exchange starts here, during the parse, and the address is 245 rewritten to the catalogue before the router ever reads it: the app 246 boots on the site the visitor was going to, and the sign-in lands under 247 it a moment later. The route below still exists and still works â it is 248 simply no longer what anybody sees. --> 249
249<script> 250 (function () { 251 // Kept in step with the route in App.tsx by hand: this runs before any 252 // module and cannot import a constant. 253 if (location.pathname !== '/auth/telegram') return; 254 255 var api = 'https://api.onefap.ai/api'; 256 // Unsubstituted, or same-origin: posting to it would spend nothing and 257 // the code would already be gone from the address bar. Untouched, the 258 // route mounts and does the exchange itself â later, but correctly. 259 if (!/^https?:\/\//.test(api)) return; 260 261 var query = new URLSearchParams(location.search); 262 var code = query.get('code'); 263 264 /** 265 * Which of the two journeys this is. 266 * 267 * Signing in and linking come back to the same address â Telegram has 268 * one redirect registered â and the note is written by whichever button 269 * started it. It is read (and cleared) even when there is no code, 270 * because a visitor who pressed Cancel in Telegram must not leave a 271 * stale note behind for the next sign-in to trip over. 272 */ 273 var linkKey = 'onef_tg_link_intent'; 274 var linkBack = null; 275 try { 276 // Read, NOT removed. The note is only spent once this script commits 277 // to handling the callback: every path that hands the work back to 278 // the route has to leave it in place, or the route sees a sign-in 279 // where a link was asked for. 280 linkBack = localStorage.getItem(linkKey); 281 } catch (e) { 282 // Storage refused. Treated as a sign-in, which is what this did 283 // before linking existed. 284 } 285 286 // Cancelled in Telegram, or an old address opened again: there is 287 // nothing to spend, and the route is where that is answered. The note 288 // goes, though â left behind, the next ordinary sign-in on this browser 289 // would be rerouted into a link and refused. 290 if (!code) { 291 try { 292 if (linkBack !== null) localStorage.removeItem(linkKey); 293 } catch (e2) { 294 // Nothing to clean up, then. 295 } 296 return; 297 } 298 299 var token = null; 300 if (linkBack !== null) { 301 try { 302 token = localStorage.getItem('onef_auth'); 303 } catch (e3) { 304 token = null; 305 } 306 } 307 308 /** 309 * A link with no session left to attach to. 310 * 311 * The session expired while the visitor was in Telegram. Falling 312 * through to the sign-in call would create a brand-new account from 313 * their Telegram identity â and that identity is then owned by it for 314 * good, so the account they meant to link can never have it. 315 * 316 * Handed back to the route WITH the note still in storage: the route 317 * reads the same key, sees a link it cannot complete, and says so. This 318 * used to delete the note first, which meant the route read nothing, 319 * decided it was an ordinary sign-in, and created exactly the account 320 * these ten lines exist to prevent. 321 */ 322 if (linkBack !== null && !token) return; 323 324 // Committed now: this script is making the call, so the note is spent. 325 try { 326 if (linkBack !== null) localStorage.removeItem(linkKey); 327 } catch (e4) { 328 // The note stays; the route will find it and answer a second time, 329 // which is a wasted exchange rather than a wrong one. 330 } 331 332 var linking = linkBack !== null; 333 334 /** 335 * Where a sign-in was started: written by the button that sent the 336 * visitor to Telegram (see utils/signInReturn) â to localStorage, or 337 * to a cookie where that is refused â read and forgotten here, with the same checks the app makes â a path on this site that 338 * is not this callback, and no older than half an hour. Without it 339 * everybody landed on the catalogue: the profile they pressed Write 340 * on, the template they pressed Generate on and the promo code a QR 341 * code had filled in were all left behind in Telegram. 342 */ 343 var back = null; 344 if (!linking) { 345 try { 346 var backKey = 'onef_signin_back'; 347 var read = function (raw) { 348 try { 349 var note = JSON.parse(raw || 'null'); 350 return note && typeof note.to === 'string' && typeof note.at === 'number' ? note : null; 351 } catch (e7) { 352 return null; 353 } 354 }; 355 var stored = null; 356 try { 357 stored = read(localStorage.getItem(backKey)); 358 } catch (e6) { 359 stored = null; 360 }
361 // Apart from the read: a note that was read stays read even if 362 // storage then refuses to forget it. 363 try { 364 localStorage.removeItem(backKey); 365 } catch (e8) { 366 /* nothing to do */ 367 } 368 // Where localStorage is refused the note was left in a cookie. Both 369 // are read and the newer wins, as in utils/signInReturn. 370 var baked = null; 371 var jar = document.cookie.split('; '); 372 for (var j = 0; j < jar.length; j++) { 373 if (jar[j].indexOf(backKey + '=') === 0) { 374 baked = read(decodeURIComponent(jar[j].slice(backKey.length + 1))); 375 document.cookie = backKey + '=; Max-Age=0; Path=/; SameSite=Lax'; 376 } 377 } 378 var saved = stored && baked ? (baked.at > stored.at ? baked : stored) : stored || baked; 379 if ( 380 saved && 381 Date.now() - saved.at < 30 * 60000 && 382 saved.to.charAt(0) === '/' && 383 saved.to.charAt(1) !== '/' && 384 saved.to.indexOf('/auth/') !== 0 385 ) { 386 back = saved.to; 387 } 388 } catch (e5) { 389 back = null; 390 } 391 } 392 393 var headers = { 'content-type': 'application/json', accept: 'application/json' }; 394 if (linking) headers.authorization = 'Bearer ' + token; 395 396 window.__telegramExchange = { 397 // Which endpoint answered, so the bundle knows whether it is holding 398 // a session or a confirmation that an identity was attached. 399 kind: linking ? 'link' : 'signin', 400 // Where the visitor was when they pressed the button, so they land 401 // back on it rather than on the catalogue. 402 returnPath: linkBack || null, 403 // Kept because the line at the bottom is about to erase them, and the 404 // other site's leg replays this address on that origin â verbatim, 405 // rather than rebuilt from a copy of the route that can go stale. 406 path: location.pathname, 407 search: location.search, 408 // Raced against a deadline, because not failing is not the same as 409 // answering. The bundle turns this promise into the signed-in header 410 // and shows a placeholder in the meantime; a fetch that hangs â a 411 // captive portal, a proxy holding the socket, a phone that changed 412 // network mid-redirect â neither resolves nor rejects, so the 413 // placeholder is what the visitor keeps. Not a spinner that ends in 414 // an error: a pulsing grey pill where the sign-in button belongs, for 415 // as long as the tab is open. 416 // 417 // Fifteen seconds: our own API answers this in about six tenths of a 418 // second before it has even called Telegram, so anything past this is 419 // not slow, it is gone. Losing the race reads as a refusal, and the 420 // bundle already knows what to do with one â it reopens the sign-in 421 // dialog, where the visitor can simply press the button again. 422 started: Promise.race([ 423 fetch(api + (linking ? '/auth/telegram/link' : '/auth/telegram/oidc'), { 424 method: 'POST', 425 headers: headers, 426 body: JSON.stringify({ code: code, state: query.get('state') || '' }), 427 }).then( 428 function (res) { 429 return res.json().then( 430 function (body) { 431 return { ok: res.ok, body: body }; 432 }, 433 function () { 434 return { ok: false, body: null }; 435 }, 436 ); 437 }, 438 function () { 439 // A rejection lands here rather than on the consumer's catch, 440 // so both ways of failing arrive in the same shape. 441 return { ok: false, body: null }; 442 }, 443 ), 444 new Promise(function (resolve) { 445 setTimeout(function () { 446 // `timedOut`, not a bare refusal: the race does not abort the 447 // request, so the server may well be finishing the job. The 448 // consumer needs to tell "it said no" from "it did not answer 449 // in time", because for a link those two want opposite screens. 450 resolve({ ok: false, body: null, timedOut: true }); 451 }, 15000); 452 }), 453 ]), 454 }; 455 456 // Before the first module runs, so the router opens on the catalogue â 457 // or, for a link, on the screen that asked for it â and the callback 458 // route never mounts. It also takes the code out of the address bar a 459 // second earlier than the app could: it is a live credential, and it 460 // rides into every referrer, every screenshot and every history entry 461 // it is left in. 462 history.replaceState(null, '', linkBack || back || '/'); 463 })(); 464 </script>
464 465 466 <!-- The first screen talks to three origins in turn â this one, the API and 467 the media CDN â and each is a fresh DNS+TCP+TLS: the three hostnames 468 hold single-name certificates, so a browser cannot reuse the document's 469 socket for them. Left to the bundle to discover, both handshakes are 470 paid in series after the JavaScript has parsed; warmed here they happen 471 during the HTML parse instead. 472 473 Written as %VITE_% tokens rather than as hostnames because this same 474 shell builds the second brand against a different API and a different 475 CDN, and because a hostname in source is exactly what the brand guard 476 exists to keep out. Vite substitutes them at build time; unset, they 477 resolve to the document's own origin, which is a harmless no-op. 478 `crossorigin` belongs on the API hint only â that fetch is anonymous 479 CORS, while the covers are plain <img> with no crossOrigin attribute, 480 and a mismatch on either opens a second socket nothing then uses. --> 481 <link rel="preconnect" href="https://api.onefap.ai/api" crossorigin /> 482 <!-- The chat service is its own origin: open its connection while the app boots, not after the room asks for its history (cold TLS + CORS preflight was ~1.5 s of a phone's first load). --> 483 <link rel="preconnect" href="https://projectaix-chat.onrender.com" crossorigin /> 484 <link rel="preconnect" href="https://cdn.onlyfap.ai" /> 485 486 <!-- The page is set in Inter, and nothing discovers that until the CSS has 487 downloaded and parsed â two round trips after this line. Preloaded 488 here it travels alongside the stylesheet instead of behind it, which 489 is the difference between text appearing in Inter and text appearing 490 in Arial and then re-setting a beat later. Only the latin cut: it is 491 the one every visitor needs whatever their language, and the other two 492 are warmed by the bootstrap above when the stored language calls for 493 them. `crossorigin` is required on font preloads even same-origin â 494 omitted, the file is fetched twice. --> 495 <link rel="preload" as="font" type="font/woff2" href="/fonts/inter-latin-v20.woff2" crossorigin /> 496 497 <!-- The banner still is the LCP element, and it is painted by React as a 498 CSS background, so nothing can discover it until the bundle has 499 downloaded, parsed and committed. 22.9 KB that transfers in about a 500 millisecond was being waited on for hundreds. Only the signed-out 501 poster is preloaded: the member cut is chosen once auth resolves, which 502 is not knowable here. 503 504 Two of them, because the band picks its poster by density and 505 preloading the other one is a fetch nothing then uses. The query is 506 BANNER_2X_QUERY in HeroBanner, written out here because a <link> cannot 507 read a constant â the two are the same string and have to stay that 508 way, since a preload that disagrees with the component costs a download 509 and still leaves the real poster undiscovered. The second link is its 510 exact complement, so precisely one of them ever fires. 511 512 `min-resolution` is Safari 16, and a media query the parser cannot read 513 does not fail open â it is replaced by `not all`, which is false. So on 514 every WebKit below 16 both of these were false together: the dense one 515 did not fire, and neither did its complement, and the poster this whole 516 block exists to discover was left to the bundle, hundreds of 517 milliseconds later. 518 519 The prefixed feature is the same question in a dialect those versions 520 speak, and it is added to the complement ONLY. That asymmetry is the 521 point. The component picks its cut through `DENSE_OK`, which needs both 522 this query and an AV1 decoder â so on any browser without AV1, dense or 523 not, it draws the 1x still. Teaching the dense preload to fire there 524 would hand those screens a high-priority download of a file nothing 525 paints, and still leave the one that is painted undiscovered. So the 526 dense branch stays as it is and answers only where `min-resolution` 527 works, while the complement now fires everywhere it should â which is 528 every one of those browsers, since that is the file all of them use. 529 530 `data-hero-preload` is how prerender.mjs finds these to take them out 531 again. The banner is on the home page's two tabs and nowhere else, and 532 a high-priority preload of a picture the page never paints is a 533 download charged against the page that does need one â which is what 534 the console was saying on /premium, /privacy and /terms. --> 535 <link 536 rel="preload" 537 as="image" 538 data-hero-preload 539 href="/banner-bg-2x.jpg" 540 media="(min-resolution: 2dppx) and (min-width: 851px)" 541 fetchpriority="high" 542 /> 543 <link 544 rel="preload" 545 as="image" 546 data-hero-preload 547 href="/banner-bg.jpg" 548 media="not all and (min-resolution: 2dppx) and (min-width: 851px), not all and (-webkit-min-device-pixel-ratio: 2) and (min-width: 851px)" 549 fetchpriority="high" 550 /> 551 552 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 553 554 <!-- The site as something a phone can keep. 555 556 The manifest is generated per brand at build time (see vite-pwa.ts) â 557 it carries the display name and the description the install dialog 558 prints, and neither of those may be a literal in a tracked file. Only 559 the address is fixed, which is why this line can be. 560 561 The three below are iOS, which reads none of the manifest. Safari w
561ants 562 its own icon (PNG, no transparency, corners left square â it rounds 563 them itself), its own permission to run without chrome, and its own 564 copy of the name to put under the icon. `black` for the status bar 565 rather than `black-translucent`: translucent moves the page up under 566 the clock, which every header and fixed overlay in the app would then 567 have to apologise for; opaque black sits above a ground that is nearly 568 black already and changes no layout at all. --> 569 <link rel="manifest" href="/manifest.webmanifest" /> 570 <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> 571 <meta name="apple-mobile-web-app-capable" content="yes" /> 572 <meta name="mobile-web-app-capable" content="yes" /> 573 <meta name="apple-mobile-web-app-status-bar-style" content="black" /> 574 <meta name="apple-mobile-web-app-title" content="OneFap" /> 575 576 <!-- %SEO_% and not %VITE_%: these are substituted by vite-seo.ts, which 577 gives every one of them a fallback. Vite's own pass leaves an undefined 578 variable on the page as the literal token, and a title reading 579 '%VITE_SEO_TITLE%' is worse than any default could be. --> 580 <meta name="description" content="AI video generator: turn your images into stunning AI-powered videos. Professional image-to-video generation with advanced AI models." /> 581 <meta name="author" content="OneFap" /> 582 <title>My collection â OneFap</title> 583 584 <!-- max-image-preview:large is what lets the covers appear at full size in 585 a result rather than as a thumbnail, and max-snippet:-1 lifts the cap 586 on the text under it. Both default to something smaller. --> 587 <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" /> 588 589 <!-- The RTA label. It is the string filters look for â declaring the site 590 adult is what keeps it correctly classified rather than mistaken for a 591 general-audience site that slipped something through. --> 592 <meta name="rating" content="RTA-5042-1996-1400-1577-RTA" /> 593 594 <!-- The card a link to this site draws in a chat app or a timeline. The 595 banner poster stands in as the image: it is already on the page, it is 596 already preloaded above, and a share with no image is a share nobody 597 clicks. og:url is rewritten per address by the script below. --> 598 <meta property="og:type" content="website" /> 599 <meta property="og:site_name" content="OneFap" /> 600 <meta property="og:title" content="My collection â OneFap" /> 601 <meta property="og:description" content="AI video generator: turn your images into stunning AI-powered videos. Professional image-to-video generation with advanced AI models." /> 602 <meta property="og:url" content="https://onefap.ai/collection" /> 603 <meta property="og:image" content="https://onefap.ai/banner-bg-2x.jpg" /> 604 <meta name="twitter:card" content="summary_large_image" /> 605 <meta name="twitter:title" content="My collection â OneFap" /> 606 <meta name="twitter:description" content="AI video generator: turn your images into stunning AI-powered videos. Professional image-to-video generation with advanced AI models." /> 607 <meta name="twitter:image" content="https://onefap.ai/banner-bg-2x.jpg" /> 608 609
609<script type="application/ld+json"> 610 {"@context":"https://schema.org","@graph":[{"@type":"SoftwareApplication","@id":"https://onefap.ai/#app","name":"OneFap","url":"https://onefap.ai/","applicationCategory":"MultimediaApplication","operatingSystem":"Web","description":"AI video generator: turn your images into stunning AI-powered videos. Professional image-to-video generation with advanced AI models.","publisher":{"@id":"https://onefap.ai/#org"}},{"@type":"Organization","@id":"https://onefap.ai/#org","name":"OneFap","url":"https://onefap.ai/","logo":"https://onefap.ai/logo.svg"},{"@type":"WebSite","@id":"https://onefap.ai/#site","name":"OneFap","url":"https://onefap.ai/","publisher":{"@id":"https://onefap.ai/#org"}}]} 611 </script>
611 612 613 <!-- The canonical address, written here rather than served in the markup. 614 This shell answers every route, so a fixed canonical would tell a 615 crawler that /premium and /terms are both duplicates of the home page 616 and drop them from the index. Computed instead from the address being 617 viewed, against the site's own origin so the apex and www spellings 618 collapse to one. 619 620 Google reads a canonical injected this way, since there is no static 621 one for it to conflict with â but it does so only after rendering, so 622 this stays in the parse rather than moving into React, where it would 623 land a bundle later. --> 624
624<script> 625 (function () { 626 var origin = 'https://onefap.ai' || location.origin; 627 // Trailing slash off so '/premium/' and '/premium' are one address, 628 // and query and hash off: they name a state, not a document. 629 var href = origin + (location.pathname.replace(/\/+$/, '') || '/'); 630 631 // Reuse the tag if the prerender already wrote one (prerender.mjs does, 632 // for every address it emits). Appending a second would be worse than 633 // having none: two canonicals disagreeing is read as no answer at all. 634 var link = document.querySelector('link[rel="canonical"]'); 635 if (!link) { 636 link = document.createElement('link'); 637 link.rel = 'canonical'; 638 document.head.appendChild(link); 639 } 640 link.href = href; 641 642 var og = document.querySelector('meta[property="og:url"]'); 643 if (og) og.setAttribute('content', href); 644 })(); 645 </script>
645 646
646<script type="module" crossorigin src="/assets/index-C9eo8hQZ.js"></script>
646 647 <link rel="stylesheet" crossorigin href="/assets/index-D1eXPblZ.css"> 648 <link rel="modulepreload" crossorigin href="/assets/AdHead-2GDnI-eN.js" /> 649 </head> 650 <body> 651 <div id="root"><!--$--><div class="min-h-screen flex flex-col"><header class="sticky top-0 z-50 backdrop-blur-xl border-b border-border transition-colors duration-200 bg-background-card/70" style="padding-top:max(env(safe-area-inset-top), 1rem)"><div class="page-container pb-4 flex items-center justify-between space-x-3 rtl:space-x-reverse"><div class="flex items-center space-x-2.5 lg:space-x-3 rtl:space-x-reverse min-w-0"><a class="flex items-center gap-2.5 lg:gap-3 min-w-0" href="/" data-discover="true"><svg viewBox="0 0 64 64" class="shrink-0 w-10 h-10 lg:w-11 lg:h-11 rounded-lg lg:rounded-xl" role="img" aria-label="OneFap" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="logo-mark" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="rgb(var(--accent-light))"></stop><stop offset="1" stop-color="rgb(var(--accent))"></stop></linearGradient></defs><rect width="64" height="64" rx="16" fill="url(#logo-mark)"></rect><g transform="translate(12.16 12.16) scale(0.62)"><path d="M55,32 C44,48 34,57 24,57 C16,57 11,51 11,44 C11,39 13,35 17,32 C13,29 11,25 11,20 C11,13 16,7 24,7 C34,7 44,16 55,32 Z" fill="#fff"></path></g></svg><span class="block truncate md:shrink-0 text-xl sm:text-2xl lg:text-3xl font-bold tracking-tight text-foreground">OneFap</span></a><span aria-hidden="true" class="hidden md:block w-px h-5 lg:h-7 bg-border-strong"></span><span class="hidden md:block text-[15px] lg:text-[18px] font-bold uppercase tracking-[0.1em] text-white truncate">#1 ADULT AI VIDEO GENERATOR</span></div><div class="flex items-center space-x-2 rtl:space-x-reverse shrink-0"><div class="relative"><button type="button" aria-label="Language and colour theme" aria-expanded="false" class="flex items-center gap-1 sm:gap-1.5 h-10 px-2 sm:px-2.5 rounded-full bg-background-elevated border border-border hover:border-border-strong transition-colors"><span aria-hidden="true" class="text-base leading-none">ð¬ð§</span><span class="hidden sm:inline text-xs font-semibold text-foreground-muted">EN</span><span aria-hidden="true" class="w-3 h-3 rounded-full ring-1 ring-inset ring-white/25" style="background-color:#4361ee"></span></button></div><button type="button" class="h-10 px-4 rounded-full border border-accent text-sm font-semibold text-accent-light hover:bg-accent hover:text-white transition-colors">Login</button></div></div></header><main class="flex-1 page-container pt-3 pb-6"><div class="animate-fade-in"><section class="mb-3 rounded-2xl border border-border bg-background-card p-4 sm:p-5"><h2 class="text-[22px] sm:text-[28px] font-extrabold leading-tight text-foreground">AI image generator</h2><p class="mt-1 text-[13px] font-medium text-foreground-muted">Describe a picture and get it in seconds â no photo needed.</p><div class="mt-4 flex flex-col sm:flex-row sm:items-end gap-3"><div class="flex-1 min-w-0"><label for="ad-hero-prompt" class="text-xs font-semibold text-foreground-muted">Describe the picture</label><div class="relative mt-1"><textarea id="ad-hero-prompt" rows="2" placeholder="a woman standing naked in a bar" class="w-full resize-none rounded-2xl border border-border bg-background-elevated/60 px-3 pt-2 pb-12 text-sm text-foreground placeholder:text-foreground-dim focus:outline-none focus:border-accent"></textarea><a href="https://onlyfap.ai/?utm_source=onefap.ai&utm_medium=ad&utm_content=hero_field&cx=4d741ff9-d46a-4bc1-bf5d-9cae5ca26b30" rel="noopener" class="absolute bottom-2 start-2 max-w-[calc(100%-1rem)] inline-flex items-center gap-1.5 rounded-full border border-border bg-background-card px-3 h-8 text-[12px] font-semibold text-foreground hover:border-accent transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user w-3.5 h-3.5 shrink-0 text-accent-light" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg><span class="truncate">Animate a photo</span><sup class="text-[11px] font-bold uppercase tracking-[0.08em] text-foreground-dim">AD</sup></a></div></div><button type="button" disabled="" aria-busy="false" class="h-[3.375rem] shrink-0 rounded-full bg-foreground text-background px-6 text-base font-bold flex items-center justify-center gap-2 disabled:opacity-60">Generate<svg viewBox="0 0 64 64" class="w-[18px] h-[18px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><circle cx="32" cy="32" r="29" fill="none" stroke="currentColor" stroke-width="4"></circle><g transform="translate(11.54 12.16) scale(0.62)"><path d="M55,32 C44,48 34,57 24,57 C16,57 11,51 11,44 C11,39 13,35 17,32 C13,29 11,25 11,20 C11,13 16,7 24,7 C34,7 44,16 55,32 Z" fill="currentColor"></path></g></svg></button></div></section><a href="https://onlyfap.ai/?utm_source=onefap.ai&utm_medium=ad&utm_content=banner&cx=e0bdd6fe-2eff-43b1-a25a-4cd363ed811a" rel="noopener" class="group flex items-center gap-3 rounded-2xl border border-border bg-gradient-to-r rtl:bg-gradient-to-l from-accent/20 via-accent/10 to-transparent p-3 sm:p-4 transition-colors hover:border-accent mb-3"><span aria-hidden="true" class="w-11 h-11 shrink-0 rounded-full bg-accent flex items-center justify-center"><svg viewBox="0 0 64 64" class="w-[22px] h-[22px] text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M55,32 C44,48 34,57 24,57 C16,57 11,51 11,44 C11,39 13,35 17,32 C13,29 11,25 11,20 C11,13 16,7 24,7 C34,7 44,16 55,32 Z" fill="currentColor"></path></svg></span><span class="flex-1 min-w-0"><span class="block text-[11px] font-bold uppercase tracking-[0.08em] text-accent-light">AD</span><span class="block text-sm sm:text-base font-bold text-foreground leading-snug">Turn any photo into a video</span><span class="block text-[11px] sm:text-xs text-foreground-muted leading-snug line-clamp-2">Upload a photo and get your own video in a couple of minutes.</span></span><span class="shrink-0 flex items-center gap-1.5 rounded-full bg-foreground text-background px-3 sm:px-4 h-9 text-[12px] sm:text-[13px] font-bold"><span class="hidden sm:inline">Try it free</span><sup class="hidden sm:inline text-[11px] font-bold uppercase tracking-[0.08em]">AD</sup><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right w-4 h-4 rtl:rotate-180" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span></a><nav id="tab-strip" class="mb-4"><div class="grid grid-cols-12 gap-1.5 lg:hidden gen-row:grid-cols-2"><a class="col-span-6 gen-row:col-span-1 relative flex min-h-[56px] flex-col items-center justify-center space-y-1 rounded-xl border px-1 py-2 text-center text-[11px] font-semibold leading-tight transition-colors border-border bg-background-elevated/60 text-foreground-muted hover:text-foreground" href="/" data-discover="true"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-compass h-[18px] w-[18px] shrink-0" aria-hidden="true"><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"></path><circle cx="12" cy="12" r="10"></circle></svg><span class="max-w-full [overflow-wrap:anywhere]">Explore</span></a><a aria-current="page" class="col-span-6 gen-row:col-span-1 relative flex min-h-[56px] flex-col items-center justify-center space-y-1 rounded-xl border px-1 py-2 text-center text-[11px] font-semibold leading-tight transition-colors border-accent/60 bg-accent/15 text-foreground" href="/collection" data-discover="true"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart h-[18px] w-[18px] shrink-0 text-accent-light" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path></svg><span class="max-w-full [overflow-wrap:anywhere]">My Collection</span></a></div><div class="hidden lg:flex items-center space-x-8 rtl:space-x-reverse border-b border-border overflow-x-auto no-scrollbar"><a class="display-title relative -mb-px inline-flex items-center min-h-[44px] pb-3.5 pt-2.5 text-sm !leading-[1.35] sm:text-base sm:!leading-[1.35] whitespace-nowrap transition-colors text-foreground-muted hover:text-foreground border-b-2 border-transparent" href="/" data-discover="true">Explore</a><a aria-current="page" class="display-title relative -mb-px inline-flex items-center min-h-[44px] pb-3.5 pt-2.5 text-sm !leading-[1.35] sm:text-base sm:!leading-[1.35] whitespace-nowrap transition-colors text-foreground border-b-2 border-accent" href="/collection" data-discover="true">My Collection</a></div></nav><div class="flex flex-col items-center text-center py-20 px-4"><h2 class="display-title text-lg sm:text-xl text-foreground">Sign in to see your videos</h2><p class="mt-1.5 text-sm text-foreground-muted max-w-sm">Everything you generate is saved here to your account.</p><button type="button" class="mt-6 inline-flex items-center gap-2 rounded-full bg-accent hover:bg-accent-hover px-6 py-2.5 text-sm font-semibold text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-log-in w-4 h-4" aria-hidden="true"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" x2="3" y1="12" y2="12"></line></svg>Login</button></div></div></main><footer class="page-container pt-4 pb-safe"><nav class="flex justify-center flex-wrap gap-x-6 text-[13px] text-foreground-muted"><a class="inline-flex items-center py-2.5 hover:text-foreground transition-colors" href="/privacy" data-discover="true">Privacy Policy</a><a class="inline-flex items-center py-2.5 hover:text-foreground transition-colors" href="/terms" data-discover="true">Terms of Use</a><a class="inline-flex items-center py-2.5 hover:text-foreground transition-colors" href="/api-access" data-discover="true">API</a><a href="https://t.me/onlyfap_support_bot" target="_blank" rel="noopener noreferrer" class="inline-flex items-center py-2.5 hover:text-foreground transition-colors">Support</a><a href="https://t.me/onlyfap_ai_bot" target="_blank" rel="noopener noreferrer" class="inline-flex items-center py-2.5 hover:text-foreground transition-colors">Telegram</a></nav></footer><div class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-md" role="dialog" aria-modal="true" aria-labelledby="age-gate-title"><div tabindex="-1" class="flex w-full max-w-md max-h-full flex-col overflow-hidden rounded-[28px] border border-border bg-background-card shadow-2xl focus:outline-none" style="background-image:radial-gradient(100% 52% at 50% 0%, rgb(var(--accent-alt) / 0.32), transparent 72%), radial-gradient(80% 40% at 50% 100%, rgb(var(--accent) / 0.14), transparent 72%)"><div class="flex min-h-0 flex-1 flex-col overflow-hidden "><div tabindex="0" role="group" aria-label="Write your text and get a personalised video in 2 minutes!" class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-6 pb-0 sm:p-8 sm:pb-0"><h2 id="age-gate-title" class="text-center text-[32px] sm:text-[38px] font-extrabold leading-none tracking-tight text-foreground">One<span class="text-accent-light">Fap</span></h2><p class="mt-4 mb-7 text-center text-base sm:text-lg font-medium leading-snug text-foreground">Write your text and get a personalised video in 2 minutes!</p><div class="flex flex-col gap-3"><div class="flex gap-3.5 rounded-2xl border border-border bg-background/60 p-4"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-alert w-[22px] h-[22px] mt-0.5 shrink-0 text-accent-light" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg><div class="min-w-0"><p class="text-[15px] font-extrabold uppercase leading-snug tracking-wide text-foreground">18+ ADULTS ONLY</p><p class="mt-1.5 text-[13px] leading-relaxed text-foreground-muted">I confirm I am at least 18 years old. This platform contains explicit AI-generated content.</p></div></div><div class="flex gap-3.5 rounded-2xl border border-border bg-background/60 p-4"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-check w-[22px] h-[22px] mt-0.5 shrink-0 text-accent-light" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><polyline points="16 11 18 13 22 9"></polyline></svg><div class="min-w-0"><p class="text-[15px] font-extrabold uppercase leading-snug tracking-wide text-foreground">ETHICAL USE POLICY</p><p class="mt-1.5 text-[13px] leading-relaxed text-foreground-muted">I will not generate non-consensual deepfakes of real people.</p></div></div><div class="flex gap-3.5 rounded-2xl border border-border bg-background/60 p-4"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale w-[22px] h-[22px] mt-0.5 shrink-0 text-accent-light" aria-hidden="true"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path><path d="M7 21h10"></path><path d="M12 3v18"></path>
651<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"></path></svg><div class="min-w-0"><p class="text-[15px] font-extrabold uppercase leading-snug tracking-wide text-foreground">ZERO LIABILITY</p><p class="mt-1.5 text-[13px] leading-relaxed text-foreground-muted">I am solely responsible for the content I generate and distribute.</p></div></div></div></div></div><div class="shrink-0 px-6 pb-6 pt-5 sm:px-8 sm:pb-8"><button type="button" class="w-full h-14 rounded-full bg-white hover:bg-white/90 text-base font-extrabold uppercase tracking-wide text-accent transition-colors">Continue</button><p class="mt-4 text-center text-xs leading-relaxed text-foreground-dim">By entering, you agree to our<!-- --> <a class="inline-block py-1 underline hover:text-foreground-muted transition-colors" href="/privacy" data-discover="true">Privacy Policy</a> <!-- -->&<!-- --> <a class="inline-block py-1 underline hover:text-foreground-muted transition-colors" href="/terms" data-discover="true">Terms of Use</a></p></div></div></div><!--$--><a href="https://onlyfap.ai/?utm_source=onefap.ai&utm_medium=ad&utm_content=chip&cx=6bdd047d-b53c-4494-878d-557ddd0e9101" rel="noopener" class="fixed bottom-[max(1rem,env(safe-area-inset-bottom,0px))] end-4 z-40 flex max-w-[calc(100vw-2rem)] items-center gap-2 rounded-full border border-accent bg-background-elevated/95 backdrop-blur-sm px-4 h-11 text-[13px] font-semibold text-foreground shadow-lg hover:border-accent-light transition-colors"><svg viewBox="0 0 64 64" class="w-4 h-4 shrink-0 text-accent-light" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><circle cx="32" cy="32" r="29" fill="none" stroke="currentColor" stroke-width="4"></circle><g transform="translate(11.54 12.16) scale(0.62)"><path d="M55,32 C44,48 34,57 24,57 C16,57 11,51 11,44 C11,39 13,35 17,32 C13,29 11,25 11,20 C11,13 16,7 24,7 C34,7 44,16 55,32 Z" fill="currentColor"></path></g></svg><span class="truncate">Make a video from a photo</span><span class="shrink-0 text-[11px] font-bold uppercase tracking-[0.08em] text-foreground-dim">AD</span></a><!--/$--></div><!--/$--></div> 652 <!-- Drawn only when the page finds itself in a frame â see the head. Kept 653 out of #root because that is the element React owns and prerender 654 fills; a notice inside it would be thrown away on mount. --> 655 <div id="framed-note"> 656 This page cannot be shown inside another site. 657 <a id="framed-link" href="/">Open it directly</a> 658
658<script> 659 (function () { 660 var a = document.getElementById('framed-link'); 661 if (a) a.href = window.self.location.href; 662 a.target = '_top'; 663 })(); 664 </script>
664 665 </div> 666 </body> 667</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.