Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Uploads

Upload collections handle file storage with automatic metadata tracking. Enable uploads by setting upload = true or providing a config table.

Configuration

crap.collections.define("media", {
    labels = { singular = "Media", plural = "Media" },
    upload = {
        mime_types = { "image/*" },
        max_file_size = "10MB",    -- accepts bytes or "10MB", "1GB", etc.
        image_sizes = {
            { name = "thumbnail", width = 300, height = 300, fit = "cover" },
            { name = "card", width = 640, height = 480, fit = "cover" },
        },
        admin_thumbnail = "thumbnail",
        format_options = {
            webp = { quality = 80 },
            avif = { quality = 60 },
        },
    },
    fields = {
        crap.fields.text({ name = "alt", admin = { description = "Alt text" } }),
    },
})

Upload Config Properties

PropertyTypeDefaultDescription
mime_typesstring[]{} (any)MIME type allowlist. Supports glob patterns ("image/*"). Empty = allow all.
max_file_sizeinteger/stringglobal defaultMax file size. Accepts bytes (integer) or human-readable ("10MB", "1GB"). Overrides [upload] max_file_size in crap.toml.
image_sizesImageSize[]{}Resize definitions for image uploads. See Image Processing.
admin_thumbnailstringnilName of an image_sizes entry to use as thumbnail in admin lists.
format_optionstable{}Auto-generate format variants. See Image Processing.

Auto-Injected Fields

When uploads are enabled, these fields are automatically injected before your custom fields:

FieldTypeHiddenDescription
filenametextNo (readonly)Sanitized filename with unique prefix
mime_typetextYesMIME type of the uploaded file
filesizenumberYesFile size in bytes
widthnumberYesImage width (images only)
heightnumberYesImage height (images only)
urltextYesURL path to the original file
focal_xnumberYesFocal point X coordinate (0.0–1.0, default center)
focal_ynumberYesFocal point Y coordinate (0.0–1.0, default center)

For each image size, additional fields are injected:

Field PatternTypeDescription
{size}_urltextURL to the resized image
{size}_widthnumberActual width after resize
{size}_heightnumberActual height after resize
{size}_webp_urltextURL to WebP variant (if enabled)
{size}_avif_urltextURL to AVIF variant (if enabled)

File Storage

Files are stored at <config_dir>/uploads/<collection_slug>/:

uploads/
└── media/
    ├── a1b2c3_my-photo.jpg          # original
    ├── a1b2c3_my-photo_thumbnail.jpg # resized
    ├── a1b2c3_my-photo_thumbnail.webp
    ├── a1b2c3_my-photo_thumbnail.avif
    ├── a1b2c3_my-photo_card.jpg
    ├── a1b2c3_my-photo_card.webp
    └── a1b2c3_my-photo_card.avif

Filenames are sanitized (lowercase, characters that are not alphanumeric, hyphens, or underscores are replaced with hyphens) and prefixed with a random 10-character nanoid.

URL Structure

Files are served at /uploads/<collection>/<filename>:

/uploads/media/a1b2c3_my-photo.jpg
/uploads/media/a1b2c3_my-photo_thumbnail.webp

API Response

The sizes field in API responses is a structured object assembled from the per-size columns:

{
    "url": "/uploads/media/a1b2c3_my-photo.jpg",
    "filename": "a1b2c3_my-photo.jpg",
    "sizes": {
        "thumbnail": {
            "url": "/uploads/media/a1b2c3_my-photo_thumbnail.jpg",
            "width": 300,
            "height": 300,
            "formats": {
                "webp": { "url": "/uploads/media/a1b2c3_my-photo_thumbnail.webp" },
                "avif": { "url": "/uploads/media/a1b2c3_my-photo_thumbnail.avif" }
            }
        }
    }
}

MIME Type Patterns

PatternMatches
"image/*"All image types (png, jpeg, gif, webp, etc.)
"application/pdf"Only PDF files
"*/*" or "*"Any file type

Empty mime_types array also accepts any file.

Error Cleanup

If an error occurs during upload processing (e.g., image resize fails partway through), all files written so far are automatically cleaned up. This prevents orphaned files from accumulating on disk.

Content Negotiation

When serving image files, the upload handler performs automatic content negotiation based on the browser’s Accept header. If a modern format variant exists on disk, it is served instead of the original:

  1. AVIF — served if the client sends Accept: image/avif and a .avif variant exists
  2. WebP — served if the client sends Accept: image/webp and a .webp variant exists
  3. Original — served if no matching variant exists

AVIF is preferred over WebP when both are accepted. The response includes a Vary: Accept header so caches store format-specific versions correctly.

This works for all image URLs (/uploads/...) including originals and resized variants. Non-image files (PDFs, etc.) are always served as-is.

Focal Point

Upload collections include focal_x and focal_y fields that store the subject/focus coordinates of an image as floats in the 0.0–1.0 range. Center is (0.5, 0.5).

Setting in Admin UI: On the upload collection edit page, click anywhere on the image preview to set the focal point. A crosshair marker shows the current position. The values are saved with the form.

Frontend usage: Use the coordinates with CSS object-position to keep the subject in frame when cropping at different aspect ratios:

.responsive-image {
  object-fit: cover;
  object-position: calc(var(--focal-x) * 100%) calc(var(--focal-y) * 100%);
}

Or inline from template data:

<img src="/uploads/media/photo.jpg"
     style="object-fit: cover; object-position: 50% 30%;" />

The values are available in API responses as focal_x and focal_y number fields.

File Deletion

When a document in an upload collection is deleted, all associated files (original + resized + format variants) are deleted from disk.