After having trouble with fof upload extension for Flarum, I wanted something lightweight, fast, and maintenance-free — without relying on complex PHP backends or heavy extensions.
So I created (with IA help) this pure JavaScript Cloudinary integration, which adds a sleek “Upload Images” button direct on the Flarum composer (text editor).
This small script adds a clean and fast image upload button directly into the Flarum Composer, allowing users to upload images to Cloudinary and automatically insert them into their posts as Markdown (
).
It’s designed to be lightweight, independent of extensions, and optimized for performance — ideal for users who want Cloudinary integration without slowing down their forum.
Why built it
-I have trouble with Fof Upload and couldn't fix it.
While extensions like fof/upload are powerful, they can sometimes:
-Require server-side setup or API credentials.
-Add extra load to your server.
-Be overkill for small forums or shared hosting environments.
This script solves all that. It’s:
✅ Client-side only — no PHP or backend configuration.
⚡ Extremely lightweight — pure JS, under 2 KB.
🔒 Secure — uses a Cloudinary upload preset for controlled uploads.
🌍 CDN-optimized — images are auto-transformed for size and quality.
🖼️ Seamless UX — integrated directly into Flarum’s composer toolbar.
How it works
Just paste this code into your Custom Footer field under Admin → Appearance → Custom Footer, save changes, and reload your forum.
<script>document.addEventListener("DOMContentLoaded",function(){const e="**YOUR_CLOUDINARY_NAME_ACCOUNT**",t="**YOUR_UPLOAD_PRESET_uploads**",n="**YOUR_CLOUDINARY_FOLDER_NAME**",o=setInterval(()=>{const a=document.querySelector(".Composer-footer,.Composer-controls");if(!a)return;clearInterval(o);if(document.getElementById("cloudinaryUploadBtn"))return;const r=document.createElement("button");r.id="cloudinaryUploadBtn",r.type="button",r.className="Button Button--icon upload-btn",r.title="Subir imágenes desde ordenador",r.innerHTML=`<i class="fas fa-upload"></i>`,a.prepend(r),r.addEventListener("mouseenter",()=>{const e=document.createElement("div");e.className="upload-tooltip",e.innerText=r.title,document.body.appendChild(e);const t=r.getBoundingClientRect();e.style.left=t.left+t.width/2+"px",e.style.top=t.top-28+"px",r._tooltip=e}),r.addEventListener("mouseleave",()=>{r._tooltip&&r._tooltip.remove()}),r.addEventListener("click",()=>{const a=document.createElement("input");a.type="file",a.accept="image/*",a.click(),a.onchange=async()=>{const i=a.files[0];if(!i)return;r.disabled=!0,r.style.opacity=".6",r.innerHTML="⏳";try{const a=new FormData;a.append("file",i),a.append("upload_preset",t),n&&a.append("folder",n);const l=await fetch(`https://api.cloudinary.com/v1_1/${e}/image/upload`,{method:"POST",body:a}),d=await l.json();if(!d.secure_url)throw new Error(JSON.stringify(d));const s=d.secure_url.replace("/upload/","/upload/f_auto,q_auto,w_1200/"),c=document.querySelector(".Composer textarea,.Composer-editor textarea");if(c){const e=c.selectionStart||c.value.length,t=c.selectionEnd||c.value.length,n=c.value;c.value=n.slice(0,e)+`\n\n`+n.slice(t),c.dispatchEvent(new Event("input",{bubbles:!0}))}else{const e=document.querySelector(".Composer [contenteditable='true']");e&&(e.focus(),document.execCommand("insertText",!1,``))}r.innerHTML="✅",setTimeout(()=>r.innerHTML=`<i class="fas fa-upload"></i>`,1200)}catch(e){console.error("Cloudinary upload error:",e),alert("Error al subir la imagen. Revisa la consola."),r.innerHTML="❌",setTimeout(()=>r.innerHTML=`<i class="fas fa-upload"></i>`,2e3)}finally{r.disabled=!1,r.style.opacity="1"}}})},500)});</script>
<style>.upload-btn{background:none;border:none;cursor:pointer;color:#6c63ff;transition:.2s;display:inline-flex;align-items:center}.upload-btn:hover{color:#4b47c5;transform:scale(1.1)}.upload-tooltip{position:absolute;background:#333;color:#fff;padding:4px 8px;font-size:12px;border-radius:4px;white-space:nowrap;opacity:.9;transform:translateX(-50%);pointer-events:none;z-index:9999}</style>
Example:

The result: users to upload images to Cloudinary and automatically insert them into their posts

It listens for the composer to load, injects a small upload button (<i class="fas fa-upload"></i>), and when clicked, it opens the native file picker.
Once a file is selected, it uploads directly to Cloudinary using your unsigned upload preset, and automatically inserts the optimized image markdown into your post.
What You Need from Cloudinary
-Cloud Name → found in your Cloudinary Dashboard under Account Details.
-Upload Preset → create one in your Media Library → Settings → Upload Presets.
-Make sure it’s unsigned, so users can upload without API keys.
-Folder Name → the Cloudinary folder where uploads will be organized (e.g., your website name).
✅ No dependency on extra Flarum extensions
✅ Fast uploads with automatic Cloudinary optimization (f_auto,q_auto,w_1200)
✅ Works perfectly with Markdown image syntax
✅ Beautiful hover tooltip and FontAwesome upload icon
✅ Keeps your Flarum lightweight
⚠️ Side Effect Notice
BBC New More extension (BBC it works perfectly) may not work properly alongside this script. This is likely due to conflicts with how the Composer handles formatted text. If you rely heavily on BBCodes New More, test before deploying.