Asset component

CalmLensAsset shows any asset from its asset URL, public or signed, natively in your page, in the way its moderation decision allows for the viewer: the content when it is available, a blurred placeholder and a reveal button when it is age-restricted, and an unavailable notice otherwise. Widgets covers installing it, styling and translating.

Show an asset

Pass the asset URL as src: client.cdn.getAssetUrl({ assetId }) for a public asset, or one your server signs with client.cdn.signAssetUrl({ assetId }) for a private one. Set viewerClaims.ageConfirmed only after your app confirms the viewer passed the required age check:

import { CalmLensAsset } from "calmlens/react";

type PostPhotoProps = {
  assetId: string;
  /** The asset URL your server sent with the post: public, or signed for a private asset. */
  src: string;
  alt: string;
  /** Width divided by height, which you store when the image is uploaded. */
  aspectRatio: number;
  ageConfirmed: boolean;
  onReveal: () => void;
};

// Your server signs a private asset's URL again with cdn.signAssetUrl once it expires.
async function refreshUrl(assetId: string): Promise<string> {
  const response = await fetch(`/api/photos/${assetId}/url`);
  if (!response.ok) throw new Error(`Couldn't refresh the photo (${response.status})`);
  return (await response.json()).url;
}

export function PostPhoto({
  assetId,
  src,
  alt,
  aspectRatio,
  ageConfirmed,
  onReveal,
}: PostPhotoProps) {
  return (
    <CalmLensAsset
      src={src}
      onExpired={() => refreshUrl(assetId)}
      viewerClaims={{ ageConfirmed }}
      onAgeConfirmRequest={onReveal}
      alt={alt}
      aspectRatio={aspectRatio}
      image={{ widths: [400, 800], sizes: "(min-width: 768px) 50vw, 100vw" }}
    />
  );
}

The component reads the asset's display info from {src}/info once it is mounted, then shows what the info describes. Nothing is requested until it has a src.

availability for the viewerWhat the component shows
availableThe content, over its blurred placeholder until it loads
age-restrictedThe placeholder with a notice, and a reveal button when you supply onAgeConfirmRequest
unavailableAn unavailable notice, without requesting the content

The CDN has the last word: a resource it refuses switches the component to the notice that fits, whatever the info said. Replace the notices with renderUnavailable and renderAgeGate (in Vue, the unavailable and age-gate slots), which receive the placeholder URL and a confirm callback for your own reveal button.

Render on the server

Pass the display info as info, read on your server with client.cdn.getAssetInfo(assetUrl) (getAssetInfo(url) from calmlens/app in JavaScript), and the first render is the real element, including during server rendering. Images, websites and the age and unavailable notices then work before your scripts load; video shows its file with its poster and documents their first page's still with a link that opens them, until the page hydrates.

Without info, the component renders a placeholder box until the info arrives. Set aspectRatio (width divided by height) to give it its final size.

Age confirmation

PropDefaultWhat your app supplies
viewerClaims.ageConfirmedfalsetrue after the viewer passes the age check required for this content and region.
onAgeConfirmRequestNot setA callback that starts your age-check flow when the viewer presses View. Without it, the notice has no reveal button.

Pressing View calls onAgeConfirmRequest; it does not confirm age or reveal the content itself. Keep viewerClaims.ageConfirmed false while the check is pending, declined, expired or identifies a minor. Once it passes, render the component with viewerClaims.ageConfirmed set to true. Reset it when the viewer or their eligibility changes.

The component adds the claims to every request it makes, including the info and responsive variants; placeholder URLs carry only region. Claims you pass replace any the src carries, and default claims are left out. Any claim makes the CDN's response private and uncacheable, so set them only when your app needs them. In plain HTML, set viewer-claims to a JSON object; Vue uses :viewer-claims="{ region, ageConfirmed }". CalmLens accepts the assertions without verifying them; they never override a block or grant private access.

A public asset URL works for anyone who has it, with any claims. When your backend must approve each viewer, make the asset private and give the component a short-lived signed asset URL issued after your own check. See Age-restricted delivery and Native age checks.

Viewer region

Pass the viewer's country as viewerClaims.region, since regional rules apply to every project. Without it, the CDN uses the country of the connection that requests the content. Viewer region covers where the country should come from, proxies and signed URLs.

import { CalmLensAsset } from "calmlens/react";

// Claims are optional customer assertions evaluated by the CDN.
// ageConfirmed stays false until your app completes the required age check.
export function RegionalPhoto({
  src,
  region,
  ageConfirmed,
}: {
  /** The asset URL: public, or signed by your server for a private asset. */
  src: string;
  region: string;
  ageConfirmed: boolean;
}) {
  return <CalmLensAsset src={src} viewerClaims={{ region, ageConfirmed }} alt="Uploaded photo" />;
}

The info's availability is decided for the same claims, so an asset blocked in the viewer's region shows the unavailable notice without requesting the content. When you read info on your server, read it for the viewer's claims too.

What it shows

AssetWhat the component shows
ImageThe image, with srcset widths from image.widths
websiteThe page's screenshot, with a link to the page
VideoA video player, with the preview as its poster
AudioAn audio player under the waveform, drawn from content.rendered.peaks with its playback progress, or the waveform image
PDF, or a document or presentation rendered as oneThe document viewer, a page at a time
plaintext-documentThe text, always shown as text
SpreadsheetThe spreadsheet viewer
Nothing displayable yetThe preview, or a file-type icon when there is none
Documents, text and spreadsheetsPreview

Documents, text and spreadsheets are shown by the asset viewers, whose options the component takes in groups: document.initialPage and document.pdf, and spreadsheet.pageSize and spreadsheet.getText. A spreadsheet's sheets are in its text child asset, not beneath its own asset URL, so pass spreadsheet.getText to read them; without it the viewer shows its failure notice.

Set display="preview" to show only the asset's still, for grids and lists: the preview, or the image itself, or a file-type icon.

Video and audio play the browser-safe file CalmLens makes once the asset is decided. video sets autoPlay, muted, loop and controls (on by default).

Adaptive streams

Adaptive streamsPreview

When the asset has an adaptive stream (content.hls), video and audio play it instead of the file, so playback adapts to the viewer's connection. Browsers that play HLS themselves play it natively; elsewhere the component loads a small HLS player the first time it plays a stream, so pages without streams don't download it. Browsers that can play neither, and a stream that fails to play, fall back to the browser-safe file. Set video.stream to false (the no-stream attribute on calmlens-asset) to always play the file.

Private assets

Pass a signed asset URL as src. Every resource the component loads, such as the rendered file, a video's poster or its stream, is a path beneath it, so one URL covers them all. Pass onExpired, which asks your server for a fresh signed asset URL once the one in use expires; the content loads again beneath it, and video and audio resume where they stopped. Without onExpired, an expired URL shows the unavailable notice. The example under Show an asset refreshes its URL this way.

The calmlens-asset element takes onExpired as a property.

Options

  • image.lens and image.transform apply a named lens or a direct transform to images, website screenshots and preview stills. image.widths and image.sizes add a srcset, so browsers fetch the smallest image that fits. Each width is a separate image transform; see Lenses and transforms.
  • Images come from the asset's rendered resource, so the CDN serves the best format the browser accepts without a list of formats.
  • fit is contain (the default) or cover, and aspectRatio keeps the box its final size before the info arrives.
  • lazy (on by default) loads images only when they near the viewport; fetchPriority sets their priority.
  • The placeholder is drawn in the browser from the info's content.preview.thumbhash, with no request; without one, the component loads the asset's public placeholder from the CDN.
CallbackVue eventElement eventWhen
onLoadloadcalmlens-loadThe content shown loaded
onErrorerrorcalmlens-errorThe content couldn't be shown, after any refresh of the asset URL
onAvailabilityChangeavailability-changecalmlens-availability-changeThe viewer's availability became known or changed
onAgeConfirmRequestage-confirm-requestcalmlens-age-confirm-requestThe viewer pressed the reveal button
onPlay, onEndedplay, endedcalmlens-play, calmlens-endedVideo or audio started or finished
onTimeUpdatetimeupdatecalmlens-timeupdateVideo or audio played on, with its position in seconds

The calmlens-asset element takes src, display, viewer-claims, alt, fit, aspect-ratio, eager, fetch-priority, lens, transform, widths (comma-separated), sizes, no-controls, autoplay, muted, loop, no-stream, initial-page and page-size as attributes, and info, onExpired, messages, pdf, getText, renderUnavailable and renderAgeGate as properties.

React Native

CalmLensAsset from calmlens/react-native shows assets the same way with React Native's own views. React Native has no srcset, so the component measures itself and loads the smallest of image.widths that covers its width in device pixels. Give it a size with aspectRatio or a style; otherwise it keeps a minimum height while the image loads.

React Native has no built-in video or audio player. Pass renderPlayer to play them, for example with expo-video; it receives what to play as src, with streaming: true when it is the adaptive stream (an HLS playlist) and the rendered file otherwise, the poster and audio's peaks. Call its onError when playback fails, with where it was: a stream gives way to the file, and a file is retried beneath a fresh URL from onExpired, with resumeFrom saying where to resume. Without renderPlayer, video and audio show their preview with a button that opens the file in the device's player. Documents, text, spreadsheets and websites show their preview still with a button that opens them:

import { CalmLensAsset, type MediaPlayerSource } from "calmlens/react-native";
import { useVideoPlayer, VideoView } from "expo-video";
import { StyleSheet } from "react-native";

function Player({ src, streaming }: MediaPlayerSource) {
  const player = useVideoPlayer({ uri: src, contentType: streaming ? "hls" : "auto" });
  return <VideoView player={player} style={StyleSheet.absoluteFill} nativeControls />;
}

export function PostMedia({ src, alt }: { src: string; alt: string }) {
  return (
    <CalmLensAsset
      src={src}
      image={{ widths: [320, 640, 1080] }}
      alt={alt}
      renderPlayer={(source) => <Player {...source} />}
    />
  );
}

lazy, fetchPriority, image.sizes, document and spreadsheet are web-only and not available in React Native.

Last updated

Signing in

Signing out

Cookies on CalmLens

We use essential cookies for sign-in and security. With your permission, functional cookies remember your preferences and Google Analytics measures website use and signups. Optional ad measurement cookies link Google ad clicks to signups. Personalised advertising stays off. Manage cookies