Images and media internals
Uploads live in the media library (Media model, folders, tags, usage index); images are rendered into variants on request or by a queue worker. The user side is on Media library and Image Rendering.
Storage
Media files are stored on the uploads disk the core provider defines (storage/app/evolve/uploads). Since 2.12 storage/app of every release is a symlink to shared/storage/app next to releases/ (Deployment\SharedStorage), so a deploy keeps uploads and evolve:env:setup and the Envoy media tasks move the one directory. Rendered variants are written under public/img and served statically by the web server on the next hit; /index.php/img/... answers 301 to /img/... (2.12).
Visibility and the private variant cache 2.22
Contracts\MediaVisibility (default Services\Media\OpenMediaVisibility, bound with singletonIf) is asked by MediaController and ImageController before anything is delivered; a false answers 404 like a missing hash. isPubliclyCacheable() decides the cache disk of a variant: ImageVariant::cacheDisk() returns public or image-cache-private (storage/app/evolve/image-cache), ImageRenderer::setCacheDisk() writes there, and Services\Media\MediaCacheControl turns the answer into public, max-age=N or private, max-age=600. ImageRenderer::clearCache() and evolve:clear-images clear both disks. Events\MediaFileStored fires after the file is on the uploads disk (upload, picker, replace). The module side of this is on Media extension points; without a module every variant renders into public/img with the headers it always had (MediaDeliveryWithoutModuleTest).
Upload rules
evolve.media-manager.max-file-size and allowed-extensions in core/config/evolve.php, enforced in Http\Livewire\MediaManager. The allowlist excludes active content (svg, html, xml, js, php) because files are served from the media URL and would execute in the browser (2.4).
Metadata and recognition
Jobs\UpdateImageMetadata reads dimensions and EXIF after upload and, with image.enable-image-recognition (IMAGE_RECOGNITION), asks Services\CloudVision (Google Cloud Vision, credentials from GOOGLE_APPLICATION_CREDENTIALS) for labels, logos, text and objects, which become suggested tags. evolve:update-image-metadata runs the job for files missing metadata. Extensions are stored lowercased (2.8).
Rendering pipeline
| Class | Role |
|---|---|
Services\ImageRenderer | takes a source path and Media, setSize(), setFilters(), setFormat(), useCache(), setCachePath(), render() and toResponse(); uses the image.driver (gd or imagick) and image.compression per format |
Services\ImageConversions | the list in image.conversions: every size the theme's <x-image> requests, declared once so the queue knows what to pre-render |
Services\ImageVariant | one rendered size, format and filter set of one image |
ImageFilters\* | the filters a template may request |
Jobs\GenerateImageConversions, Jobs\GenerateImageVariant | queued rendering; a cold variant is answered with the original and rendered by the worker (2.12) |
The theme's <x-image> component asks for a variant by size and crop (focal point from the media record). With QUEUE_CONNECTION=sync the first request renders inline; with a worker, evolve:images:warm --missing after a deploy pre-renders what is missing and the queue health check reports a stalled worker. evolve:clear-images deletes every variant; image.allow-ids (IMAGE_ALLOW_IDS) lets a template request an image by ID. WebP derivatives of ICC-tagged images honour the configured quality since 2.12.1.
File URLs
core/routes/routes.php serves files under media/{media}/download, media/{media}/preview/{slug?} and media/{media}/{slug?} (media.get), registered in that order. The slug of media.get is constrained to anything except download and preview, so neither the order nor a route cache lets it shadow the other two. MediaController::get() ignores the slug and resolves by hash. Services\Media\FilePathOutputter (what Media::path() uses for every type except images) emits /media/{hash}/{slug}.{ext} with Str::slug() of the name, falling back to the hash when the slug is empty, and ignores image options. Media::filePath() calls it directly, which is how the downloads block links images to their original file. The URL patterns of MediaUsageIndexer and MediaReferenceAnnotator only read the hash, so they match the old and the new shape (2.19).
Usage index
Services\Media\MediaUsageIndexer writes MediaUsage rows for every blueprint field and inline link in block content that references a media ID; evolve:index-media-usages rebuilds it. The panel shows the count on the card and the list in the file drawer, and evolve:prune-media-attachments removes attachments whose owner model or class no longer exists.
Favicons
FaviconController generates the favicon set through RealFaviconGenerator (services.realfavicongenerator.api-key, a shared public key) into public/favicons, hardened against SSRF and arbitrary writes (2.4). head.blade.php includes public/favicons/markup.html when it exists.