Media API
Overview
The Media API handles file uploads, metadata management, and reference tracking for images and documents in Aether CMS. It supports automatic file optimization, metadata propagation, and comprehensive reference management.
File Types
Supported File Types
Images:
- JPEG, JPG (.jpg, .jpeg)
- PNG (.png)
- GIF (.gif)
- WebP (.webp)
- SVG (.svg)
- AVIF (.avif)
- ICO (.ico)
Documents:
- PDF, DOC, DOCX
- TXT, MD
- Other document formats
File types are automatically detected by MIME type and file extension.
Media Endpoints
Get All Media Files
GET /api/media
Query Parameters:
type
- File type filter (image
ordocument
, default:image
)
Success Response (200):
{
"success": true,
"data": [
{
"id": "a1b2c3d4e5f6",
"filename": "hero-image-a1b2c3d4e5f6.jpg",
"originalFilename": "hero-image.jpg",
"url": "/images/hero-image-a1b2c3d4e5f6.jpg",
"size": 245760,
"type": "image",
"createdAt": "2024-01-01T00:00:00.000Z",
"modifiedAt": "2024-01-01T00:00:00.000Z",
"alt": "Hero image for homepage",
"width": 1920,
"height": 1080
}
]
}
Upload Media File
POST /api/media/upload
Content-Type: multipart/form-data
Form Fields:
file
- The file to upload (required)alt
- Alt text for images (optional)width
- Image width in pixels (optional)height
- Image height in pixels (optional)
Example using JavaScript FormData:
const formData = new FormData()
formData.append("file", fileInput.files[0])
formData.append("alt", "Description of the image")
formData.append("width", "1920")
formData.append("height", "1080")
const response = await fetch("/api/media/upload", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
})
Success Response (201):
{
"success": true,
"data": {
"id": "f6e5d4c3b2a1",
"filename": "uploaded-image-f6e5d4c3b2a1.jpg",
"originalFilename": "uploaded-image.jpg",
"path": "/path/to/uploads/images/uploaded-image-f6e5d4c3b2a1.jpg",
"url": "/images/uploaded-image-f6e5d4c3b2a1.jpg",
"size": 102400,
"type": "image",
"createdAt": "2024-01-01T12:00:00.000Z",
"alt": "Description of the image",
"width": 1920,
"height": 1080
}
}
Get Single Media File
GET /api/media/{id}
Query Parameters:
checkReferences
- Include reference information (true
/false
)includeDetails
- Include detailed reference data (true
/false
)
Success Response (200):
{
"success": true,
"data": {
"id": "f6e5d4c3b2a1",
"filename": "uploaded-image-f6e5d4c3b2a1.jpg",
"originalFilename": "uploaded-image.jpg",
"url": "/images/uploaded-image-f6e5d4c3b2a1.jpg",
"size": 102400,
"type": "image",
"createdAt": "2024-01-01T12:00:00.000Z",
"alt": "Description of the image",
"width": 1920,
"height": 1080
},
"referenced": true,
"references": [
{
"id": "post_123",
"title": "My Blog Post",
"type": "post",
"referenceType": "featuredImage",
"slug": "my-blog-post",
"status": "published"
}
]
}
Update Media Metadata
PUT /api/media/{id}
Request Body:
{
"alt": "Updated alt text",
"caption": "Updated caption text"
}
Success Response (200):
{
"success": true,
"data": {
"id": "f6e5d4c3b2a1",
"filename": "uploaded-image-f6e5d4c3b2a1.jpg",
"alt": "Updated alt text",
"caption": "Updated caption text",
"updatedAt": "2024-01-01T13:00:00.000Z"
}
}
Delete Media File
DELETE /api/media/{id}
Query Parameters:
clean
- Remove references from content (true
/false
)
Success Response (200):
{
"success": true,
"referencesFound": 2,
"referencesRemoved": 2
}
Reference Management
Check Media References
GET /api/media/{id}/references
Query Parameters:
type
- Media type (image
ordocument
)
Success Response (200):
{
"success": true,
"referenced": true,
"references": [
{
"id": "post_123",
"title": "My Blog Post",
"type": "post",
"referenceType": "featuredImage"
},
{
"id": "post_456",
"title": "Another Post",
"type": "post",
"referenceType": "embedded"
}
],
"file": {
"id": "f6e5d4c3b2a1",
"filename": "image.jpg",
"url": "/images/image.jpg"
}
}
Reference Types
Featured Image:
- Media used as the featured image of a post/page
- Stored in frontmatter as
featuredImage
object
Embedded:
- Media referenced within content body
- Includes Markdown images, HTML img tags, figure elements
Propagate Metadata Changes
POST /api/media/{id}/propagate-metadata
Updates alt text and captions across all content that references this media file.
Request Body:
{
"oldAlt": "Old alt text",
"newAlt": "New alt text",
"oldCaption": "Old caption",
"newCaption": "New caption"
}
Success Response (200):
{
"success": true,
"updatedCount": 3,
"altUpdates": true,
"captionUpdates": true,
"message": "Updated metadata in 3 references"
}
File Management Features
Automatic File Naming
Files are automatically renamed to prevent conflicts:
Original: hero-image.jpg
Stored: hero-image-a1b2c3d4e5f6.jpg
- Normalized filename (lowercase, safe characters)
- Unique ID appended
- Original filename preserved in metadata
File Organization
content/uploads/
├── images/
│ ├── hero-image-a1b2c3d4e5f6.jpg
│ ├── hero-image-a1b2c3d4e5f6.jpg.metadata.json
│ └── ...
└── documents/
├── guide-b2c3d4e5f6a1.pdf
├── guide-b2c3d4e5f6a1.pdf.metadata.json
└── ...
Metadata Storage
Each file has an associated .metadata.json
file:
{
"alt": "Hero image for homepage",
"width": 1920,
"height": 1080,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
Usage Examples
Upload Image with Metadata
async function uploadImage(file, altText) {
const formData = new FormData()
formData.append("file", file)
formData.append("alt", altText)
// Add dimensions if available
if (file.width && file.height) {
formData.append("width", file.width.toString())
formData.append("height", file.height.toString())
}
try {
const response = await fetch("/api/media/upload", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
})
const result = await response.json()
if (result.success) {
console.log("Image uploaded:", result.data)
return result.data
} else {
throw new Error(result.error)
}
} catch (error) {
console.error("Upload failed:", error)
throw error
}
}
Get Media Library
async function getMediaLibrary(type = "image") {
try {
const response = await fetch(`/api/media?type=${type}`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
const result = await response.json()
if (result.success) {
return result.data
} else {
throw new Error(result.error)
}
} catch (error) {
console.error("Failed to fetch media:", error)
return []
}
}
Check and Clean References
async function deleteMediaSafely(mediaId) {
try {
// First check references
const refResponse = await fetch(`/api/media/${mediaId}/references`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
const refResult = await refResponse.json()
if (refResult.success && refResult.referenced) {
console.log(`Found ${refResult.references.length} references`)
// Ask user for confirmation
const shouldClean = confirm(
`This media is referenced in ${refResult.references.length} content items. Remove references and delete?`
)
if (!shouldClean) {
return false
}
}
// Delete with cleaning
const deleteResponse = await fetch(`/api/media/${mediaId}?clean=true`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
})
const deleteResult = await deleteResponse.json()
if (deleteResult.success) {
console.log(`Media deleted. ${deleteResult.referencesRemoved} references cleaned.`)
return true
} else {
throw new Error(deleteResult.error)
}
} catch (error) {
console.error("Failed to delete media:", error)
return false
}
}
Update Media Metadata
async function updateMediaMetadata(mediaId, metadata) {
try {
const response = await fetch(`/api/media/${mediaId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(metadata),
})
const result = await response.json()
if (result.success) {
// Optionally propagate changes to content
if (metadata.alt || metadata.caption) {
await propagateMetadataChanges(mediaId, metadata)
}
return result.data
} else {
throw new Error(result.error)
}
} catch (error) {
console.error("Failed to update metadata:", error)
throw error
}
}
async function propagateMetadataChanges(mediaId, updates) {
try {
const response = await fetch(`/api/media/${mediaId}/propagate-metadata`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
oldAlt: updates.oldAlt,
newAlt: updates.alt,
oldCaption: updates.oldCaption,
newCaption: updates.caption,
}),
})
const result = await response.json()
if (result.success) {
console.log(`Metadata propagated to ${result.updatedCount} content items`)
}
} catch (error) {
console.error("Failed to propagate metadata:", error)
}
}
Error Handling
Common Error Responses
400 Bad Request:
{
"success": false,
"error": "No file uploaded"
}
404 Not Found:
{
"success": false,
"error": "File not found"
}
413 Payload Too Large:
{
"success": false,
"error": "File too large"
}
415 Unsupported Media Type:
{
"success": false,
"error": "Unsupported file type"
}
File Validation
The API validates:
- File size limits
- File type restrictions
- MIME type verification
- Filename sanitization
- Metadata format
Security Considerations
- Files are scanned for malicious content (Future Feature)
- Filenames are sanitized to prevent path traversal
- File types are validated by both extension and MIME type
- Metadata is partially validated and sanitized (Planned to be improved)
Integration Examples
Featured Image Selection
// Select media for featured image
async function selectFeaturedImage() {
const mediaLibrary = await getMediaLibrary("image")
// Show media selection UI
const selectedMedia = await showMediaSelector(mediaLibrary)
if (selectedMedia) {
return {
id: selectedMedia.id,
url: selectedMedia.url,
alt: selectedMedia.alt,
width: selectedMedia.width,
height: selectedMedia.height,
}
}
return null
}
Content Image Insertion
// Insert image into content editor
function insertImageIntoContent(editor, mediaItem) {
const imageMarkdown = ``
// Insert at cursor position
editor.insertText(imageMarkdown)
}
Bulk Media Operations
async function bulkDeleteMedia(mediaIds) {
const results = []
for (const id of mediaIds) {
try {
const result = await deleteMediaSafely(id)
results.push({ id, success: result })
} catch (error) {
results.push({ id, success: false, error: error.message })
}
}
return results
}