Now with AI-powered page building via MCP Server

Media

Images and files live in the workspace media library. How blocks reference them, why each upload gets its own URL, and what that means when you replace one.

Media is workspace-level, not page-level. One library, organised in folders, shared by every page and every block.

The media field

A block reaches an asset through fields.media:

export const imageProps = {
  src: fields.media({ label: "Image", required: true }),
  alt: fields.text({ label: "Alt text" }),
};

The stored value is a URL string, not an id and not an object. Your component receives it ready to use:

function ImageBlock({ content }) {
  if (!content.src) return null;
  return <img src={content.src} alt={content.alt ?? ""} />;
}

Because the value is a plain URL, a media field costs nothing at render time - there is no lookup, no resolution step, and no failure mode where the image is still loading.

Every upload gets its own URL

Uploaded assets are served from a CDN host, with a hash in the path:

https://assets.cmssy.io/{workspaceId}/78aa0167-cmssy-og-default.png

That hash is per upload. It is what makes assets immutable and safely cacheable forever - but it has a consequence people meet the hard way:

Uploading a replacement does not update the blocks pointing at the old file. A new upload is a new URL; existing blocks keep the old one and keep rendering the old image. If you swapped a logo and the site still shows the previous one, nothing is cached wrong - the blocks are simply still pointing where they always did.

There is no in-place replace: the media tools are list, upload and move, so a new file always means a new URL. The remedy is to re-point the blocks - a search-and-replace over block content, which is exactly what the MCP server is good at.

The practical consequence is worth planning for. An asset that many pages reference - a logo, a default OG image - is cheaper to swap if you route it through site config or a single block, rather than pasting it into twenty places.

Folders

Folders are a flat-ish tree with a parentId. They organise the library for humans; they are not part of the URL, so moving an asset between folders does not break anything pointing at it.

Through MCP you can list, create, rename, delete and move - which makes bulk reorganisation scriptable rather than an afternoon of dragging.

Next.js images

Assets come from a different origin than your app, so next/image will refuse them until the host is allowed:

// next.config.mjs
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "assets.cmssy.io" },
    ],
  },
};

A wildcard hostname: "**" works and is what the reference app uses, but it lets any HTTPS host through your image optimizer. Naming the asset host is the tighter choice, and costs one line.

Alt text is content

Pair every fields.media with a text field for alt text, and let editors fill it. Alt text describes what the image means in this context - the same photo needs different alt text on a case study and in a logo wall. It cannot be a property of the file, which is why it belongs on the block rather than the library.

Next steps