Media library
Named storages — what they are, why they exist, every knob, and where to find the deeper docs.
The CMS's media library is built around named storages you declare in
.norcube/cms/config.yml. Each storage pins where bytes live on disk, what's
allowed in, and what shape the value takes when it lands in your page's
frontmatter. Image and file fields opt into a storage by name; the picker
and upload UI gate on the storage's config.
This page is the entry point. The deeper concepts each live on their own page:
Frontmatter shape
The publicUrl contract — what your build pipeline sees in frontmatter and why.
Naming + dedup
Four filename strategies (slug, slug+hash, uuid, original) plus dedup.
Inline modes
inline: ref vs inline: svg — when to inline SVG markup directly into frontmatter.
Why named storages
The pre-storages flow was "the editor's picker shows the whole repo and every upload goes to one hardcoded directory." Fine for the smallest sites; breaks the moment you have more than one kind of asset:
- An icon library the designer maintains in
src/assets/icons/*.svg— edited via PR, not the CMS. The editor needs a picker scoped to that directory, SVG-only, with no upload affordance. - User uploads going to
public/uploads/, collision-safe filenames, served from the deployed site as/uploads/.... - A public/favicon directory at the repo root that needs neither validation nor a special picker — just the legacy whole-repo flow.
Named storages let you express all three side-by-side, and let each field opt into the right one.
Minimal config
# .norcube/cms/config.yml
storages:
uploads:
type: repo
path: public/uploads
publicUrl: /uploads
accept: image/*, application/pdf
max_size: 10485760 # 10 MiB
defaultStorage: uploads# In a schema block
hero:
type: image
storage: uploadsA field with no storage: falls back to defaultStorage. Sites with no
storages: block at all keep the old whole-repo picker (so nothing breaks
on migration).
Storage knobs
Every storage takes the same set of common knobs plus driver-specific ones.
| Knob | Values | Effect |
|---|---|---|
type | repo | The only shipped driver. R2 / S3 / GCS are scaffolded but rejected at resolve time. |
path | repo-relative directory | Where the bytes live on disk. Leading / trailing slashes are trimmed. |
publicUrl | URL prefix (or empty) | Drives the frontmatter shape. See Frontmatter shape. |
mode | read-write (default), read-only | read-only hides the upload button AND rejects POSTs server-side. |
accept | MIME glob list (image/*, image/svg+xml) | Picker filter + server-side upload validation. |
max_size | bytes | Server-side pre-upload check; the picker hints the limit on the dropzone. |
naming | slug, slug+hash, uuid, empty (= original) | Server-side filename derivation. See Naming + dedup. |
inline | ref (default), svg | svg inlines the markup into frontmatter. See Inline modes. |
dedup | hash | Short-circuit identical uploads. Requires naming: slug+hash. See Naming + dedup. |
Read-only storages
mode: read-only hides the upload affordance in the picker AND rejects
the POST server-side. Use this for asset libraries maintained outside the
CMS — designer-owned icon sets, build-time-generated thumbnails, anything
where the authoritative writer is git itself.
storages:
icons:
type: repo
path: src/assets/icons
accept: image/svg+xml
mode: read-onlyThe server-side check is the trust boundary — hiding the button alone
would let a determined caller bypass via a direct POST /sites/:id/media.
Both gates run.
Upload reliability
Uploads go through a local mirror of your repo — the CMS commits the file and pushes to your branch via git itself, not the GitHub Contents API.
Why: GitHub's Contents API caps the request body around 4.8 MB, and uploading a binary requires base64-encoding (adds ~33% to the payload), so a 3.6 MB image is already at the cap. Above that the API returns 502s that surface as an unhelpful HTML "Unicorn!" page — not a useful error.
Plain git handles binary natively, no envelope, no size ceiling. This is transparent to you as a user — same upload button, same progress bar, files just don't fail past a megabyte.
Default storage
defaultStorage: controls which storage a field with no storage: key
inherits. When unset, the first declared storage is the implicit
default (YAML map order is preserved through the loader).
storages:
uploads:
type: repo
path: public/uploads
publicUrl: /uploads
icons:
type: repo
path: src/assets/icons
mode: read-only
inline: svg
defaultStorage: uploads # explicit; otherwise "uploads" because declared firstPick defaultStorage: explicitly when the order of your storages might
change for unrelated reasons — config-editor reorders, alphabetisation,
etc. The explicit value is stable; the implicit "first declared" tracks
authoring order.
What's not there yet
Storage features on the list but not shipped:
- R2 / S3 / GCS drivers. Only
type: repo(files in git) ships today. Cloud-object drivers are on the way but not wired. optimize.svgo/optimize.webp/optimize.resize— server-side optimisation pipeline for images on upload.cdn.rewrite_to— path substitution beyond whatpublicUrlalready covers.versioning: append-hash— cache-busting via query-param suffixes on stored URLs.