Skip to main content

Overview

The Postiz Media Library is your centralized hub for managing all media assets used across your social media posts. Upload, organize, and reuse images, videos, and other media files efficiently.

Accessing the Media Library

The media library can be accessed in several ways:

Post Editor

Click “Insert Media” when creating or editing a post

Standalone Page

Navigate to Media Library from the main menu

Quick Upload

Drag and drop files directly into the post editor

API Upload

Upload via REST API for integration workflows

Uploading Media

Upload Methods

Simply drag files from your computer into the media library.
const dragAndDrop = async (files: File[]) => {
  const totalSize = files.reduce((acc, file) => acc + file.size, 0);
  
  // Maximum 1GB per upload session
  if (totalSize > MAX_UPLOAD_SIZE) {
    toaster.show(
      'Upload size limit exceeded. Maximum 1 GB per upload.',
      'warning'
    );
    return;
  }
  
  for (const file of files) {
    uppy.addFile(file);
  }
};

Upload Limits

  • Maximum file size: 1 GB per upload session
  • Supported formats: Images (JPEG, PNG, GIF, WebP), Videos (MP4, MOV)
  • Storage: Cloud-based (Cloudflare R2 or local storage)

Upload Service

Postiz uses Uppy for robust file uploading:
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';

const useUppyUploader = ({ 
  allowedFileTypes,
  onUploadSuccess,
  onStart,
  onEnd 
}) => {
  const uppy = new Uppy({
    restrictions: {
      allowedFileTypes: allowedFileTypes.split(','),
      maxFileSize: MAX_UPLOAD_SIZE,
    },
  });
  
  uppy.on('upload', () => {
    onStart?.();
  });
  
  uppy.on('complete', async (result) => {
    const files = result.successful.map(file => ({
      id: file.meta.mediaId,
      path: file.meta.mediaPath
    }));
    
    onUploadSuccess?.(files);
    onEnd?.();
  });
  
  return uppy;
};

Media Storage Backend

Storage Factory

Postiz supports multiple storage backends:
// libraries/nestjs-libraries/src/upload/upload.factory.ts
export class UploadFactory {
  static createStorage() {
    const type = process.env.UPLOAD_DIRECTORY;
    
    switch (type) {
      case 'cloudflare':
        return new CloudflareStorage();
      case 'local':
        return new LocalStorage();
      default:
        return new LocalStorage();
    }
  }
}

Cloudflare R2 Storage

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

export class CloudflareStorage {
  private s3Client: S3Client;
  
  constructor() {
    this.s3Client = new S3Client({
      region: 'auto',
      endpoint: process.env.CLOUDFLARE_ENDPOINT,
      credentials: {
        accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID,
        secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY,
      },
    });
  }
  
  async uploadFile(file: Express.Multer.File) {
    const key = `${Date.now()}-${file.originalname}`;
    
    await this.s3Client.send(
      new PutObjectCommand({
        Bucket: process.env.CLOUDFLARE_BUCKET_NAME,
        Key: key,
        Body: file.buffer,
        ContentType: file.mimetype,
      })
    );
    
    return {
      path: `${process.env.CLOUDFLARE_BUCKET_URL}/${key}`,
      originalname: file.originalname
    };
  }
}

Browsing Media

Media Grid View

The media library displays files in a paginated grid:
const MediaBox = ({ setMedia, type }) => {
  const [page, setPage] = useState(0);
  
  const loadMedia = useCallback(async () => {
    return await fetch(`/media?page=${page + 1}`).json();
  }, [page]);
  
  const { data, isLoading } = useSWR(`get-media-${page}`, loadMedia);
  
  return (
    <div className="media-grid">
      {data?.results?.map(media => (
        <MediaCard key={media.id} media={media} />
      ))}
      
      {data?.pages > 1 && (
        <Pagination 
          current={page}
          totalPages={data.pages}
          setPage={setPage}
        />
      )}
    </div>
  );
};

Filtering Media

const imageFiles = data?.results?.filter(f => 
  f.path.indexOf('mp4') === -1
);

Media API Endpoints

Fetching Media

// GET /media?page=1
interface MediaResponse {
  results: Media[];
  pages: number;
  total: number;
}

interface Media {
  id: string;
  name: string;
  path: string;
  originalName?: string;
  createdAt: string;
  organizationId: string;
}

Saving Media

// Controller: media.controller.ts
@Post('/upload-simple')
async uploadSimple(
  @GetOrgFromRequest() org: Organization,
  @UploadedFile() file: Express.Multer.File,
  @Body('preventSave') preventSave: string = 'false'
) {
  const originalName = file.originalname;
  const uploadedFile = await this.storage.uploadFile(file);
  
  if (preventSave === 'true') {
    return { path: uploadedFile.path };
  }
  
  return this._mediaService.saveFile(
    org.id,
    uploadedFile.originalname,
    uploadedFile.path,
    originalName
  );
}

Deleting Media

// DELETE /media/:id
const deleteImage = async (mediaId: string) => {
  if (!await deleteDialog(
    'Are you sure you want to delete the image?'
  )) {
    return;
  }
  
  await fetch(`/media/${mediaId}`, {
    method: 'DELETE'
  });
  
  mutate(); // Refresh media list
};

Using Media in Posts

Selecting Media

const MultiMediaComponent = ({ name, onChange, value }) => {
  const [currentMedia, setCurrentMedia] = useState(value);
  
  const showModal = () => {
    modals.openModal({
      title: 'Media Library',
      fullScreen: true,
      children: (close) => (
        <MediaBox 
          setMedia={(media) => {
            const newMedia = [...currentMedia, ...media];
            setCurrentMedia(newMedia);
            onChange({
              target: { name, value: newMedia }
            });
            close();
          }}
          closeModal={close}
        />
      )
    });
  };
  
  return (
    <button onClick={showModal}>
      Insert Media
    </button>
  );
};

Media Settings

Configure media-specific settings for each post:
interface MediaSettings {
  alt?: string;              // Alt text for accessibility
  thumbnail?: string;        // Video thumbnail
  thumbnailTimestamp?: number; // Video thumbnail timestamp
}

const MediaComponentInner = ({ media, onSelect, onClose }) => {
  const [settings, setSettings] = useState<MediaSettings>({
    alt: media.alt || '',
    thumbnail: media.thumbnail,
    thumbnailTimestamp: media.thumbnailTimestamp || 0
  });
  
  const save = () => {
    onSelect(settings);
    onClose();
  };
  
  return (
    <div>
      <Input 
        label="Alt Text"
        value={settings.alt}
        onChange={(e) => setSettings({ ...settings, alt: e.target.value })}
      />
      {/* Video thumbnail settings */}
      <button onClick={save}>Save</button>
    </div>
  );
};

Media Organization

Drag to Reorder

Reorder media in posts using drag and drop:
import { ReactSortable } from 'react-sortablejs';

<ReactSortable
  list={currentMedia}
  setList={(value) => onChange({ target: { name: 'upload', value } })}
  className="flex gap-[10px]"
  animation={200}
  swap={true}
  handle=".dragging"
>
  {currentMedia.map((media, index) => (
    <MediaCard key={media.id} media={media} index={index} />
  ))}
</ReactSortable>

Media Metadata

// POST /media/information
interface SaveMediaInformationDto {
  mediaId: string;
  alt?: string;
  description?: string;
  tags?: string[];
}

const saveMediaInformation = async (data: SaveMediaInformationDto) => {
  await fetch('/media/information', {
    method: 'POST',
    body: JSON.stringify(data)
  });
};

AI-Generated Media

Generate Images

// POST /media/generate-image-with-prompt
const generateImage = async (prompt: string) => {
  const response = await fetch('/media/generate-image-with-prompt', {
    method: 'POST',
    body: JSON.stringify({ prompt })
  });
  
  const media = await response.json();
  return media; // Automatically saved to library
};

Generate Videos

// POST /media/generate-video
interface VideoDto {
  identifier: string;  // 'veo3' or 'image-text-slides'
  prompt?: string;
  images?: Array<{ path: string }>;
  duration?: number;
}

const generateVideo = async (config: VideoDto) => {
  const response = await fetch('/media/generate-video', {
    method: 'POST',
    body: JSON.stringify(config)
  });
  
  return await response.json();
};

Third-Party Media

Integrate media from third-party sources:
const ThirdPartyMedia = ({ onChange }) => {
  const sources = [
    { name: 'Unsplash', icon: 'unsplash' },
    { name: 'Pexels', icon: 'pexels' },
    { name: 'Giphy', icon: 'giphy' }
  ];
  
  const selectFromSource = (source: string) => {
    modal.openModal({
      title: `Select from ${source}`,
      children: <ThirdPartyPicker 
        source={source}
        onSelect={onChange}
      />
    });
  };
};

Video Handling

Video Previews

import { VideoFrame } from '@gitroom/react/helpers/video.frame';

const VideoPreview = ({ url }) => {
  return (
    <VideoFrame 
      url={url}
      autoplay={false}
    />
  );
};

Video Thumbnails

Generate and set custom video thumbnails:
const setVideoThumbnail = (media: Media, timestamp: number) => {
  // Extract frame at timestamp
  const thumbnail = extractVideoFrame(media.path, timestamp);
  
  // Update media settings
  onChange({
    ...media,
    thumbnail,
    thumbnailTimestamp: timestamp
  });
};

Media Optimization

  • Image Compression - Images are automatically compressed for web
  • Format Conversion - Convert to optimal formats (WebP for images)
  • Lazy Loading - Media loads only when visible
  • CDN Delivery - Fast delivery via Cloudflare CDN
const mediaDirectory = useMediaDirectory();

// Automatically adds CDN URL and optimizations
const optimizedUrl = mediaDirectory.set(media.path);

Best Practices

Use Descriptive Names

Name your media files descriptively before uploading for easy identification.

Add Alt Text

Always add alt text to images for accessibility and SEO.

Optimize Before Upload

Compress large images before uploading to save storage and bandwidth.

Regular Cleanup

Periodically delete unused media to keep your library organized.

Pagination Component

The media library includes a smart pagination system:
const Pagination = ({ current, totalPages, setPage }) => {
  const paginationItems = useMemo(() => {
    if (totalPages <= 10) {
      return Array.from({ length: totalPages }, (_, i) => i + 1);
    }
    
    // Smart pagination with dots for large sets
    const delta = 3;
    const range = [];
    
    for (let i = 1; i <= totalPages; i++) {
      if (i === 1 || i === totalPages || 
          (i >= current - delta && i < current + delta)) {
        range.push(i);
      }
    }
    
    // Add dots where there are gaps
    const withDots = [];
    let prev;
    for (const i of range) {
      if (prev !== undefined && i - prev > 1) {
        withDots.push('...');
      }
      withDots.push(i);
      prev = i;
    }
    
    return withDots;
  }, [current, totalPages]);
  
  return (
    <ul className="flex gap-1">
      <li>
        <button onClick={() => setPage(current - 1)}>
          Previous
        </button>
      </li>
      {paginationItems.map((item, index) => (
        <li key={index}>
          {item === '...' ? (
            <span>...</span>
          ) : (
            <button onClick={() => setPage(item - 1)}>
              {item}
            </button>
          )}
        </li>
      ))}
      <li>
        <button onClick={() => setPage(current + 1)}>
          Next
        </button>
      </li>
    </ul>
  );
};

Troubleshooting

  1. Check file size is under 1GB
  2. Verify file format is supported
  3. Check internet connection
  4. Try uploading smaller batches
  5. Clear browser cache

API Reference

EndpointMethodDescription
/mediaGETFetch paginated media
/media/upload-simplePOSTUpload single file
/media/upload-serverPOSTUpload with server processing
/media/:endpointPOSTMultipart upload endpoints
/media/:idDELETEDelete media file
/media/informationPOSTSave media metadata
/media/generate-imagePOSTGenerate AI image
/media/generate-videoPOSTGenerate AI video

Next Steps

Post Scheduling

Use your media library assets in scheduled posts

AI Features

Generate images and videos with AI

Build docs developers (and LLMs) love