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 viewer | What the component shows |
|---|---|
available | The content, over its blurred placeholder until it loads |
age-restricted | The placeholder with a notice, and a reveal button when you supply onAgeConfirmRequest |
unavailable | An 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
| Prop | Default | What your app supplies |
|---|---|---|
viewerClaims.ageConfirmed | false | true after the viewer passes the age check required for this content and region. |
onAgeConfirmRequest | Not set | A 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
| Asset | What the component shows |
|---|---|
| Image | The image, with srcset widths from image.widths |
| website | The page's screenshot, with a link to the page |
| Video | A video player, with the preview as its poster |
| Audio | An 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 one | The document viewer, a page at a time |
| plaintext-document | The text, always shown as text |
| Spreadsheet | The spreadsheet viewer |
| Nothing displayable yet | The preview, or a file-type icon when there is none |
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 streamsPreviewWhen 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.lensandimage.transformapply a named lens or a direct transform to images, website screenshots and preview stills.image.widthsandimage.sizesadd asrcset, 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.
fitiscontain(the default) orcover, andaspectRatiokeeps the box its final size before the info arrives.lazy(on by default) loads images only when they near the viewport;fetchPrioritysets 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.
| Callback | Vue event | Element event | When |
|---|---|---|---|
onLoad | load | calmlens-load | The content shown loaded |
onError | error | calmlens-error | The content couldn't be shown, after any refresh of the asset URL |
onAvailabilityChange | availability-change | calmlens-availability-change | The viewer's availability became known or changed |
onAgeConfirmRequest | age-confirm-request | calmlens-age-confirm-request | The viewer pressed the reveal button |
onPlay, onEnded | play, ended | calmlens-play, calmlens-ended | Video or audio started or finished |
onTimeUpdate | timeupdate | calmlens-timeupdate | Video 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.