Images and media
Components and hooks for showing Image properties stored on the device, and for resolving raw media strings from feeds into images, video, audio, file links, HTML and JSON.
Which component or hook to use depends on what you're holding:
| You have | Use |
|---|---|
An Image item property from the local database | SeedImage, or useEnsureLocalImage for your own <img> |
| A raw string from a feed or XML (a URL, an Arweave transaction ID, a seed UID) | SeedMediaImage, SeedMediaVideo, SeedMediaAudio, SeedMediaFile, or useResolvedMediaRef |
| HTML or JSON to display | SeedHtml, SeedJson |
import {
SeedImage,
useEnsureLocalImage,
useResolvedMediaRef,
SeedMediaImage,
SeedMediaVideo,
SeedMediaAudio,
SeedMediaFile,
SeedHtml,
attachArweaveLoadFailover,
SeedJson,
formatSeedJson,
useNormalizedFeedItemFields,
} from '@seedprotocol/react'Image files live in the browser's private file system (OPFS), under the SDK's images/ folder, with resized copies in width folders (images/480/, images/760/, images/1024/, images/1440/ and images/1920/). Saving an image writes all five copies; an image downloaded from Arweave gets one when it's first shown at a width. For uploading images and how they're stored, see Working with file and image properties.
SeedImage
Renders an Image item property as an <img>. It picks the stored size closest to width, and while the file isn't on the device yet, it downloads it from Arweave and shows a gateway URL in the meantime. When that URL fails to load, it moves on to the next read gateway.
type SeedImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
imageProperty: IItemProperty<any>
width?: number
height?: number
filename?: string
ensure?: boolean
}The real type is built on DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, and SeedImage forwards ref to the <img> on React 18 and 19.
Props
| Name | Type | Default | Description |
|---|---|---|---|
imageProperty | IItemProperty<any> | required | The Image property, for example from useItemProperty or item.properties. |
width | number | The display width in pixels. Picks the closest stored size and, with ensure, creates that size if it's missing. Also set as the <img>'s width. | |
height | number | Passed to the <img>. | |
filename | string | Use this filename instead of the property's value. | |
ensure | boolean | true | Download the image and create the needed size if they're missing. With false, only what's already on the device is shown. |
alt | string | the property name | Alt text. |
other <img> props | Passed to the <img>. |
What it shows
SeedImage uses the first source it has, in this order:
- the stored size closest to
width, whenwidthis set and sized copies exist - the original file on the device
- the property's value when it's a
FileorBlob(a preview before saving) or ablob:URL - while
ensureis on and nothing local is ready, an Arweave gateway URL (gatewayHreffromuseEnsureLocalImage). If the<img>fails to load it,SeedImagetries the next gateway, and still calls youronError - a transparent 1×1 placeholder
It renders nothing when the property has no value, no filename and no Arweave transaction ID. When the property is cleared it stops showing the image and renders nothing, and when the value changes to another file it shows the new one.
SeedImage is wrapped in React.memo with the default shallow comparison of all props, so it re-renders when any prop changes, className, style and handlers included.
Example
import { SeedImage, useItemProperty } from '@seedprotocol/react'
function PostCover({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'cover' })
if (!property) return null
return (
<SeedImage
imageProperty={property}
width={760}
alt="Cover image"
style={{ width: 760, maxWidth: '100%' }}
/>
)
}useEnsureLocalImage
The download-and-resize step of SeedImage as a hook, for when you render the image yourself. It makes sure the original is on the device (downloading it from Arweave if needed) and that a resized copy exists for width.
function useEnsureLocalImage(params: {
imageProperty: IItemProperty<any>
filename?: string
width?: number
enabled?: boolean
}): {
status: 'idle' | 'loading' | 'ready' | 'unsupported' | 'failed' | 'missing-id'
gatewayHref: string | undefined
gatewayHrefs: string[]
onGatewayError: () => void
result: EnsureImageLocalResult | null
}The types are exported as UseEnsureLocalImageParams and UseEnsureLocalImageReturn.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
imageProperty | IItemProperty<any> | required | The Image property, as for SeedImage. |
filename | string | Use this filename (or Arweave transaction ID) instead of the property's value. | |
width | number | The display width. The hook creates the closest standard size (480, 760, 1024, 1440 or 1920). Without it, it creates the 480 size. | |
enabled | boolean | true | false skips the work and returns status: 'idle'. |
Returns
| Field | Type | Description |
|---|---|---|
status | string | 'idle' before starting or when there's nothing to look up; 'loading' while working; then the result's status. |
gatewayHref | string | undefined | The current gateway URL for the image's transaction ID, when one is known: an entry of gatewayHrefs, starting with the first. Set even before the download finishes, so you can show it right away. undefined once every gateway has failed. |
gatewayHrefs | string[] | <gateway>/raw/<txId> on every read gateway: preferred gateways first (where a just-published image appears before it reaches the others), then the public gateways, then the configured host if it isn't already listed. Empty without a transaction ID. |
onGatewayError | () => void | Moves gatewayHref to the next gateway. Call it from the <img>'s onError. |
result | EnsureImageLocalResult | null | { status, filePath?, createdWidths? }: where the original is stored and which sizes were created. |
The final statuses mean:
| Status | Meaning |
|---|---|
'ready' | The original is on the device and the requested size exists. |
'failed' | The file wasn't found locally and couldn't be downloaded, or resizing failed. |
'missing-id' | There's no filename or transaction ID to look up. |
'unsupported' | This environment can't store downloaded files in OPFS. |
The transaction ID comes, in order, from the property's stored transaction ID, filename, the property's resolved reference, or its value, whichever is a 43-character Arweave ID. Calls for the same image and size share one download.
Example
import type { IItemProperty } from '@seedprotocol/sdk'
import { SeedImage, useEnsureLocalImage, useItemProperty } from '@seedprotocol/react'
function Cover({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'cover' })
// The hook needs a property; while there's none, `enabled: false` keeps it idle.
const imageProperty = property as IItemProperty<any>
const { status, gatewayHref, onGatewayError } = useEnsureLocalImage({
imageProperty,
width: 1024,
enabled: !!property,
})
if (property && status === 'ready') {
return <SeedImage imageProperty={imageProperty} width={1024} ensure={false} alt="Cover" />
}
if (gatewayHref) {
return <img src={gatewayHref} onError={onGatewayError} alt="Cover (loading from Arweave)" />
}
return <p>{status === 'failed' ? 'Image unavailable' : 'Loading image…'}</p>
}Once status is 'ready', the file is local, so SeedImage can show it with ensure={false}.
useResolvedMediaRef
Turns a raw media string, as found in feeds and XML, into a URL you can put in src or href.
function useResolvedMediaRef(params: {
value: string | undefined | null
enabled?: boolean
treatAs?: 'arweaveTx' | 'seedUid' | 'url'
}): {
href: string | undefined
status: 'idle' | 'loading' | 'ready' | 'empty' | 'unresolved' | 'error'
source: 'direct' | 'gateway' | 'localBlob' | undefined
error: Error | null
result: ResolveMediaRefResult | null
refetch: () => void
}The types are exported as UseResolvedMediaRefParams and UseResolvedMediaRefReturn.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
value | string | undefined | null | required | The raw string. |
enabled | boolean | true | false skips resolving and returns status: 'idle'. |
treatAs | 'arweaveTx' | 'seedUid' | 'url' | Skip detection and treat value as this kind. |
How values resolve
value looks like | Result |
|---|---|
http(s)://, blob: or data: URL | ready, source: 'direct', the URL as is |
| an EAS explorer attestation URL | unresolved (it isn't media) |
| a 43-character Arweave transaction ID | ready: a blob: URL of the local copy in images/ (source: 'localBlob') if there is one, otherwise the gateway URL (source: 'gateway') |
a seed UID (0x + 64 hex characters) | looks up the item's stored transaction ID in the local database, then resolves it as above; unresolved if there isn't one |
| a seed local ID, or anything else | unresolved |
empty or whitespace, null or undefined | idle (the 'empty' status in the type isn't reached through this hook) |
Returns
| Field | Type | Description |
|---|---|---|
href | string | undefined | The URL for the current value, when it resolved. After value, enabled or treatAs changes, and after refetch, href is undefined and status is 'loading' until the new resolve finishes. |
status | string | See above. 'error' means the lookup threw. |
source | 'direct' | 'gateway' | 'localBlob' | undefined | Where href points. |
error | Error | null | Set when status is 'error'. |
result | ResolveMediaRefResult | null | The SDK's full result, including the reason when unresolved. |
refetch | () => void | Resolve again, for example after the file has been downloaded. |
A blob: URL the hook created for a local copy (source: 'localBlob') is revoked when value, enabled or treatAs changes, on refetch, and on unmount, so don't keep href beyond the component's life.
import { useResolvedMediaRef } from '@seedprotocol/react'
function EnclosureLink({ value }: { value: string }) {
const { href, status } = useResolvedMediaRef({ value })
if (status === 'loading') return <span>…</span>
if (!href) return <span>Media unavailable</span>
return <a href={href}>Open media</a>
}SeedMediaImage, SeedMediaVideo, SeedMediaAudio, SeedMediaFile
Elements for raw media strings. Each resolves value with useResolvedMediaRef and renders nothing until it has a URL, so give them a placeholder of your own if you need one.
type SeedMediaImageProps = Omit<ImgProps, 'src'> & MediaProps<ImgProps & { src: string }>
type SeedMediaVideoProps = Omit<VideoProps, 'src'> & MediaProps<VideoProps & { src: string }>
type SeedMediaAudioProps = Omit<AudioProps, 'src'> & MediaProps<AudioProps & { src: string }>
type SeedMediaFileProps = Omit<AnchorProps, 'href'> &
MediaProps<AnchorProps & { href: string }> & { download?: string | boolean }
type MediaProps<P> = {
value: string | undefined | null
enabled?: boolean
treatAs?: 'arweaveTx' | 'seedUid' | 'url'
render?: (props: P) => React.ReactNode
}MediaProps, ImgProps and the rest are shorthand on this page for the shared props and React's element props. The package exports the four …Props types.
Props
| Name | Applies to | Default | Description |
|---|---|---|---|
value | all | required | The raw media string. |
enabled | all | true | false renders nothing. |
treatAs | all | As in useResolvedMediaRef. | |
render | all | Render your own element. Receives the merged props, with src (or href) set. | |
alt | image | '' | Alt text. |
controls | video, audio | true | Show the browser's controls. |
download | file | Passed to the link's download attribute. | |
target, rel | file | see below | For http(s) links, target defaults to '_blank' and rel to 'noopener noreferrer'. |
children | file | the URL's last path segment, or "Open" | The link text. |
| other element props | all | Passed to the element. |
Example
import { SeedMediaAudio, SeedMediaFile, SeedMediaImage, SeedMediaVideo } from '@seedprotocol/react'
function Enclosures({ image, video, audio, pdf }: Record<'image' | 'video' | 'audio' | 'pdf', string>) {
return (
<>
<SeedMediaImage value={image} alt="Episode artwork" width={320} />
<SeedMediaVideo value={video} preload="metadata" />
<SeedMediaAudio value={audio} />
<SeedMediaFile value={pdf} download>
Download transcript
</SeedMediaFile>
</>
)
}A File item property stored on the device doesn't need these components: its value is already a URL to the stored file, made again each time the item loads. Build the link as Offer a file for download does, which also names the download; File, Image and Html properties says when the SDK revokes that URL.
SeedHtml
Renders HTML through a sanitizer that you supply. The SDK stores HTML as is and doesn't ship a default policy, so sanitize is required.
type SeedHtmlProps = Omit<DivProps, 'dangerouslySetInnerHTML' | 'children'> & {
html: string | null | undefined
sanitize: (raw: string) => string
render?: (props: { html: string }) => React.ReactNode
}| Name | Type | Default | Description |
|---|---|---|---|
html | string | null | undefined | required | The HTML. Empty or whitespace-only renders nothing. |
sanitize | (raw: string) => string | required | Your sanitizer, for example DOMPurify.sanitize. Runs again when html or sanitize changes, so define it outside the component (or with useCallback); an inline arrow function runs it on every render. |
render | (props: { html: string }) => ReactNode | Receive the sanitized HTML and render your own container instead of the default <div>. The load failover below then needs attachArweaveLoadFailover on your container. | |
other <div> props | Passed to the <div>. |
Before sanitize runs, SeedHtml moves each Arweave link in a src, href, poster or srcset attribute onto the first gateway in the reader's read list, keeping the link's /raw/<id> or /<id> form and the rest of the markup byte for byte. It counts ar://<id>, /raw/<id> on any host, and /<id> on a known gateway. With the default gateway settings, ar://<id> and https://ar.seedprotocol.io/raw/<id> both become https://arweave.net/raw/<id>, so your sanitizer sees https links and never ar://. The stored Html doesn't change. When an img, video, audio or source inside the <div> then fails to load, SeedHtml points it at the same transaction on the next gateway in the list, until the list runs out. Links in published content explains where these links come from and which host each one carries.
import DOMPurify from 'dompurify'
import { SeedHtml, useItemProperty } from '@seedprotocol/react'
const sanitize = (raw: string) => DOMPurify.sanitize(raw)
function PostBody({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'body' })
const html = property?.value != null ? String(property.value) : undefined
return <SeedHtml className="prose" html={html} sanitize={sanitize} />
}attachArweaveLoadFailover
The load failover SeedHtml runs on its own <div>, for a container you render with render.
function attachArweaveLoadFailover(container: HTMLElement): () => voidIt listens for load errors in the capture phase on container (load errors don't bubble), and when an img, video, audio or source inside it fails on an Arweave link in src, srcset or poster, moves that link to the next gateway in the read list. A <source> that moves makes its media element reload. It returns a function that removes the listener.
import { useEffect, useRef } from 'react'
import DOMPurify from 'dompurify'
import { SeedHtml, attachArweaveLoadFailover } from '@seedprotocol/react'
const sanitize = (raw: string) => DOMPurify.sanitize(raw)
function ArticleBody({ html }: { html: string }) {
const ref = useRef<HTMLElement>(null)
useEffect(() => (ref.current ? attachArweaveLoadFailover(ref.current) : undefined), [])
return <article ref={ref} dangerouslySetInnerHTML={{ __html: html }} />
}
export function Article({ html }: { html: string | undefined }) {
return <SeedHtml html={html} sanitize={sanitize} render={(props) => <ArticleBody html={props.html} />} />
}SeedJson
Displays a value as formatted JSON in a <pre>. It never evaluates the value.
type SeedJsonProps = Omit<PreProps, 'children' | 'dangerouslySetInnerHTML'> & {
value: unknown
format?: (value: unknown) => string
formatOptions?: FormatSeedJsonOptions
render?: (props: { text: string }) => React.ReactNode
}| Name | Type | Default | Description |
|---|---|---|---|
value | unknown | required | What to show. A string that looks like a JSON object or array is parsed first, so a Json property's stored string shows formatted. |
format | (value: unknown) => string | Replace the built-in formatting. | |
formatOptions | FormatSeedJsonOptions | Options for the built-in formatSeedJson. | |
render | (props: { text: string }) => ReactNode | Render your own element with the formatted text. | |
other <pre> props | Passed to the <pre>. |
import { SeedJson, useItemProperty } from '@seedprotocol/react'
function MetadataView({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'metadata' })
return <SeedJson value={property?.value} formatOptions={{ maxDepth: 4 }} />
}formatSeedJson
The formatter behind SeedJson, for use outside React.
function formatSeedJson(
value: unknown,
options?: { maxDepth?: number; maxStringLength?: number; space?: string | number },
): string| Option | Default | Description |
|---|---|---|
maxDepth | 6 | Deeper objects show as "[Max depth]". |
maxStringLength | 50000 | Longer strings are cut and end with …. |
space | 2 | Indentation, passed to JSON.stringify. |
A string that looks like a JSON object or array is parsed and re-formatted; any other string is returned as is (cut to maxStringLength), without quotes. It handles values JSON.stringify can't: BigInt and symbols become strings, functions "[Function]", dates ISO strings, and an object that contains itself "[Circular]". undefined and null return 'undefined' and 'null', and a value that still can't be serialized returns '[Unserializable JSON]'. The options type is exported as FormatSeedJsonOptions.
Only a real cycle prints "[Circular]": an object that appears twice in the value without one is printed in full both times.
useNormalizedFeedItemFields
Classifies the fields of a parsed feed item using a manifest that says what each field holds. It runs synchronously and doesn't touch the network. Pass the results to the SeedMedia* components and SeedHtml.
function useNormalizedFeedItemFields(
item: Record<string, unknown> | null | undefined,
manifest: FeedFieldManifest,
): Record<string, NormalizedFeedFieldValue | undefined>FeedFieldManifest and NormalizedFeedFieldValue come from @seedprotocol/sdk:
type FeedFieldManifest = Record<string, {
role: 'image' | 'audio' | 'video' | 'file' | 'html' | 'text'
treatAs?: 'arweaveTx' | 'seedUid' | 'url'
}>
type NormalizedFeedFieldValue =
| { role: 'image' | 'audio' | 'video' | 'file'; raw: string; classification: MediaRefClassification }
| { role: 'html'; raw: string }
| { role: 'text'; raw: string }For each manifest key, the hook reads the item's field under that key, or its snake_case or camelCase spelling, as a trimmed string. Missing or empty fields map to undefined. Media roles also get a classification (URL, Arweave ID, seed UID and so on). null or undefined for item returns {}.
The result is memoised on item and manifest. Define the manifest outside the component, or memoise it, so it isn't recomputed on every render.
import DOMPurify from 'dompurify'
import type { FeedFieldManifest } from '@seedprotocol/sdk'
import { SeedHtml, SeedMediaImage, useNormalizedFeedItemFields } from '@seedprotocol/react'
const sanitize = (raw: string) => DOMPurify.sanitize(raw)
const manifest: FeedFieldManifest = {
featureImage: { role: 'image' },
html: { role: 'html' },
title: { role: 'text' },
}
function FeedEntry({ entry }: { entry: Record<string, unknown> }) {
const fields = useNormalizedFeedItemFields(entry, manifest)
return (
<article>
<h2>{fields.title?.raw}</h2>
<SeedMediaImage value={fields.featureImage?.raw} alt="" />
<SeedHtml html={fields.html?.raw} sanitize={sanitize} />
</article>
)
}Related
- Working with file and image properties: uploading, storing and displaying images
- Files and OPFS:
useFiles,useOPFSFilesandOPFSFilesManager - Item properties:
useItemProperty - Feed overview: generating feeds from published items
- Importing a feed with mapping
- Reading and writing data: Html and Json
Publishing and EAS sync
React hooks for publishing an item (usePublishItem) and for watching the SDK's EAS sync runs (useEasSyncProcesses and related hooks).
Files and OPFS
Hooks and a ready-made component for listing, browsing and managing the files a Seed app stores in the browser's private file system (OPFS).