How to work with video in BCMS

CleanShot 2025-07-03 at 17.52.21@2x.jpg
By Momčilo Popov
Read time 1 min
Posted on 3 Jul 2025

Almost every website needs video. Maybe it’s a product demo. Maybe it’s an interview or a loop in the background. Video helps explain things better. So you need a way to manage and show it properly.

Here’s how to do that in BCMS.

Create a video widget

In BCMS, video is just content block. You create a widget for it like any other block.

A basic video widget should have:

  • A media field (file) for uploaded videos

  • A boolean (autoplay) to control playback

  • A string field (youtube_url) if you want to support YouTube embeds too

CleanShot 2025-07-03 at 18.11.15@2x.jpg

The generated TypeScript type will look like this:

export interface VideoWidget {
  file?: PropMediaDataParsed;
  autoplay?: boolean;
  youtube_url?: string;
}

Write a component to render it

Once you have the widget, make a React (Next.js) component to show the video.

This one checks if a video was uploaded. If not, it checks if there’s a YouTube URL.

'use client';
import React, { FC, useEffect, useState } from 'react';
import type { VideoWidget as VideoWidgetType } from '@bcms-types/ts';
import BCMSMedia from '../bcms-media';
import { bcmsMediaClient } from '@root/media-client';
interface Props {
  data: VideoWidgetType;
}
const VideoWidget: FC<Props> = ({ data }) => {
  const [youtubeVideoId, setYoutubeVideoId] = useState('');
  useEffect(() => {
    if (!data.youtube_url) return;
    const regExp =
      /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
    const match = data.youtube_url.match(regExp);
    setYoutubeVideoId(match && match[7].length === 11 ? match[7] : '');
  }, [data.youtube_url]);
  return (
    <div>
      {data.file ? (
        <BCMSMedia
          video={{
            src:
              bcmsMediaClient.cmsOrigin +
              bcmsMediaClient.media.toUri(data.file._id, data.file.name),
            autoplay: data.autoplay,
          }}
        />
      ) : youtubeVideoId ? (
        <iframe
          src={`https://www.youtube.com/embed/${youtubeVideoId}`}
          allow="autoplay; encrypted-media"
          allowFullScreen
          className="aspect-video w-full"
          title="YouTube video"
        />
      ) : null}
    </div>
  );
};
export default VideoWidget;

What ss bcmsMediaClient and why it's separate

In most of my BCMS setups, you’ll see two client instances:

  • client.ts - used for authenticated actions like fetching content

  • media-client.ts - used only for accessing public media

I keep bcmsMediaClient separate because it's safe to expose in the browser. It only includes public keys and is used for loading images and videos from the BCMS CDN. Nothing sensitive is in there. But still, no need to use it with the content API key.

Here’s what it looks like:

// media-client.ts

import { Client } from '@thebcms/client';
export const bcmsMediaClient = new Client(
  process.env.NEXT_PUBLIC_BCMS_ORG_ID!,
  process.env.NEXT_PUBLIC_BCMS_INSTANCE_ID!,
  {
    id: process.env.NEXT_PUBLIC_BCMS_MEDIA_API_KEY_ID!,
    secret: process.env.NEXT_PUBLIC_BCMS_MEDIA_API_KEY_SECRET!,
  },
  {
    injectSvg: true,
  },
);

You use it to:

  • Generate media URLs: bcmsMediaClient.media.toUri(...)

  • Pass config to < BCMSImage /> or < BCMSContentManager />

  • Avoid exposing full API access in the browser

If you only use one client for everything, you might leak sensitive keys in the frontend. That’s why I always create bcmsMediaClient separately.

Reuse the rendering logic

To keep things clean, you can use a small helper component like BCMSMedia to render both images and videos. Here’s the version for video only:

'use client';
import React, { FC } from 'react';
interface Props {
  video?: {
    src: string;
    autoplay?: boolean;
  };
  className?: string;
}
const BCMSMedia: FC<Props> = ({ video, className = '' }) => {
  if (!video) return null;
  return (
    <video
      controls={!video.autoplay}
      autoPlay={video.autoplay}
      playsInline={video.autoplay}
      muted={video.autoplay}
      loop={video.autoplay}
      className={`w-full object-contain ${className}`}
    >
      <source src={video.src} type="video/mp4" />
      Your browser does not support the video element.
    </video>
  );
};
export default BCMSMedia;

How BCMS stores videos

When you upload a video in the dashboard, BCMS stores it on our CDN. You get a public URL. That’s what you use in the < video > tag.

You don’t need to worry about hosting or storage.

But BCMS doesn’t compress or resize videos. It gives you the original file. So make sure your videos are optimized before uploading.

Right now, uploading is only possible through the dashboard. API upload is coming soon, but only in the Pro version.

Using.... Cloudinary instead?

If you need more control - like resizing, transcoding, adaptive streaming - you can use Cloudinary.

Store the Cloudinary URL in your widget. Then render it however you want. BCMS doesn’t care where the video is hosted. You control that part.

Render it with BCMSContentManager

If you're using BCMSContentManager to render dynamic content, just pass the VideoWidget like this:

import { BCMSContentManager } from '@thebcms/components-react';
import VideoWidget from '@/components/widgets/video';
export default function BlogContent({ content }) {
  return (
    <BCMSContentManager
      items={content}
      clientConfig={bcmsMediaClient.getConfig()}
      widgetComponents={{
        video: VideoWidget,
      }}    
    />
  );
}

Summary

  • You can upload .mp4 files to BCMS and display them on your site

  • You can also embed YouTube videos

  • Videos are served from the CDN in their original format

  • No file upload API yet, but it’s on the roadmap

  • You’re free to use Cloudinary or any other service

  • BCMS just gives you the data - you decide how to render it

If you want more control, build your widget and component logic around your needs. That’s the point of BCMS - it stays out of your way.

....

Join our Newsletter

Get all the latest BCMS updates, news and events.

You’re in!

The first mail will be in your inbox next Monday!
Until then, let’s connect on Discord as well:

Join BCMS community on Discord

By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.

Gradient