Responsive Images
Responsive Images
Recipes for working with media assets and building responsive image markup. ModulaCMS generates resized variants of uploaded images based on configured dimension presets. The media API provides the data needed to build srcset attributes and <picture> elements.
For background on the media system, see media management.
Upload an Image
Upload via multipart form POST. The server validates the file, generates dimension variants, uploads all sizes to S3, and returns the media record.
curl:
curl -X POST http://localhost:8080/api/v1/media \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@/path/to/hero.jpg"
Response (201):
{
"media_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
"name": "hero.jpg",
"display_name": "hero.jpg",
"alt": null,
"caption": null,
"description": null,
"class": null,
"mimetype": "image/jpeg",
"dimensions": "{\"width\":1920,\"height\":1080}",
"url": "https://cdn.example.com/media/hero.jpg",
"srcset": "https://cdn.example.com/media/hero-320w.jpg 320w, https://cdn.example.com/media/hero-768w.jpg 768w, https://cdn.example.com/media/hero-1920w.jpg 1920w",
"author_id": "01JMKW8N3QRYZ7T1B5K6F2P4HD",
"date_created": "2026-01-15T10:00:00Z",
"date_modified": "2026-01-15T10:00:00Z"
}
Go SDK:
import (
"os"
modula "github.com/hegner123/modulacms/sdks/go"
)
f, err := os.Open("/path/to/hero.jpg")
if err != nil {
// handle error
}
defer f.Close()
media, err := client.MediaUpload.Upload(ctx, f, "hero.jpg", nil)
if err != nil {
if modula.IsDuplicateMedia(err) {
fmt.Println("file with this name already exists")
return
}
// handle error
}
fmt.Printf("Uploaded: %s (URL: %s)\n", media.MediaID, media.URL)
Upload with a specific storage path:
media, err := client.MediaUpload.Upload(ctx, f, "hero.jpg", &modula.MediaUploadOptions{
Path: "images/heroes",
})
Upload with progress tracking:
stat, _ := f.Stat()
media, err := client.MediaUpload.UploadWithProgress(ctx, f, "hero.jpg", stat.Size(),
func(sent, total int64) {
pct := float64(sent) / float64(total) * 100
fmt.Printf("\r%.0f%%", pct)
},
nil,
)
TypeScript SDK (admin):
// Browser environment
const fileInput = document.querySelector<HTMLInputElement>('#file')
const file = fileInput!.files![0]
const media = await admin.mediaUpload.upload(file)
console.log(`Uploaded: ${media.media_id} (URL: ${media.url})`)
// With options
const media = await admin.mediaUpload.upload(file, {
path: 'images/heroes',
})
List Dimension Presets
Dimension presets define the target sizes for responsive image variants. Each uploaded image is resized to match every active preset.
curl:
curl http://localhost:8080/api/v1/mediadimensions \
-H "Authorization: Bearer YOUR_API_KEY"
Response:
[
{ "md_id": "01HXK5A1...", "label": "thumbnail", "width": 150, "height": 150, "aspect_ratio": "1:1" },
{ "md_id": "01HXK5B2...", "label": "small", "width": 320, "height": null, "aspect_ratio": null },
{ "md_id": "01HXK5C3...", "label": "medium", "width": 768, "height": null, "aspect_ratio": null },
{ "md_id": "01HXK5D4...", "label": "large", "width": 1280, "height": null, "aspect_ratio": null },
{ "md_id": "01HXK5E5...", "label": "hero", "width": 1920, "height": null, "aspect_ratio": "16:9" }
]
Go SDK:
dims, err := client.MediaDimensions.List(ctx)
if err != nil {
// handle error
}
for _, d := range dims {
label := ""
if d.Label != nil {
label = *d.Label
}
w := int64(0)
if d.Width != nil {
w = *d.Width
}
fmt.Printf("%s: %dpx wide\n", label, w)
}
TypeScript SDK (read-only):
const dims = await client.listMediaDimensions()
for (const d of dims) {
console.log(`${d.label}: ${d.width}px wide`)
}
TypeScript SDK (admin):
const dims = await admin.mediaDimensions.list()
Create a Dimension Preset
curl:
curl -X POST http://localhost:8080/api/v1/mediadimensions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "social-card", "width": 1200, "height": 630, "aspect_ratio": "1.91:1"}'
Go SDK:
w := int64(1200)
h := int64(630)
label := "social-card"
ratio := "1.91:1"
dim, err := client.MediaDimensions.Create(ctx, modula.CreateMediaDimensionParams{
Label: &label,
Width: &w,
Height: &h,
AspectRatio: &ratio,
})
TypeScript SDK (admin):
const dim = await admin.mediaDimensions.create({
label: 'social-card',
width: 1200,
height: 630,
aspect_ratio: '1.91:1',
})
Build an HTML srcset from Media Data
The srcset field on a media record contains a prebuilt srcset string. Use it directly in your HTML.
Go (template helper):
func responsiveImage(m modula.Media) string {
alt := ""
if m.Alt != nil {
alt = *m.Alt
}
// Use prebuilt srcset if available
if m.Srcset != nil && *m.Srcset != "" {
return fmt.Sprintf(
`<img src="%s" srcset="%s" sizes="(max-width: 768px) 100vw, 50vw" alt="%s">`,
m.URL, *m.Srcset, alt,
)
}
// Fallback: single image
return fmt.Sprintf(`<img src="%s" alt="%s">`, m.URL, alt)
}
TypeScript:
function responsiveImage(media: Media): string {
const alt = media.alt ?? ''
if (media.srcset) {
return `<img src="${media.url}" srcset="${media.srcset}" sizes="(max-width: 768px) 100vw, 50vw" alt="${alt}">`
}
return `<img src="${media.url}" alt="${alt}">`
}
Build srcset manually from dimension presets and a known URL pattern:
function buildSrcset(baseUrl: string, dims: MediaDimension[]): string {
return dims
.filter(d => d.width !== null)
.sort((a, b) => (a.width ?? 0) - (b.width ?? 0))
.map(d => {
// Convention: dimension variants use -{width}w suffix before extension
const ext = baseUrl.substring(baseUrl.lastIndexOf('.'))
const base = baseUrl.substring(0, baseUrl.lastIndexOf('.'))
return `${base}-${d.width}w${ext} ${d.width}w`
})
.join(', ')
}
Get Media References
Find which content fields reference a specific media asset. Useful before deleting media to understand the impact.
curl:
curl "http://localhost:8080/api/v1/media/references?q=01HXK4N2F8RJZGP6VTQY3MCSW9" \
-H "Authorization: Bearer YOUR_API_KEY"
TypeScript SDK (admin):
const refs = await admin.media.getReferences('01HXK4N2F8RJZGP6VTQY3MCSW9' as MediaID)
console.log(`Found ${JSON.stringify(refs)} references`)
Delete Media with Cleanup
Delete a media asset and automatically remove all content field references to it. This removes the S3 files for all dimension variants and nullifies any content fields that referenced the asset.
curl:
curl -X DELETE "http://localhost:8080/api/v1/media/?q=01HXK4N2F8RJZGP6VTQY3MCSW9&clean_refs=true" \
-H "Authorization: Bearer YOUR_API_KEY"
Go SDK:
// Standard delete (fails if references exist)
err := client.Media.Delete(ctx, modula.MediaID("01HXK4N2F8RJZGP6VTQY3MCSW9"))
// For admin operations with reference cleanup, use the admin media resource
TypeScript SDK (admin):
// Standard delete
await admin.media.remove('01HXK4N2F8RJZGP6VTQY3MCSW9' as MediaID)
// Delete with cleanup (removes S3 files and cleans content references)
await admin.media.deleteWithCleanup('01HXK4N2F8RJZGP6VTQY3MCSW9' as MediaID)
Check Media Health
Scan for orphaned files in the media S3 bucket that have no corresponding database record.
curl:
curl http://localhost:8080/api/v1/media/health \
-H "Authorization: Bearer YOUR_API_KEY"
TypeScript SDK (admin):
// Check for orphaned files
const health = await admin.media.health()
// Clean up orphaned files
const cleanup = await admin.media.cleanup()
Next Steps
- Content Modeling -- define media fields on your datatypes
- Media Management -- full media system documentation