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:
- Local/Node.js: Files stored in
public/uploads/, served by Astro’s static file handling - Cloudflare: Files stored in R2 via the
CMS_ASSETSbucket 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 |
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. 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.
Image optimization
Section titled “Image optimization”Uploaded images are automatically optimized when rendered on public pages. The CMS includes an on-demand image transformation endpoint powered by Sharp.
How it works
Section titled “How it works”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.
Helpers for custom templates
Section titled “Helpers for custom templates”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 pointcmsImage("/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".
Presets
Section titled “Presets”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> — art-directed images
Section titled “<CmsPicture> — art-directed images”<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.
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.