Media library

Media library

Named storages — what they are, why they exist, every knob, and where to find the deeper docs.

Not verified yet

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:

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: uploads

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

KnobValuesEffect
typerepoThe only shipped driver. R2 / S3 / GCS are scaffolded but rejected at resolve time.
pathrepo-relative directoryWhere the bytes live on disk. Leading / trailing slashes are trimmed.
publicUrlURL prefix (or empty)Drives the frontmatter shape. See Frontmatter shape.
moderead-write (default), read-onlyread-only hides the upload button AND rejects POSTs server-side.
acceptMIME glob list (image/*, image/svg+xml)Picker filter + server-side upload validation.
max_sizebytesServer-side pre-upload check; the picker hints the limit on the dropzone.
namingslug, slug+hash, uuid, empty (= original)Server-side filename derivation. See Naming + dedup.
inlineref (default), svgsvg inlines the markup into frontmatter. See Inline modes.
deduphashShort-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-only

The 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 first

Pick 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 what publicUrl already covers.
  • versioning: append-hash — cache-busting via query-param suffixes on stored URLs.

On this page