diff --git a/components/nft-image/renderers/NFTImageRenderer.tsx b/components/nft-image/renderers/NFTImageRenderer.tsx index ac4f0e08d9..2f1d4ffaa6 100644 --- a/components/nft-image/renderers/NFTImageRenderer.tsx +++ b/components/nft-image/renderers/NFTImageRenderer.tsx @@ -5,6 +5,7 @@ import { Col } from "react-bootstrap"; import styles from "../NFTImage.module.scss"; import NFTImageBalance from "../NFTImageBalance"; import type { BaseRendererProps } from "../types/renderer-props"; +import { withArweaveFallback } from "../utils/arweave-fallback"; function getSrc( nft: BaseRendererProps["nft"], @@ -45,7 +46,7 @@ export default function NFTImageRenderer(props: Readonly) { id={props.id ?? `image-${props.nft.id}`} src={src} alt={props.nft.name} - onError={({ currentTarget }) => { + onError={withArweaveFallback(({ currentTarget }) => { if (currentTarget.src === props.nft.thumbnail) { currentTarget.src = props.nft.scaled ? props.nft.scaled @@ -55,7 +56,7 @@ export default function NFTImageRenderer(props: Readonly) { } else if ("metadata" in props.nft) { currentTarget.src = props.nft.metadata.image; } - }} + })} /> {props.showBalance && ( ) { return ( @@ -32,7 +33,7 @@ export default function NFTVideoRenderer(props: Readonly) { : props.nft.animation } className={props.imageStyle} - onError={({ currentTarget }) => { + onError={withArweaveFallback(({ currentTarget }) => { if ( "metadata" in props.nft && currentTarget.src === props.nft.compressed_animation @@ -41,7 +42,7 @@ export default function NFTVideoRenderer(props: Readonly) { } else if ("metadata" in props.nft) { currentTarget.src = props.nft.metadata.animation; } - }}> + })}> ); } diff --git a/components/nft-image/utils/arweave-fallback.ts b/components/nft-image/utils/arweave-fallback.ts new file mode 100644 index 0000000000..4a88374003 --- /dev/null +++ b/components/nft-image/utils/arweave-fallback.ts @@ -0,0 +1,46 @@ +import type React from "react"; + +function isArweaveUrl(url: string): boolean { + try { + const h = new URL(url).hostname.toLowerCase(); + return h === "arweave.net" || h.endsWith(".arweave.net"); + } catch { + return false; + } +} + +function getArweaveFallbackUrl(url: string): string | null { + if (!isArweaveUrl(url)) return null; + try { + const u = new URL(url); + u.hostname = "ar-io.net"; + u.host = "ar-io.net" + (u.port ? ":" + u.port : ""); + return u.toString(); + } catch { + return null; + } +} + +type MediaErrorEvent = + | React.SyntheticEvent + | React.SyntheticEvent; + +export function withArweaveFallback( + onError?: (event: MediaErrorEvent) => void +): (event: MediaErrorEvent) => void { + return (event: MediaErrorEvent) => { + const target = event.currentTarget; + const src = target.src; + if (src && isArweaveUrl(src)) { + const fallback = getArweaveFallbackUrl(src); + if (fallback) { + if (target.dataset['arweaveOriginalSrc'] === undefined) { + target.dataset['arweaveOriginalSrc'] = src; + } + target.src = fallback; + return; + } + } + onError?.(event); + }; +} diff --git a/config/nextConfig.ts b/config/nextConfig.ts index a4cf75f8a0..624b7c783d 100644 --- a/config/nextConfig.ts +++ b/config/nextConfig.ts @@ -19,6 +19,7 @@ export function sharedConfig( { protocol: "https", hostname: "6529.io" }, { protocol: "https", hostname: "staging.6529.io" }, { protocol: "https", hostname: "arweave.net" }, + { protocol: "https", hostname: "ar-io.net" }, { protocol: "http", hostname: "localhost" }, { protocol: "https", hostname: "media.generator.seize.io" }, { protocol: "https", hostname: "d3lqz0a4bldqgf.cloudfront.net" },