Skip to content

Assets

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 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.

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:

  • Local/Node.js: Files stored in public/uploads/, served by Astro’s static file handling
  • Cloudflare: Files stored in R2 via the CMS_ASSETS bucket binding, served by a dynamic route

The CMS uses a storage adapter (src/cms/adapters/storage.ts) with putFile, getFile, deleteFile functions; the CmsStorageAdapter interface and getStorage() live in src/cms/core/runtime.ts. The setup script configures the correct implementation for your deployment target.

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
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).

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)

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 fields

With 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.

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. Image-field thumbnails in the admin also reflect it via object-position.

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.

Uploaded images are automatically optimized when rendered on public pages. The CMS includes an on-demand image transformation endpoint powered by Sharp.

Images in rich text and block content are automatically served as optimized WebP with responsive srcset. No configuration is needed for local uploads.

The transformation endpoint is at /api/cms/img/[...path]:

/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 is resized preserving its aspect ratio. Supplying both w and h triggers a cover crop. Omitting both fx and fy uses attention-based framing (not a centered crop); the 50 fallback only kicks in when a focal point is partially specified — i.e. exactly one of fx/fy is set, and the missing axis defaults to 50. Transformed images are cached to .cms-cache/img/ (the cache key includes crop + focal) and served with immutable cache headers.

When deployed to Cloudflare, the same helpers emit /cdn-cgi/image/ URLs (with fit=cover and gravity) so cropping runs on Cloudflare’s image resizing instead of Sharp.

Use cmsImage and cmsSrcset in your own templates. Both accept an optional crop argument ({ aspect, focalX, focalY }) to request a cropped rendition:

import { cmsImage, cmsSrcset } from "@kidecms/core";
// Single optimized URL (resize only)
cmsImage("/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
cmsImage("/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=30

The 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".

Named renditions give editors and developers a shared vocabulary and keep the transform cache bounded to a known set of sizes. Built-in presets ship by default: 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).

<CmsPicture> renders a <picture> with cropped, multi-format sources and resolves the asset’s focal point automatically. It also sets width/height on the fallback <img> (from the preset aspect, or the asset’s intrinsic dimensions) to prevent layout shift.

---
import CmsPicture from "@/components/CmsPicture.astro";
---
<!-- Hero: 16:9 on desktop, recomposed to a 4:5 crop on mobile -->
<CmsPicture
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

Use plain cmsImage/cmsSrcset (or CSS object-fit: cover with focal object-position) for cards, thumbnails, and inline images; reach for <CmsPicture> when you need true art direction — a different composition per breakpoint, which CSS alone cannot do.

Requested widths are snapped to the nearest allowed size to maximize cache efficiency: 320, 480, 640, 768, 960, 1024, 1280, 1536, 1920.