Media API

On this page

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 or document, 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 or document)

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 = `![${mediaItem.alt}](/content/uploads${mediaItem.url})`

    // 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
}