1<!DOCTYPE html><html lang="en"><head><meta charSet="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="preload" as="image" href="/logo-no-sign.png"/><link rel="preload" as="image" href="/logo-wordmark.png"/>
vendor: 66 bytes, line 1
1<script async="" src="https://www.googletagmanager.com/gtag/js?id=
1G-P4F8SK0C03
vendor: 11 bytes, line 1
1"></script>
1<title>Blok Storage Presets â Supabase, S3, Cloudinary</title><meta name="description" content="Ready-made uploaders for Blok: Supabase, S3-compatible storage, Cloudinary, and IndexedDB â no backend to write for file uploads."/><link rel="canonical" href="https://blokeditor.com/presets/"/><link rel="alternate" type="text/markdown" href="https://blokeditor.com/presets.md"/><link rel="alternate" hreflang="en" href="https://blokeditor.com/presets/"/><link rel="alternate" hreflang="ru" href="https://blokeditor.com/ru/presets/"/><link rel="alternate" hreflang="x-default" href="https://blokeditor.com/presets/"/><meta property="og:type" content="website"/><meta property="og:site_name" content="Blok"/><meta property="og:locale" content="en_US"/><meta property="og:locale:alternate" content="ru_RU"/><meta property="og:url" content="https://blokeditor.com/presets/"/><meta property="og:title" content="Blok Storage Presets â Supabase, S3, Cloudinary"/><meta property="og:description" content="Ready-made uploaders for Blok: Supabase, S3-compatible storage, Cloudinary, and IndexedDB â no backend to write for file uploads."/><meta property="og:image" content="https://blokeditor.com/og-default.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Blok â Storage presets"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="Blok Storage Presets â Supabase, S3, Cloudinary"/><meta name="twitter:description" content="Ready-made uploaders for Blok: Supabase, S3-compatible storage, Cloudinary, and IndexedDB â no backend to write for file uploads."/><meta name="twitter:image" content="https://blokeditor.com/og-default.png"/><meta name="twitter:image:alt" content="Blok â Storage presets"/><meta name="theme-color" content="#e14b6e"/><link rel="preload" as="font" type="font/woff2" href="/fonts/plus-jakarta-sans-latin.woff2" crossorigin="anonymous"/><link rel="icon" type="image/x-icon" href="/favicon.ico"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png"/><link rel="icon" type="image/png" sizes="64x64" href="/favicon-64x64.png"/><link rel="icon" type="image/png" sizes="128x128" href="/favicon-128x128.png"/><link rel="icon" type="image/png" sizes="256x256" href="/favicon-256x256.png"/><link rel="icon" type="image/svg+xml" href="/favicon.svg"/><link rel="apple-touch-icon" sizes="192x192" href="/android-chrome-192x192.png"/><link rel="manifest" href="/site.webmanifest"/><link rel="modulepreload" href="/assets/manifest-c746c058.js"/><link rel="modulepreload" href="/assets/entry.client-CYNZ4gJQ.js"/><link rel="modulepreload" href="/assets/jsx-runtime-BqgtD8fB.js"/><link rel="modulepreload" href="/assets/client--or4H8Oa.js"/><link rel="modulepreload" href="/assets/react-dom-Bn_sNRcU.js"/>
1<link rel="modulepreload" href="/assets/errorBoundaries-RsYZHCc5.js"/><link rel="modulepreload" href="/assets/root-DBwQp-RA.js"/><link rel="modulepreload" href="/assets/analytics-C9N8WMi8.js"/><link rel="modulepreload" href="/assets/proxy-CPIdTNUQ.js"/><link rel="modulepreload" href="/assets/I18nContext-CukFlrBm.js"/><link rel="modulepreload" href="/assets/FrameworkContext-Dnjp-HNi.js"/><link rel="modulepreload" href="/assets/presets-Cd5MP1oH.js"/><link rel="modulepreload" href="/assets/Footer-BUORHFmj.js"/><link rel="modulepreload" href="/assets/CodeBlock-BETo0aUY.js"/><link rel="modulepreload" href="/assets/Prose-Cb804ILs.js"/><link rel="modulepreload" href="/assets/useCopyToClipboard-O8-D1bL6.js"/>
1<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://blokeditor.com/#organization","name":"Blok","url":"https://blokeditor.com/","logo":"https://blokeditor.com/android-chrome-512x512.png","sameAs":["https://github.com/JackUait/blok","https://www.npmjs.com/package/@bloklabs/core"]},{"@type":"WebSite","@id":"https://blokeditor.com/#website","url":"https://blokeditor.com/","name":"Blok","description":"Blok is a headless block-based rich text editor for React, Vue, and Angular that stores content as typed JSON instead of HTML.","inLanguage":"en","publisher":{"@id":"https://blokeditor.com/#organization"}}]}</script>
1<link rel="stylesheet" href="/assets/index-tO0rXQyT.css"/>
1<script> 2if (['localhost', '127.0.0.1', '[::1]'].includes(location.hostname)) { 3 window['ga-disable-G-P4F8SK0C03'] = true; 4}</script>
4<script>
vendor: 127 bytes, lines 5-8
5window.dataLayer = window.dataLayer || []; 6function gtag(){dataLayer.push(arguments);} 7gtag('js', new Date()); 8gtag('config', '
8G-P4F8SK0C03
vendor: 30 bytes, line 8
8', { send_page_view: false });
8</script>
8<script> 9(function() { 10 const STORAGE_KEY = 'blok-docs-theme'; 11 const VALID_THEMES = ['light', 'dark', 'system']; 12 13 const stored = localStorage.getItem(STORAGE_KEY); 14 const theme = stored && VALID_THEMES.includes(stored) ? stored : 'system'; 15 16 let resolvedTheme = theme; 17 if (theme === 'system') { 18 const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 19 resolvedTheme = systemPrefersDark ? 'dark' : 'light'; 20 } 21 22 document.documentElement.setAttribute('data-theme', resolvedTheme); 23 document.documentElement.classList.add(resolvedTheme); 24})();</script>
24</head><body><div style="opacity:1;transform:none"><a href="#main-content" class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:rounded-full focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-semibold focus:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">Skip to content</a><nav class="inset-x-0 top-0 z-40 px-3 sm:px-4 fixed will-change-transform" data-nav="true" data-blok-testid="nav"><div class="relative mx-auto flex w-full items-center justify-between gap-5 border transition-[height,max-width,border-radius,background-color,border-color,box-shadow,padding] duration-[450ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none mt-0 h-16 max-w-6xl rounded-none border-transparent bg-transparent px-6"><a class="nav-brand flex shrink-0 items-center gap-1.5 font-display text-xl font-extrabold tracking-tight" href="/" data-discover="true"><img src="/logo-no-sign.png" alt="Blok" width="42" height="42" class="nav-brand-mascot" style="object-fit:contain" data-blok-testid="logo"/><img src="/logo-wordmark.png" alt="" width="154" height="72" class="nav-brand-wordmark h-7 w-auto" data-blok-testid="nav-wordmark"/></a><div class="relative flex justify-center sm:max-w-md sm:flex-1"><button type="button" class="group flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-border/50 bg-background/55 backdrop-blur-md transition-colors duration-200 ease-out hover:border-border hover:bg-background/80 sm:h-12 sm:w-full sm:justify-between sm:pl-5 sm:pr-1.5" aria-label="Search (âK)" aria-expanded="false"><span class="flex size-[18px] shrink-0 items-center justify-center text-primary transition-transform duration-200 group-hover:scale-110 sm:hidden"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true"><circle cx="9" cy="9" r="6.25" stroke="currentColor" stroke-width="2.2"></circle><path d="M17.5 17.5l-4-4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></path></svg></span><span class="hidden truncate text-[15px] font-semibold tracking-[-0.01em] text-foreground/75 transition-colors group-hover:text-foreground sm:inline">Search docs...</span><span class="ml-3 hidden items-center gap-2.5 sm:flex"><kbd class="hidden items-center gap-0.5 rounded-md bg-secondary px-1.5 py-1 font-mono text-[10px] font-semibold leading-none text-muted-foreground/70 transition-colors group-hover:bg-secondary/80 md:inline-flex"><span class="text-[12px] leading-none">â</span>K</kbd><span class="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-[0_1px_3px_rgba(225,29,72,0.35)] transition-transform duration-200 group-hover:scale-105"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true"><circle cx="9" cy="9" r="6.25" stroke="currentColor" stroke-width="2.2"></circle><path d="M17.5 17.5l-4-4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></path></svg></span></span></button></div><div class="flex shrink-0 items-center gap-0.5"><div class="relative"><button class="flex size-9 cursor-pointer items-center justify-center rounded-full text-foreground/80 transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" aria-expanded="false" aria-haspopup="menu" aria-label="Language: English" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-[18px]" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg></button><div class="absolute right-0 top-[calc(100%+0.5rem)] z-50 min-w-[12rem] origin-top-right rounded-2xl border border-border bg-popover p-1.5 shadow-card transition-all duration-150 pointer-events-none scale-95 opacity-0" role="menu" aria-label="Select language" aria-hidden="true"><a hrefLang="en" class="flex w-full cursor-pointer items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-semibold transition-colors hover:bg-secondary text-foreground" role="menuitem" aria-current="true" tabindex="-1" href="/presets/" data-discover="true"><span class="flex shrink-0 items-center"><svg width="20" height="14" viewBox="0 0 20 14" fill="none" class="rounded-[3px] shadow-sm"><rect width="20" height="14" rx="2" fill="#fff"></rect><rect width="20" height="1.077" fill="#B22234"></rect><rect y="2.154" width="20" height="1.077" fill="#B22234"></rect><rect y="4.308" width="20" height="1.077" fill="#B22234"></rect><rect y="6.462" width="20" height="1.077" fill="#B22234"></rect><rect y="8.615" width="20" height="1.077" fill="#B22234"></rect><rect y="10.769" width="20" height="1.077" fill="#B22234"></rect><rect y="12.923" width="20" height="1.077" fill="#B22234"></rect><rect width="8" height="7.538" fill="#3C3B6E"></rect><g fill="white"><circle cx="1.5" cy="1.1" r="0.6"></circle><circle cx="4" cy="1.1" r="0.6"></circle><circle cx="6.5" cy="1.1" r="0.6"></circle><circle cx="2.75" cy="2.5" r="0.6"></circle><circle cx="5.25" cy="2.5" r="0.6"></circle><circle cx="1.5" cy="3.9" r="0.6"></circle><circle cx="4" cy="3.9" r="0.6"></circle><circle cx="6.5" cy="3.9" r="0.6"></circle><circle cx="2.75" cy="5.3" r="0.6"></circle><circle cx="5.25" cy="5.3" r="0.6"></circle><circle cx="1.5" cy="6.7" r="0.6"></circle><circle cx="4" cy="6.7" r="0.6"></circle><circle cx="6.5" cy="6.7" r="0.6"></circle></g></svg></span><span class="flex-1">English</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-4 text-primary transition-opacity opacity-100" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg></a><a hrefLang="ru" class="flex w-full cursor-pointer items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-semibold transition-colors hover:bg-secondary" role="menuitem" tabindex="-1" href="/ru/presets/" data-discover="true"><span class="flex shrink-0 items-center"><svg width="20" height="14" viewBox="0 0 20 14" fill="none" class="rounded-[3px] shadow-sm"><rect width="20" height="14" rx="2" fill="#0039A6"></rect><rect width="20" height="4.67" rx="2" ry="0" fill="white"></rect><rect y="9.33" width="20" height="4.67" rx="0" ry="2" fill="#D52B1E"></rect></svg></span><span class="flex-1">Ð ÑÑÑкий</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-4 text-primary transition-opacity opacity-0" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg></a></div></div><a href="https://github.com/JackUait/blok" target="_blank" rel="noopener noreferrer" aria-label="GitHub" title="GitHub" data-slot="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-semibold transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg]:pointer-events-none [&_svg:not([class*='
24size-'])]:size-4 shrink-0 [&_svg]:shrink-0 active:scale-[0.97] hover:bg-secondary size-9 rounded-full text-foreground/80 hover:text-foreground"><svg class="size-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.04-.02-2.05-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.33-1.76-1.33-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.49.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.34-5.47-5.95 0-1.31.47-2.39 1.24-3.23-.13-.3-.54-1.52.11-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6.01 0c2.29-1.55 3.3-1.23 3.3-1.23.65 1.66.24 2.88.12 3.18.77.84 1.23 1.92 1.23 3.23 0 4.62-2.81 5.64-5.49 5.94.43.37.81 1.1.81 2.22 0 1.6-.01 2.9-.01 3.29 0 .32.21.7.82.58A12.01 12.01 0 0 0 24 12.5C24 5.87 18.63.5 12 .5z"></path></svg></a><button data-slot="button" class="inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-semibold transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 active:scale-[0.97] hover:bg-secondary size-9 rounded-full text-foreground/80 hover:text-foreground" type="button" aria-label="Toggle theme, current: Light theme" title="Light theme"><span class="relative flex size-[18px] items-center justify-center"><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-sun size-[18px] animate-in fade-in zoom-in-95 spin-in-45 fill-mode-both duration-300 ease-out motion-reduce:animate-none" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg></span></button><span class="mx-1.5 hidden h-5 w-px bg-border sm:block" aria-hidden="true"></span><div class="relative"><button type="button" class="flex size-9 cursor-pointer items-center justify-center rounded-full text-foreground/80 transition-colors hover:bg-secondary hover:text-foreground" aria-label="Toggle menu" aria-expanded="false" aria-haspopup="menu" data-nav-toggle="true"><span class="flex flex-col items-center justify-center gap-[3.5px]" aria-hidden="true"><span class="block h-[1.5px] w-[18px] rounded-full bg-current"></span><span class="block h-[1.5px] w-[18px] rounded-full bg-current"></span><span class="block h-[1.5px] w-[18px] rounded-full bg-current"></span></span></button><div class="absolute right-0 top-[calc(100%+0.625rem)] z-50 flex min-w-[14rem] origin-top-right flex-col rounded-2xl border border-border bg-popover p-2 shadow-card transition-all duration-150 pointer-events-none scale-95 opacity-0" aria-hidden="true"><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary" tabindex="-1" href="/docs/" data-discover="true">Docs</a><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary active bg-secondary text-foreground" tabindex="-1" href="/presets/" data-discover="true">Presets</a><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary" tabindex="-1" href="/server/" data-discover="true">Server</a><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary" tabindex="-1" href="/demo/" data-discover="true">Demo</a><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary" tabindex="-1" href="/migration/" data-discover="true">Migration</a><a class="rounded-xl px-3.5 py-2.5 text-sm font-semibold transition-colors hover:bg-secondary" tabindex="-1" href="/changelog/" data-discover="true">Changelog</a></div></div></div></div></nav><main class="min-h-screen bg-background pt-16"><div class="mx-auto w-full max-w-4xl px-6 py-10 lg:py-14" data-blok-testid="presets-docs"><div class="mb-10"><h1 class="text-3xl font-extrabold tracking-tight text-foreground sm:text-4xl">Storage presets</h1><p class="mt-3 max-w-2xl text-base leading-relaxed text-muted-foreground">
24Ready-made uploaders from @bloklabs/presets. They give Blok somewhere to put uploaded files, so you do not have to write an upload handler. Each one plugs straight into the uploader config option.</p></div><div class="mb-12 overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm" data-blok-testid="presets-table"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Preset</th><th class="px-4 py-3 font-semibold text-foreground">Re-hosts a remote URL?</th><th class="px-4 py-3 font-semibold text-foreground">Production-ready?</th></tr></thead><tbody><tr class="border-b border-border last:border-0" data-blok-testid="presets-summary-fetch-endpoint"><td class="px-4 py-3 align-top"><a href="#fetch-endpoint" class="font-semibold text-foreground hover:underline">Fetch endpoint</a></td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td></tr><tr class="border-b border-border last:border-0" data-blok-testid="presets-summary-supabase"><td class="px-4 py-3 align-top"><a href="#supabase" class="font-semibold text-foreground hover:underline">Supabase</a></td><td class="px-4 py-3 align-top text-muted-foreground">No</td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td></tr><tr class="border-b border-border last:border-0" data-blok-testid="presets-summary-presigned"><td class="px-4 py-3 align-top"><a href="#presigned" class="font-semibold text-foreground hover:underline">Presigned URLs</a></td><td class="px-4 py-3 align-top text-muted-foreground">No</td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td></tr><tr class="border-b border-border last:border-0" data-blok-testid="presets-summary-cloudinary"><td class="px-4 py-3 align-top"><a href="#cloudinary" class="font-semibold text-foreground hover:underline">Cloudinary</a></td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td><td class="px-4 py-3 align-top text-muted-foreground">Yes</td></tr><tr class="border-b border-border last:border-0" data-blok-testid="presets-summary-indexeddb"><td class="px-4 py-3 align-top"><a href="#indexeddb" class="font-semibold text-foreground hover:underline">IndexedDB</a></td><td class="px-4 py-3 align-top text-muted-foreground">No</td><td class="px-4 py-3 align-top text-muted-foreground">No</td></tr></tbody></table></div><div class="flex flex-col gap-16"><section id="fetch-endpoint" class="scroll-mt-24" data-blok-testid="presets-section-fetch-endpoint" aria-label="Fetch endpoint"><div class="mb-6"><h2 class="scroll-mt-24 text-2xl font-extrabold tracking-tight text-foreground">Fetch endpoint</h2><div class="mt-2 max-w-2xl"><p class="mt-3 first:mt-0 text-base leading-relaxed text-muted-foreground">Talks to a backend you already have. Every upload goes through your server. Your server decides where the bytes actually land.</p></div></div><div class="mt-6 rounded-2xl border border-border bg-card p-4 shadow-sm"><p class="text-sm font-bold uppercase tracking-wide text-foreground">Re-hosting a remote URL</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">Yes. Your endpoint does the fetch server-side, so the browser never has to reach a third-party URL itself.</p></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Configuration</h3><div class="overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Option</th><th class="px-4 py-3 font-semibold text-foreground">Type</th><th class="px-4 py-3 font-semibold text-foreground">Default</th><th class="px-4 py-3 font-semibold text-foreground">Description</th></tr></thead><tbody><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-fetch-endpoint-baseUrl"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">baseUrl</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">(required)</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Origin of your upload service, e.g. "https://api.myapp.com".</p></td></tr><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-fetch-endpoint-field"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">field</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">"file"</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Multipart field name the endpoint reads for POST /upload.</p></td></tr><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-fetch-endpoint-headers"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">headers</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">Record<string, string> | (() => Promise<Record<string, string>>)</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">undefined</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Extra headers on every request. Pass a function to mint a fresh access token per upload.</p></td></tr></tbody></table></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Storage-side setup</h3><ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground"><li>A server that answers POST {baseUrl}/upload (multipart) and POST {baseUrl}/upload-by-url ({ url }) with { url, fileName? } JSON.</li><li>The endpoint alone decides where the file is stored: S3, a disk volume, or Supabase. This preset never sees it.</li><li>The URL your /upload-by-url endpoint receives is user-supplied. Validate it server-side and block internal and private addresses before you fetch it. If you do not, you have built an SSRF vector.</li></ul></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Usage Example</h3><div class="code-block group relative overflow-hidden rounded-2xl border border-border bg-card" data-code-block="true" data-blok-testid="code-block"><div class="flex items-center gap-3 px-3 pt-3 pb-1.5"><span class="pl-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">TypeScript</span><button class="ml-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" data-copy="true" data-code="import { fetchStorage } from '
24@bloklabs/presets'; 25 26new Blok({ 27 holder: 'editor', 28 uploader: fetchStorage({ baseUrl: 'https://api.myapp.com' }), 29});" data-blok-testid="code-copy-button" type="button" aria-label="Copy"><svg class="size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></div><div class="relative mx-3 mb-3"><div class="overflow-x-auto rounded-xl pt-3 pb-3 pl-4 font-mono text-sm leading-relaxed [&_pre]:!m-0 [&_code]:font-mono pr-8"><div><pre class="shiki"><code>import { fetchStorage } from '@bloklabs/presets'; 30 31new Blok({ 32 holder: 'editor', 33 uploader: fetchStorage({ baseUrl: 'https://api.myapp.com' }), 34});</code></pre></div></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 left-0 w-8 rounded-l-xl transition-opacity duration-200 opacity-0"></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 right-0 w-8 rounded-r-xl transition-opacity duration-200 opacity-0"></div></div></div></div></section><section id="supabase" class="scroll-mt-24" data-blok-testid="presets-section-supabase" aria-label="Supabase"><div class="mb-6"><h2 class="scroll-mt-24 text-2xl font-extrabold tracking-tight text-foreground">Supabase</h2><div class="mt-2 max-w-2xl"><p class="mt-3 first:mt-0 text-base leading-relaxed text-muted-foreground">Uploads straight to a Supabase Storage bucket using the client you already initialize.</p></div></div><div class="mt-6 rounded-2xl border border-border bg-card p-4 shadow-sm"><p class="text-sm font-bold uppercase tracking-wide text-foreground">Re-hosting a remote URL</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">No. Re-hosting a remote URL needs a server-side fetch, and the browser cannot make one on Supabase's behalf. uploadByUrl is left undefined, so Blok stores the URL verbatim instead of pretending it was re-hosted.</p></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Configuration</h3><div class="overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Option</th><th class="px-4 py-3 font-semibold text-foreground">Type</th><th class="px-4 py-3 font-semibold text-foreground">Default</th><th class="px-4 py-3 font-semibold text-foreground">Description</th></tr></thead><tbody><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-supabase-bucket"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">bucket</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string | ((kind: AssetKind) => string)</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">"blok"</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Bucket name, or a function of the asset kind for per-kind buckets.</p></td></tr><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-supabase-path"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">path</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">(file: File, ctx: UploadContext) => string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">random name, original extension kept</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Object path within the bucket.</p></td></tr></tbody></table></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Storage-side setup</h3><ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground"><li>Create the bucket in the Supabase dashboard (default name "blok", or whatever `bucket` resolves to).</li><li>Make the bucket public, or add a Storage policy granting anon SELECT. This preset returns getPublicUrl() directly, so an unreadable object shows up as a broken image.</li><li>Add a Storage INSERT policy for whichever role the client authenticates as.</li></ul></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Usage Example</h3><div class="code-block group relative overflow-hidden rounded-2xl border border-border bg-card" data-code-block="true" data-blok-testid="code-block"><div class="flex items-center gap-3 px-3 pt-3 pb-1.5"><span class="pl-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">TypeScript</span><button class="ml-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" data-copy="true" data-code="import { createClient } from '@supabase/supabase-js'; 35import { supabaseStorage } from '
35@bloklabs/presets'; 36 37const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 38new Blok({ holder: 'editor', uploader: supabaseStorage(supabase, { bucket: 'blok' }) });" data-blok-testid="code-copy-button" type="button" aria-label="Copy"><svg class="size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></div><div class="relative mx-3 mb-3"><div class="overflow-x-auto rounded-xl pt-3 pb-3 pl-4 font-mono text-sm leading-relaxed [&_pre]:!m-0 [&_code]:font-mono pr-8"><div><pre class="shiki"><code>import { createClient } from '@supabase/supabase-js'; 39import { supabaseStorage } from '@bloklabs/presets'; 40 41const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 42new Blok({ holder: 'editor', uploader: supabaseStorage(supabase, { bucket: 'blok' }) });</code></pre></div></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 left-0 w-8 rounded-l-xl transition-opacity duration-200 opacity-0"></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 right-0 w-8 rounded-r-xl transition-opacity duration-200 opacity-0"></div></div></div></div></section><section id="presigned" class="scroll-mt-24" data-blok-testid="presets-section-presigned" aria-label="Presigned URLs"><div class="mb-6"><h2 class="scroll-mt-24 text-2xl font-extrabold tracking-tight text-foreground">Presigned URLs</h2><div class="mt-2 max-w-2xl"><p class="mt-3 first:mt-0 text-base leading-relaxed text-muted-foreground">For S3 and S3-compatible storage (R2, MinIO, GCS). Your backend mints a short-lived signed URL. The browser PUTs the file straight to it.</p></div></div><div class="mt-6 rounded-2xl border border-border bg-card p-4 shadow-sm"><p class="text-sm font-bold uppercase tracking-wide text-foreground">Re-hosting a remote URL</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">No. Re-hosting a remote URL needs a server-side fetch, and a presigned PUT URL cannot do that. uploadByUrl is left undefined, so Blok stores the URL verbatim instead.</p></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Configuration</h3><div class="overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Option</th><th class="px-4 py-3 font-semibold text-foreground">Type</th><th class="px-4 py-3 font-semibold text-foreground">Default</th><th class="px-4 py-3 font-semibold text-foreground">Description</th></tr></thead><tbody><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-presigned-sign"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">sign</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">(request: SignRequest) => Promise<SignedTarget></code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">(required)</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Called with { fileName, mimeType, size, kind }. It must return { uploadUrl, publicUrl, headers? } from your backend.</p></td></tr></tbody></table></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Storage-side setup</h3><ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground"><li>A backend endpoint that creates a presigned PUT URL (e.g. S3 PutObjectCommand) and returns it as SignedTarget.</li><li>A CORS rule on the bucket that allows PUT from your app's origin. It must also allow the headers this preset sends, notably Content-Type. The browser PUTs directly to the bucket, so this is the step that actually blocks people.</li></ul></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Usage Example</h3><div class="code-block group relative overflow-hidden rounded-2xl border border-border bg-card" data-code-block="true" data-blok-testid="code-block"><div class="flex items-center gap-3 px-3 pt-3 pb-1.5"><span class="pl-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">TypeScript</span><button class="ml-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" data-copy="true" data-code="import { presignedStorage } from '
42@bloklabs/presets'; 43 44new Blok({ 45 holder: 'editor', 46 uploader: presignedStorage({ sign: (request) => api.sign(request) }), 47});" data-blok-testid="code-copy-button" type="button" aria-label="Copy"><svg class="size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></div><div class="relative mx-3 mb-3"><div class="overflow-x-auto rounded-xl pt-3 pb-3 pl-4 font-mono text-sm leading-relaxed [&_pre]:!m-0 [&_code]:font-mono pr-8"><div><pre class="shiki"><code>import { presignedStorage } from '@bloklabs/presets'; 48 49new Blok({ 50 holder: 'editor', 51 uploader: presignedStorage({ sign: (request) => api.sign(request) }), 52});</code></pre></div></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 left-0 w-8 rounded-l-xl transition-opacity duration-200 opacity-0"></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 right-0 w-8 rounded-r-xl transition-opacity duration-200 opacity-0"></div></div></div></div></section><section id="cloudinary" class="scroll-mt-24" data-blok-testid="presets-section-cloudinary" aria-label="Cloudinary"><div class="mb-6"><h2 class="scroll-mt-24 text-2xl font-extrabold tracking-tight text-foreground">Cloudinary</h2><div class="mt-2 max-w-2xl"><p class="mt-3 first:mt-0 text-base leading-relaxed text-muted-foreground">Uploads directly to Cloudinary using an unsigned upload preset. No backend required.</p></div></div><div class="mt-6 rounded-2xl border border-border bg-card p-4 shadow-sm"><p class="text-sm font-bold uppercase tracking-wide text-foreground">Re-hosting a remote URL</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">Yes. Cloudinary fetches the remote URL itself once the browser hands it over. Re-hosting works without any server of your own.</p></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Configuration</h3><div class="overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Option</th><th class="px-4 py-3 font-semibold text-foreground">Type</th><th class="px-4 py-3 font-semibold text-foreground">Default</th><th class="px-4 py-3 font-semibold text-foreground">Description</th></tr></thead><tbody><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-cloudinary-cloudName"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">cloudName</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">(required)</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Your Cloudinary cloud name.</p></td></tr><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-cloudinary-uploadPreset"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">uploadPreset</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">(required)</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Must be an UNSIGNED upload preset. A signed one would need a server.</p></td></tr><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-cloudinary-folder"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">folder</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">undefined</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">Optional folder to upload into.</p></td></tr></tbody></table></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Storage-side setup</h3><ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground"><li>A Cloudinary account and its cloud name.</li><li>An upload preset with signing mode set to Unsigned (Settings â Upload â Upload presets). A signed preset silently fails from the browser, because there is no server to sign the request.</li><li>Cloudinary sends audio assets through its video pipeline by its own design. This preset maps kinds onto resource types to match. You do not need to set anything up for that part.</li></ul></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Usage Example</h3><div class="code-block group relative overflow-hidden rounded-2xl border border-border bg-card" data-code-block="true" data-blok-testid="code-block"><div class="flex items-center gap-3 px-3 pt-3 pb-1.5"><span class="pl-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">TypeScript</span><button class="ml-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" data-copy="true" data-code="import { cloudinaryStorage } from '
52@bloklabs/presets'; 53 54new Blok({ 55 holder: 'editor', 56 uploader: cloudinaryStorage({ cloudName: 'my-cloud', uploadPreset: 'blok-unsigned' }), 57});" data-blok-testid="code-copy-button" type="button" aria-label="Copy"><svg class="size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></div><div class="relative mx-3 mb-3"><div class="overflow-x-auto rounded-xl pt-3 pb-3 pl-4 font-mono text-sm leading-relaxed [&_pre]:!m-0 [&_code]:font-mono pr-8"><div><pre class="shiki"><code>import { cloudinaryStorage } from '@bloklabs/presets'; 58 59new Blok({ 60 holder: 'editor', 61 uploader: cloudinaryStorage({ cloudName: 'my-cloud', uploadPreset: 'blok-unsigned' }), 62});</code></pre></div></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 left-0 w-8 rounded-l-xl transition-opacity duration-200 opacity-0"></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 right-0 w-8 rounded-r-xl transition-opacity duration-200 opacity-0"></div></div></div></div></section><section id="indexeddb" class="scroll-mt-24" data-blok-testid="presets-section-indexeddb" aria-label="IndexedDB"><div class="mb-6"><h2 class="scroll-mt-24 text-2xl font-extrabold tracking-tight text-foreground">IndexedDB</h2><div class="mt-2 max-w-2xl"><p class="mt-3 first:mt-0 text-base leading-relaxed text-muted-foreground">Stores uploaded files as blobs in the browser's own IndexedDB. Nothing leaves the device.</p></div></div><div class="mt-6 rounded-2xl border border-border bg-card p-4 shadow-sm"><p class="text-sm font-bold uppercase tracking-wide text-foreground">Re-hosting a remote URL</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">No. There is nothing to re-host to, so a remote URL just gets stored as-is. IndexedDB only has an uploadByFile.</p></div></div><div class="mt-6 rounded-2xl border border-destructive/40 bg-destructive/5 p-4"><p class="text-sm font-bold uppercase tracking-wide text-destructive">Not for production</p><div class="mt-2"><p class="mt-3 first:mt-0 text-sm text-muted-foreground">Demo and prototyping only. The bytes live in one visitor's browser. They are gone on another device, in another browser, or the moment the user clears site data.</p><p class="mt-3 first:mt-0 text-sm text-muted-foreground">This preset exists because Blok's built-in fallback (a blob: URL) doesn't even survive a page reload, which makes demos look broken before you've configured any real storage.</p></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Configuration</h3><div class="overflow-hidden rounded-2xl border border-border bg-card shadow-sm"><table class="w-full border-collapse text-left text-sm"><thead><tr class="border-b border-border bg-secondary/60"><th class="px-4 py-3 font-semibold text-foreground">Option</th><th class="px-4 py-3 font-semibold text-foreground">Type</th><th class="px-4 py-3 font-semibold text-foreground">Default</th><th class="px-4 py-3 font-semibold text-foreground">Description</th></tr></thead><tbody><tr class="border-b border-border last:border-0 transition-colors hover:bg-secondary/40" data-blok-testid="presets-config-indexeddb-dbName"><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-primary">dbName</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-foreground">string</code></td><td class="px-4 py-3 align-top"><code class="rounded-md bg-secondary px-1.5 py-0.5 font-mono text-xs text-muted-foreground">"blok-assets"</code></td><td class="px-4 py-3 align-top text-muted-foreground"><p class="mt-3 first:mt-0">IndexedDB database name.</p></td></tr></tbody></table></div></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Storage-side setup</h3><ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground"><li>None. This preset needs no external service, which is the point.</li><li>Call the exported resolveBlokObjectUrl(url) helper wherever you render an uploaded asset. uploadByFile returns a blok:asset/⦠reference, not a URL you can use directly. Nothing resolves it back into a blob: URL for you automatically.</li><li>If you pass a custom dbName to indexedDBStorage(), pass that same dbName to resolveBlokObjectUrl(url, { dbName }). A mismatch looks up the wrong database. It resolves to null with no error, and does not throw an exception.</li></ul></div><div class="mt-6"><h3 class="mb-3 text-sm font-bold uppercase tracking-wide text-foreground">Usage Example</h3><div class="code-block group relative overflow-hidden rounded-2xl border border-border bg-card" data-code-block="true" data-blok-testid="code-block"><div class="flex items-center gap-3 px-3 pt-3 pb-1.5"><span class="pl-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">TypeScript</span><button class="ml-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" data-copy="true" data-code="import { indexedDBStorage, resolveBlokObjectUrl } from '
62@bloklabs/presets'; 63 64new Blok({ holder: 'editor', uploader: indexedDBStorage() }); 65 66// Wherever you render a stored asset's url, e.g. an <img src>: 67const displayUrl = await resolveBlokObjectUrl(asset.url);" data-blok-testid="code-copy-button" type="button" aria-label="Copy"><svg class="size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></div><div class="relative mx-3 mb-3"><div class="overflow-x-auto rounded-xl pt-3 pb-3 pl-4 font-mono text-sm leading-relaxed [&_pre]:!m-0 [&_code]:font-mono pr-8"><div><pre class="shiki"><code>import { indexedDBStorage, resolveBlokObjectUrl } from '@bloklabs/presets'; 68 69new Blok({ holder: 'editor', uploader: indexedDBStorage() }); 70 71// Wherever you render a stored asset's url, e.g. an <img src>: 72const displayUrl = await resolveBlokObjectUrl(asset.url);</code></pre></div></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 left-0 w-8 rounded-l-xl transition-opacity duration-200 opacity-0"></div><div aria-hidden="true" class="pointer-events-none absolute inset-y-0 right-0 w-8 rounded-r-xl transition-opacity duration-200 opacity-0"></div></div></div></div></section></div></div></main><footer class="border-t border-border bg-secondary/40"><div class="mx-auto w-full max-w-6xl px-6 py-16"><div class="grid grid-cols-1 gap-12 md:grid-cols-[1.4fr_2fr]"><div data-blok-testid="footer-brand"><div class="flex items-center gap-2.5"><img src="/mascot.png" alt="Blok mascot" width="108" height="103" loading="lazy" decoding="async" class="size-9 object-contain"/><span class="font-display text-xl font-extrabold tracking-tight">Blok</span></div><p class="mt-4 max-w-xs text-[15px] leading-relaxed text-muted-foreground">A friendly block-based rich text editor for modern applications.</p></div><div class="grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3" data-blok-testid="footer-links"><div class="flex flex-col gap-4"><h4 class="text-[15px] font-semibold text-foreground">Documentation</h4><a class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" href="/docs/#quick-start" data-discover="true">Quick Start</a><a class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" href="/docs/" data-discover="true">API Reference</a><a class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" href="/migration/" data-discover="true">Migration Guide</a></div><div class="flex flex-col gap-4"><h4 class="text-[15px] font-semibold text-foreground">Resources</h4><a href="https://github.com/JackUait/blok" class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" target="_blank" rel="noopener noreferrer" data-blok-testid="github-link">GitHub</a><a href="https://www.npmjs.com/package/@bloklabs/core" class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" target="_blank" rel="noopener noreferrer">npm</a><a class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground
72/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" href="/demo/" data-discover="true">Live Demo</a></div><div class="flex flex-col gap-4"><h4 class="text-[15px] font-semibold text-foreground">Community</h4><a href="https://github.com/JackUait/blok/issues" class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" target="_blank" rel="noopener noreferrer">Issues</a><a href="https://github.com/JackUait/blok/discussions" class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" target="_blank" rel="noopener noreferrer">Discussions</a><a href="https://github.com/JackUait/blok/blob/main/CONTRIBUTING.md" class="inline-block w-fit bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-px text-[15px] text-foreground/70 transition-[color,background-size] duration-300 ease-out hover:bg-[length:100%_1px] hover:text-foreground motion-reduce:transition-none" target="_blank" rel="noopener noreferrer">Contributing</a></div></div></div><div class="mt-14 flex flex-col gap-4 border-t border-border pt-6 text-[15px] text-muted-foreground md:flex-row md:items-center md:justify-between" data-blok-testid="footer-bottom"><p class="flex flex-wrap items-center gap-x-2.5 gap-y-1"><span>© 2026<!-- --> <a href="https://t.me/jackuait" target="_blank" rel="noopener noreferrer" class="font-medium text-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline" data-blok-testid="author-link">JackUait</a></span><span aria-hidden="true" class="text-muted-foreground/50">·</span><span>Licensed under<!-- --> <a href="https://www.apache.org/licenses/LICENSE-2.0" target="_blank" rel="noopener noreferrer" class="font-medium text-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline" data-blok-testid="license-link">Apache 2.0</a></span></p><div class="flex items-center gap-6"><a hrefLang="ru" class="flex items-center gap-2 font-medium text-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline" aria-label="Language: Ð ÑÑÑкий" href="/ru/presets/" 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="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-[18px]" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg>Ð ÑÑÑкий</a><span class="h-4 w-px bg-border" aria-hidden="true"></span><div class="flex items-center gap-4"><a href="https://github.com/JackUait/blok" class="text-foreground/70 transition-colors hover:text-foreground" target="_blank" rel="noopener noreferrer" aria-label="GitHub"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path></svg></a>
72<a href="https://t.me/that_ai_guy" class="text-foreground/70 transition-colors hover:text-foreground" target="_blank" rel="noopener noreferrer" aria-label="Telegram"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path></svg></a></div></div></div></div></footer></div><div class="sr-only" aria-hidden="true" data-nosnippet="">A Markdown version of this page is available at https://blokeditor.com/presets.md.</div>
72<script>window.__reactRouterContext = {"basename":"/","future":{"unstable_enableNodeReadableStream":false,"unstable_optimizeDeps":false},"routeDiscovery":{"mode":"initial"},"ssr":false,"isSpaMode":false};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script>
72<script type="module" async="">import "/assets/manifest-c746c058.js"; 73import * as route0 from "/assets/root-DBwQp-RA.js"; 74import * as route1 from "/assets/presets-Cd5MP1oH.js"; 75 76 window.__reactRouterRouteModules = {"root":route0,"routes/presets":route1}; 77 78import("/assets/entry.client-CYNZ4gJQ.js");</script>
78<!--$?--><template id="B:0"></template><!--/$-->
78<script id="_R_">requestAnimationFrame(function(){$RT=performance.now()});</script>
78<div hidden id="S:0">
78<script>window.__reactRouterContext.streamController.enqueue("[{\"_1\":2,\"_3\":-5,\"_4\":-5},\"loaderData\",{},\"actionData\",\"errors\"]\n");</script>
78<!--$?--><template id="B:1"></template><!--/$--></div>
78<script>$RB=[];$RV=function(a){$RT=performance.now();for(var b=0;b<a.length;b+=2){var c=a[b],e=a[b+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&requestAnimationFrame(g._reactRetry)}}a.length=0}; 79$RC=function(a,b){if(b=document.getElementById(b))(a=document.getElementById(a))?(a.previousSibling.data="$~",$RB.push(a,b),2===$RB.length&&("number"!==typeof $RT?requestAnimationFrame($RV.bind(null,$RB)):(a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:$RT+300-a)))):b.parentNode.removeChild(b)};$RC("B:0","S:0")</script>
79<div hidden id="S:1">
79<script>window.__reactRouterContext.streamController.close();</script>
79</div>
79<script>$RC("B:1","S:1")</script>
79</body></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.