Assets
Media library
Section titled “Media library”The admin includes a media library at /admin/assets with:
- Grid view with thumbnails
- Folder organization (create, rename, delete, drag-to-move)
- Upload via button or drag-and-drop
- Alt text editing
- Focal point selector with live per-preset crop previews (click image to set crop center)
Image fields
Section titled “Image fields”Image fields render an upload button and a browse dialog that connects to the media library. The stored value is the file URL (/uploads/<id>.<ext>, where <id> is a nanoid and <ext> comes from the verified MIME type). The original filename is not part of the path.
Storage
Section titled “Storage”Asset metadata (filename, mime type, size, intrinsic width/height, alt, focal point) is stored in the cms_assets table. Width and height are captured from the file on upload (via Sharp) for raster images, so consumers can reserve layout space and avoid cumulative layout shift (CLS). File storage depends on the deployment target:
- Node.js:
public/uploads/(orCMS_UPLOADS_DIR) - Cloudflare: R2, through the
CMS_ASSETSbinding
To store files elsewhere, replace the storage adapter; see Adapter contracts.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/cms/assets/upload |
Upload file (multipart/form-data) |
GET |
/api/cms/assets |
List assets |
GET |
/api/cms/assets/:id |
Get single asset |
PATCH |
/api/cms/assets/:id |
Update metadata |
DELETE |
/api/cms/assets/:id |
Delete asset and file |
List parameters
Section titled “List parameters”| Param | Type | Description |
|---|---|---|
limit |
number |
Max results (default 50) |
offset |
number |
Skip N results |
folder |
string |
Filter by folder ID (empty string for root) |
q |
string |
Search by filename or alt text |
Passing ?url=/uploads/<id>.<ext> instead returns the single asset matching that URL (404 if none).
Update fields
Section titled “Update fields”| Field | Type | Description |
|---|---|---|
alt |
string |
Alt text |
filename |
string |
Display filename |
folder |
string | null |
Move to folder (null for root) |
focalX |
number | null |
Focal point X (0–100) |
focalY |
number | null |
Focal point Y (0–100) |
Programmatic API
Section titled “Programmatic API”For scripts and server code (importers, seeds), use assets.upload directly:
import { assets } from "@kidecms/core";
const asset = await assets.upload(new File([bytes], "photo.jpg", { type: "image/jpeg" }), { alt: "A photo", dedupe: true,});asset.storagePath; // "/uploads/<id>.jpg" — store this in image fieldsWith dedupe: true the upload is content-hashed: re-running the same upload returns the existing asset instead of writing a new file, so importers stay idempotent. See Migrations for the full import workflow.
Focal points
Section titled “Focal points”Set a focal point (focalX/focalY, 0–100%) on any image in the asset detail view by clicking the spot that must stay in frame. The detail view shows live crop previews for each configured aspect ratio, so you can see how the image will be framed as a hero, card, square, etc., all from a single upload with no external editing.
The focal point is applied server-side whenever an image is cropped to an aspect ratio: the transform picks the largest source window matching the target ratio, positions it on the focal point (clamped so it never runs off-frame), then resizes. Media library thumbnails also reflect it.
If no focal point is set, crops use content-aware attention framing rather than a centered crop: Sharp’s strategy.attention locally, and gravity=auto on Cloudflare, so the crop keeps the most salient region in frame.
Image optimization
Section titled “Image optimization”Images are resized and converted on demand by the endpoint /api/cms/img/[...path]: Sharp on Node, the Images binding on Cloudflare. Images inside rich text and content are served through it automatically with a responsive srcset, and take their alt text from the asset. For image fields, use <CmsImage>.
The endpoint
Section titled “The endpoint”/api/cms/img/uploads/photo.jpg?w=800 → 800px wide WebP (no crop)/api/cms/img/uploads/photo.jpg?w=1024&f=avif → 1024px wide AVIF/api/cms/img/uploads/photo.jpg?w=1280&h=549 → cropped to 1280×549 (attention framing)/api/cms/img/uploads/photo.jpg?w=1280&h=549&fx=50&fy=30 → cropped, framed on focal point/api/cms/img/uploads/photo.jpg?q=90 → quality 90| Param | Type | Default | Description |
|---|---|---|---|
w |
number |
— | Width (snapped to nearest allowed size) |
h |
number |
— | Height. When set with w, the image is cover-cropped to w×h |
fx |
number |
— | Focal point X (0–100), used when cropping |
fy |
number |
— | Focal point Y (0–100), used when cropping |
f |
string |
webp |
Format: webp, avif, jpeg, png |
q |
number |
80 |
Quality (1–100) |
With only w, the image keeps its aspect ratio. With w and h it is cover-cropped, framed on fx/fy when given (a missing axis defaults to 50) and on the most salient region otherwise. On Node, results are cached in .cms-cache/img/; every response has immutable cache headers.
On Cloudflare the Worker serves the same URLs through the Images binding and stores each rendition in the edge cache. Nothing to enable: it works in astro dev, on *.workers.dev, and on custom domains, and bills at Images pricing.
If you have enabled Image Transformations for your zone and would rather have renditions served entirely by Cloudflare’s edge (never invoking the Worker), opt in:
images: { cloudflare: "cdn-cgi" },Deployed pages then emit /cdn-cgi/image/… URLs. This needs a custom domain; astro dev keeps using the Worker route.
Presets
Section titled “Presets”Presets are named renditions. Built-in presets: hero (21:9), heroMobile (4:5), banner (16:9), card (16:9), square (1:1), thumb (1:1), and content (no crop). Override or add to them in cms.config.ts:
export default defineConfig({ images: { presets: { // merged over the built-in defaults banner: { aspect: "16/9", widths: [640, 960, 1280, 1920], formats: ["avif", "webp"], sizes: "100vw" }, product: { aspect: "4/3", widths: [320, 640, 960], formats: ["avif", "webp"], sizes: "50vw" }, }, }, collections: [ /* ... */ ],});The images block is optional — omit it and the defaults still apply. A preset with no aspect resizes without cropping (preserving the source ratio).
<CmsImage>
Section titled “<CmsImage>”<CmsImage> reads the asset record, so it applies the stored focal point, sets width/height (from the preset aspect or the asset’s dimensions) to prevent layout shift, emits AVIF and WebP sources, and renders nothing if the upload has been deleted.
---import CmsImage from "@/components/CmsImage.astro";---
<!-- The usual case --><CmsImage src={doc.image} alt={doc.title} preset="card" />
<!-- Art direction: 16:9 on desktop, recomposed to a 4:5 crop on mobile --><CmsImage src={doc.image} alt={doc.title} preset="banner" art={[{ media: "(max-width: 640px)", preset: "heroMobile" }]} loading="eager" fetchpriority="high"/>| Prop | Type | Description |
|---|---|---|
src |
string |
Asset URL (/uploads/…) or any external/static src |
alt |
string |
Alt text |
preset |
string |
Base rendition for the <img> and default sources (default banner) |
art |
{ media; preset }[] |
Art-directed sources — a different crop per breakpoint |
class |
string |
Class applied to the <img> |
focalX / focalY |
number |
Override the asset’s stored focal point |
sizes |
string |
Override the base preset’s sizes |
loading |
"eager" | "lazy" |
Native lazy-loading (default lazy) |
fetchpriority |
"high" | "auto" | "low" |
Set high for the LCP hero |
The art prop is optional — omit it and you get a single, focal-aware, multi-format rendition, which is what most images want.
cmsImageUrl and cmsSrcset
Section titled “cmsImageUrl and cmsSrcset”For an og:image tag, a CSS background-image, JSON-LD, or an email template, these helpers return the same URLs as strings. They don’t read the asset record: no stored focal point, no dimensions, and no check that the upload still exists. Pass crop ({ aspect, focalX, focalY }) for a cropped rendition.
import { cmsImageUrl, cmsSrcset } from "@kidecms/core";
// Single optimized URL (resize only)cmsImageUrl("/uploads/photo.jpg", 800);// → /api/cms/img/uploads/photo.jpg?w=800
// Responsive srcset (resize only)cmsSrcset("/uploads/photo.jpg", [480, 768, 1024]);// → /api/cms/img/uploads/photo.jpg?w=480 480w, ...
// Cropped to a 21:9 hero, framed on a focal point you supplycmsImageUrl("/uploads/photo.jpg", 1280, "webp", { aspect: "21/9", focalX: 50, focalY: 30 });// → /api/cms/img/uploads/photo.jpg?w=1280&h=549&fx=50&fy=30The height is derived from the (snapped) width and the aspect ratio, so the whole srcset stays at a consistent ratio. aspect accepts "21/9", "21:9", or "21x9".
Allowed widths
Section titled “Allowed widths”Requested widths are snapped to the nearest allowed size to maximize cache efficiency: 320, 480, 640, 768, 960, 1024, 1280, 1536, 1920.