Image Plugins
Marzipan provides two powerful image plugins for different image insertion workflows, from simple URL input to advanced management with drag-drop and paste support.
Overview
| Plugin | Best For | Features |
|---|---|---|
| imagePickerPlugin | Quick URL insertion | Simple URL input, instant insert |
| imageManagerPlugin | Full image management | Upload, recents, drag-drop, paste, thumbnails |
Both plugins insert markdown image syntax:

Image Picker Plugin
Quick and simple image URL insertion.
Installation
import { Marzipan } from '@pinkpixel/marzipan';
import { imagePickerPlugin } from '@pinkpixel/marzipan/plugins/imagePickerPlugin';
new Marzipan('#editor', {
toolbar: true,
plugins: [imagePickerPlugin()]
});
Options
interface ImagePickerOptions {
label?: string; // Toolbar button label (default: 🖼️)
title?: string; // Tooltip text (default: 'Insert image')
placeholder?: string; // Default URL shown in the prompt
promptMessage?: string; // Custom prompt message
}
Usage Example
import { imagePickerPlugin } from '@pinkpixel/marzipan/plugins/imagePickerPlugin';
new Marzipan('#editor', {
plugins: [
imagePickerPlugin({
label: '📷',
title: 'Add an image',
placeholder: 'Enter image URL...'
})
]
});
Features
- ✅ Simple URL input dialog
- ✅ Instant markdown insertion
- ✅ Lightweight and fast
- ✅ No storage or complexity
- ✅ Perfect for basic needs
- ✅ Customise the prompt message and default URL
How It Works
- Click the image picker button
- Enter image URL in the prompt
- Image markdown is inserted at cursor
- Preview updates automatically
User Experience
Click button → Enter URL: "https://example.com/photo.jpg"
Result inserted:

Image Manager Plugin
Full-featured image management with recents, upload, and more.
Installation
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';
new Marzipan('#editor', {
plugins: [imageManagerPlugin()]
});
Options
interface ImageManagerOptions {
label?: string; // Toolbar button (default: 🗂️)
title?: string; // Tooltip (default: 'Images')
maxRecent?: number; // Max recent images (default: 24)
persistThresholdBytes?: number; // localStorage size limit (default: 1MB)
uploader?: (file: File) => Promise<string>; // Custom upload handler
onInsert?: (url: string) => string; // Transform URL before insert
}
Usage Examples
Basic Usage:
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';
new Marzipan('#editor', {
plugins: [imageManagerPlugin()]
});
With Custom Upload:
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';
new Marzipan('#editor', {
plugins: [
imageManagerPlugin({
maxRecent: 12,
// Upload to your server
uploader: async (file) => {
const formData = new FormData();
formData.append('image', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
return data.url; // Return the uploaded image URL
},
// Transform URLs (e.g., add CDN prefix)
onInsert: (url) => {
if (url.startsWith('http')) return url;
return `https://cdn.example.com${url}`;
}
})
]
});
Cloudflare R2 Upload Example:
imageManagerPlugin({
uploader: async (file) => {
// Generate presigned URL for R2
const presignedResponse = await fetch('/api/upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type
})
});
const { uploadUrl, publicUrl } = await presignedResponse.json();
// Upload to R2
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
});
return publicUrl;
}
})
Features
- ✅ Recent Images - Thumbnail grid of recent uploads
- ✅ Drag & Drop - Drop files onto editor or panel
- ✅ Paste Support - Paste images from clipboard
- ✅ URL Import - Add images from URLs
- ✅ Local Upload - Upload files from device
- ✅ localStorage Persistence - Small images saved locally
- ✅ Custom Upload - Integrate with your backend
- ✅ Thumbnail Preview - Visual selection
- ✅ Remove/Clear - Manage recent images
How It Works
Adding Images:
- Click the images button
- Choose method:
- Add URL: Enter image URL
- Upload: Select file from device
- Drag-drop: Drop file on drop zone or editor
- Paste: Copy image and paste in editor
Using Recents:
- Open images panel
- Click thumbnail to insert
- Right-click × to remove from recents
Storage Behavior
Small Files (< 1MB by default):
- Converted to data URLs
- Stored in localStorage
- Available across sessions
- Instant loading
Large Files:
- Created as blob URLs
- Session-only (cleared on page refresh)
- No localStorage usage
With Custom Uploader:
- All files uploaded to your server
- Remote URLs stored in recents
- Persistent across sessions
Styling
The plugin exports imageManagerStyles for custom styling:
import {
imageManagerPlugin,
imageManagerStyles
} from '@pinkpixel/marzipan/plugins/imageManagerPlugin';
// Inject styles
const styleElement = document.createElement('style');
styleElement.textContent = imageManagerStyles;
document.head.appendChild(styleElement);
new Marzipan('#editor', {
plugins: [imageManagerPlugin()]
});
Custom CSS Variables
/* Image Manager Panel */
--mz-ip-bg: #0f1216; /* Input background */
--mz-ip-fg: #e7e7e7; /* Input text */
--mz-ip-bd: #2b2f36; /* Input border */
--mz-btn-bg: #1a1e24; /* Button background */
--mz-btn-fg: #e7e7e7; /* Button text */
--mz-btn2-bg: #1b2027; /* Card button background */
--mz-btn2-fg: #e7e7e7; /* Card button text */
--mz-drop-bg: #0e1116; /* Drop zone background */
--mz-drop-on: #172033; /* Drop zone hover */
--mz-drop-bd: #3b4350; /* Drop zone border */
--mz-card-bg: #12151a; /* Thumbnail card background */
--mz-card-bd: #2b2f36; /* Thumbnail card border */
Complete Example
import { Marzipan } from '@pinkpixel/marzipan';
import {
imageManagerPlugin,
imageManagerStyles
} from '@pinkpixel/marzipan/plugins';
// Inject styles
const style = document.createElement('style');
style.textContent = imageManagerStyles;
document.head.appendChild(style);
const [editor] = new Marzipan('#editor', {
toolbar: true,
theme: 'cave',
plugins: [
imageManagerPlugin({
maxRecent: 16,
persistThresholdBytes: 500 * 1024, // 500KB limit
// Custom upload to your API
uploader: async (file) => {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/images/upload', {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
return data.imageUrl;
},
// Add alt text transformation
onInsert: (url) => {
// Could add width/height parameters
return url;
}
})
],
value: `# My Document
Here are some images:

`
});
Image Best Practices
Optimization
- Compress images before uploading
- Use appropriate formats (WebP when possible)
- Set size limits to prevent huge uploads
- Use lazy loading for many images
- Consider CDN for production apps
Alt Text
Always include descriptive alt text for accessibility:

Not just:

Drag & Drop
The image manager supports drag & drop in two areas:
Drop Zone in Panel
Drop images onto the highlighted drop zone for quick upload.
Drop on Editor
Drop images anywhere on the editor container for instant insertion.
// Both areas support multiple files at once
// Files are processed sequentially
Paste Support
Copy images from:
- Screenshots (⌘+Shift+4 on Mac, Win+Shift+S on Windows)
- Other applications
- Web browsers
Then paste directly into the editor:
- Focus the editor textarea
- Paste (⌘+V / Ctrl+V)
- Image is automatically processed and inserted
Upload API Integration
Example backend endpoints:
Simple Upload (Express.js)
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const upload = multer({
dest: 'uploads/',
limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
});
app.post('/api/upload', upload.single('image'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const url = `/uploads/${req.file.filename}`;
res.json({ url });
});
app.use('/uploads', express.static('uploads'));
app.listen(3000);
S3/R2 Upload (Node.js)
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const s3 = new S3Client({
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY,
secretAccessKey: process.env.R2_SECRET_KEY
}
});
app.post('/api/upload-url', async (req, res) => {
const { filename, contentType } = req.body;
const key = `images/${Date.now()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: key,
ContentType: contentType
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
const publicUrl = `${process.env.R2_PUBLIC_URL}/${key}`;
res.json({ uploadUrl, publicUrl });
});
Security Considerations
When implementing image uploads:
File Validation
imageManagerPlugin({
uploader: async (file) => {
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
// Validate file size
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
throw new Error('File too large');
}
// Upload logic...
}
})
Server-Side Validation
app.post('/api/upload', upload.single('image'), (req, res) => {
const file = req.file;
// Check MIME type
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedMimes.includes(file.mimetype)) {
return res.status(400).json({ error: 'Invalid file type' });
}
// Check file size
if (file.size > 5 * 1024 * 1024) {
return res.status(400).json({ error: 'File too large' });
}
// Process upload...
});
Content Security
- Sanitize filenames to prevent path traversal
- Generate unique IDs for stored files
- Scan for malware if accepting user uploads
- Use signed URLs for temporary access
- Implement rate limiting to prevent abuse
Browser Compatibility
Image plugins work in:
- Chrome/Edge 60+
- Firefox 55+
- Safari 11+
Clipboard API (paste support):
- Chrome/Edge 76+
- Firefox 87+
- Safari 13.1+
File System Access (drag-drop):
- All modern browsers
Troubleshooting
Images Not Persisting
Check localStorage size:
// Current usage
const used = new Blob(Object.values(localStorage)).size;
console.log(`localStorage: ${(used / 1024).toFixed(2)} KB`);
Reduce persistThresholdBytes if hitting limits.
Upload Fails
- Check CORS headers on your server
- Verify authentication tokens
- Check file size limits
- Validate file types match server expectations
Paste Not Working
- Ensure editor textarea has focus
- Check browser clipboard permissions
- Verify secure context (HTTPS or localhost)
See Also
- Plugin Overview - All available plugins
- Configuration - Plugin configuration
- Marzipan API - Editor instance methods
- Bakeshop Demo - Live examples
Marzipan