Skip to content
MagiCMS

Media

On disk

Images MagiCMS uploads live under src/assets/uploads/. The field stores a path that starts with a slash, as if it were a URL: /uploads/hero.webp. That string is what JSON and Git record. It is not a CDN URL unless you have set the project up that way.

Uploads are written as a flat list of files (a sanitised name, plus .webp, or .svg left as SVG). MagiCMS does not create a folder per category on disk. You can still put files in subfolders yourself (for example src/assets/uploads/products/quiet.webp → /uploads/products/quiet.webp). Those paths work in the picker; they are just not what the upload button creates.

Keep CMS images under uploads/. The site’s CmsImage helper globs that folder. Files elsewhere under src/assets/ can show in the media library but may not resolve at build time.

Typical project
src/
assets/
uploads/
.gitkeep
hero.webp
hero_cropped_16x9.webp
team-ada.webp
media.json // MagiCMS folder labels — not directories

Folders in MagiCMS

The Media view lets you create folders and assign images to them. That grouping is for the editor (filter, organise). It does not move files. The list of folder names, and which asset belongs where, is src/media.json.

An image with no assignment appears at the library root. Organise it later without changing the path stored on the page.

src/media.json
{
"version": 1,
"folders": ["team", "heroes"],
"assets": {
"/uploads/ada.webp": { "folder": "team" },
"/uploads/hero.webp": { "folder": "heroes" }
}
}

Upload

Drop or choose a file in Media (or from an image field). Raster images are converted to WebP and downscaled if they are very wide (about 2000px). The filename is sanitised (lowercase, hyphens). If hero.webp already exists, MagiCMS writes hero-1.webp, then hero-2.webp, and so on.

If you are in a MagiCMS folder when you upload, that assignment is written into media.json. The file still lands in src/assets/uploads/.

Image picker

An image field opens the same library. Filter by MagiCMS folder, pick a file, and the field stores the path. Cropped and scaled variants show a badge from the filename (_cropped_, or a size suffix). Details of the field type are on Schema field types.

Aspect (ratio and crop)

On an image field you can set ratio as two numbers with a colon, for example "16:9", "1:1" or "4:3". After the editor picks a file, MagiCMS opens a cropper that locks to that aspect. Confirm writes a new file next to the original. The original is kept. The field is updated to the cropped path.

The crop name is the source basename plus _cropped_ and the ratio with x instead of :, always as WebP: hero.webp with 16:9 becomes hero_cropped_16x9.webp.

Optional maxWidth and maxHeight resize the pixels. Together with ratio, that resize is applied to the crop before save (same *_cropped_* filename). Without a ratio, MagiCMS writes a sibling such as hero-1600w.webp or hero-800x600.webp.

Schema vs stored path
"cover": {
"type": "image",
"ratio": "16:9",
"maxWidth": 1600
}
// after crop + max width, the page JSON typically stores:
// "cover": "/uploads/hero_cropped_16x9.webp"

Point of interest

Crop on disk is one thing. Many blocks then scale the image in the frontend with CSS (object-cover inside a fixed frame such as aspect-4/3). The file can match the schema ratio and still be cropped again in the browser. Faces and other details can sit outside that live crop.

Turn on Point of interest on the image field (showPOI: true). It needs a ratio as well. After the editor has a file that matches that ratio, MagiCMS shows a preview in the display frame. Click to mark the point that should stay in view.

MagiCMS stores a sibling value, not a new file. For a field named cover the key is coverPoi: { "x": 0.7, "y": 0.5 }, each number from 0 to 1 on the source image (left/top → right/bottom). You do not add coverPoi as its own schema field.

In Astro, pass it the same way as the path. CmsImage maps it to object-position. That only matters if the image is drawn in a cropping box: a frame with a set size or aspect ratio, plus object-cover. If the <img> just grows with its natural height, nothing is clipped and the point of interest has no effect.

Two usual patterns:

  • Put the frame on the image: w-full aspect-4/3 object-cover on CmsImage (the image element is the box).
  • Put the frame on a wrapper: a parent with a set height or aspect, overflow-hidden (needed for rounded corners and for an absolutely positioned fill), and CmsImage filling it (h-full w-full object-cover, often absolute inset-0).

Without poi, the browser uses the default centre. Helper: src/lib/imagePoi.ts.

Schema and stored POI
"cover": {
"type": "image",
"ratio": "16:9",
"showPOI": true
}
// stored with the block or entry:
"cover": "/uploads/hero_cropped_16x9.webp",
"coverPoi": { "x": 0.75, "y": 0.5 }
Cropping box (on the image or a wrapper)
{/* Frame on the image (Hero-style) */}
<CmsImage
src={cover}
alt={coverAlt}
poi={coverPoi}
class="aspect-4/3 w-full object-cover"
/>
{/* Frame on a wrapper — overflow-hidden clips to the box */}
<div class="relative aspect-4/3 overflow-hidden rounded-box">
<CmsImage
src={cover}
alt={coverAlt}
poi={coverPoi}
class="absolute inset-0 h-full w-full object-cover"
/>
</div>

In Astro

Pass the stored path to CmsImage. It uses resolveImage() in src/lib/assets.ts, which maps /uploads/… to astro:assets metadata so the build can optimise. If the field has a point of interest, pass poi as well (see above). Do not put a raw <img src="/uploads/…"> unless you have a reason: in production those files are copied for the site, but you lose the image pipeline.

Vi bruker cookies for å forbedre din opplevelse.