PageSourceSearch

https://onefap.ai/collection

html onefap.ai collected 2026-10-05 15:14:32 UTC 51,488 bytes, 667 lines download raw bytes

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&amp;utm_medium=ad&amp;utm_content=hero_field&amp;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&amp;utm_medium=ad&amp;utm_content=banner&amp;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> <!-- -->&amp;<!-- --> <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&amp;utm_medium=ad&amp;utm_content=chip&amp;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.