Load gallery images directly from R2 CDN, bypassing Next image proxy.
Use unoptimized next/image with pre-sized R2 URLs (-sm/-md/-lg) when a public R2 domain is configured. Falls back to the original file if a sized variant is missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
69094071c8
commit
b9bef5e264
@ -272,6 +272,14 @@ export const IMAGE_QUALITY =
|
|||||||
process.env.NEXT_PUBLIC_IMAGE_QUALITY
|
process.env.NEXT_PUBLIC_IMAGE_QUALITY
|
||||||
? parseInt(process.env.NEXT_PUBLIC_IMAGE_QUALITY)
|
? parseInt(process.env.NEXT_PUBLIC_IMAGE_QUALITY)
|
||||||
: 75;
|
: 75;
|
||||||
|
// Load photos from storage CDN in the browser (skip /_next/image server proxy).
|
||||||
|
// Enabled by default when a public R2 domain is configured; set to 0 to disable.
|
||||||
|
export const DIRECT_STORAGE_IMAGES =
|
||||||
|
process.env.NEXT_PUBLIC_DIRECT_STORAGE_IMAGES === '1' ||
|
||||||
|
(
|
||||||
|
process.env.NEXT_PUBLIC_DIRECT_STORAGE_IMAGES !== '0' &&
|
||||||
|
Boolean(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN)
|
||||||
|
);
|
||||||
export const BLUR_ENABLED =
|
export const BLUR_ENABLED =
|
||||||
process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
|
process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/alt-text */
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
import { BLUR_ENABLED } from '@/app/config';
|
import { BLUR_ENABLED, DIRECT_STORAGE_IMAGES } from '@/app/config';
|
||||||
import { useAppState } from '@/app/AppState';
|
import { useAppState } from '@/app/AppState';
|
||||||
|
import {
|
||||||
|
getDirectPhotoDisplayUrl,
|
||||||
|
isExternalStoragePhotoUrl,
|
||||||
|
} from '@/photo/storage';
|
||||||
import { clsx} from 'clsx/lite';
|
import { clsx} from 'clsx/lite';
|
||||||
import Image, { ImageProps } from 'next/image';
|
import Image, { ImageProps } from 'next/image';
|
||||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@ -26,11 +30,29 @@ export default function ImageWithFallback({
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [didError, setDidError] = useState(false);
|
const [didError, setDidError] = useState(false);
|
||||||
|
const [fallbackSrc, setFallbackSrc] = useState<string | undefined>();
|
||||||
const [fadeFallbackTransition, setFadeFallbackTransition] =
|
const [fadeFallbackTransition, setFadeFallbackTransition] =
|
||||||
useState(!hasLoadedWithAnimations);
|
useState(!hasLoadedWithAnimations);
|
||||||
|
|
||||||
|
const srcString = typeof props.src === 'string' ? props.src : undefined;
|
||||||
|
const useDirectStorage = DIRECT_STORAGE_IMAGES &&
|
||||||
|
Boolean(srcString) &&
|
||||||
|
isExternalStoragePhotoUrl(srcString!);
|
||||||
|
const displayWidth = typeof props.width === 'number' ? props.width : 640;
|
||||||
|
const directSrc = useDirectStorage && srcString && !fallbackSrc
|
||||||
|
? getDirectPhotoDisplayUrl(srcString, displayWidth)
|
||||||
|
: undefined;
|
||||||
|
const resolvedSrc = fallbackSrc ?? directSrc ?? props.src;
|
||||||
|
|
||||||
const onLoad = useCallback(() => setIsLoading(false), []);
|
const onLoad = useCallback(() => setIsLoading(false), []);
|
||||||
const onError = useCallback(() => setDidError(true), []);
|
const onError = useCallback(() => {
|
||||||
|
if (useDirectStorage && srcString && !fallbackSrc) {
|
||||||
|
setFallbackSrc(srcString);
|
||||||
|
setIsLoading(true);
|
||||||
|
} else {
|
||||||
|
setDidError(true);
|
||||||
|
}
|
||||||
|
}, [fallbackSrc, srcString, useDirectStorage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -61,6 +83,8 @@ export default function ImageWithFallback({
|
|||||||
>
|
>
|
||||||
<Image ref={refProp ?? ref} {...{
|
<Image ref={refProp ?? ref} {...{
|
||||||
...props,
|
...props,
|
||||||
|
src: resolvedSrc,
|
||||||
|
unoptimized: useDirectStorage,
|
||||||
priority,
|
priority,
|
||||||
className: classNameImage,
|
className: classNameImage,
|
||||||
onLoad,
|
onLoad,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { DIRECT_STORAGE_IMAGES } from '@/app/config';
|
||||||
import {
|
import {
|
||||||
getNextImageUrlForRequest,
|
getNextImageUrlForRequest,
|
||||||
NextImageSize,
|
NextImageSize,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
getStorageUrlsForPrefix,
|
getStorageUrlsForPrefix,
|
||||||
uploadFileFromClient,
|
uploadFileFromClient,
|
||||||
} from '@/platforms/storage';
|
} from '@/platforms/storage';
|
||||||
|
import { isUrlFromAwsS3 } from '@/platforms/storage/aws-s3';
|
||||||
|
import { isUrlFromCloudflareR2 } from '@/platforms/storage/cloudflare-r2';
|
||||||
|
import { isUrlFromMinio } from '@/platforms/storage/minio';
|
||||||
import { Photo } from '..';
|
import { Photo } from '..';
|
||||||
import { fetchBase64ImageFromUrl } from '@/utility/image';
|
import { fetchBase64ImageFromUrl } from '@/utility/image';
|
||||||
|
|
||||||
@ -98,12 +102,36 @@ const getSuffixFromNextImageSize = (nextSize: NextImageSize) =>
|
|||||||
OPTIMIZED_FILE_SIZES.find(({ size }) => size === nextSize)?.suffix
|
OPTIMIZED_FILE_SIZES.find(({ size }) => size === nextSize)?.suffix
|
||||||
?? OPTIMIZED_SUFFIX_DEFAULT;
|
?? OPTIMIZED_SUFFIX_DEFAULT;
|
||||||
|
|
||||||
|
export const isExternalStoragePhotoUrl = (url: string) =>
|
||||||
|
isUrlFromCloudflareR2(url) ||
|
||||||
|
isUrlFromAwsS3(url) ||
|
||||||
|
isUrlFromMinio(url);
|
||||||
|
|
||||||
|
const getNextImageSizeForDisplayWidth = (displayWidth: number): NextImageSize => {
|
||||||
|
if (displayWidth <= 200) { return 200; }
|
||||||
|
if (displayWidth <= 640) { return 640; }
|
||||||
|
if (displayWidth <= 1080) { return 1080; }
|
||||||
|
if (displayWidth <= 1200) { return 1200; }
|
||||||
|
return 1920;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Public CDN URL for a photo (e.g. R2 -md/-sm), not /_next/image. */
|
||||||
|
export const getDirectPhotoDisplayUrl = (
|
||||||
|
imageUrl: string,
|
||||||
|
displayWidth: number,
|
||||||
|
) =>
|
||||||
|
getOptimizedPhotoUrl({
|
||||||
|
imageUrl,
|
||||||
|
size: getNextImageSizeForDisplayWidth(displayWidth),
|
||||||
|
useNextImage: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const getOptimizedPhotoUrl = (
|
export const getOptimizedPhotoUrl = (
|
||||||
args: Parameters<typeof getNextImageUrlForRequest>[0] & {
|
args: Parameters<typeof getNextImageUrlForRequest>[0] & {
|
||||||
useNextImage?: boolean
|
useNextImage?: boolean
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { useNextImage = true } = args;
|
const { useNextImage = !DIRECT_STORAGE_IMAGES } = args;
|
||||||
const suffix = getSuffixFromNextImageSize(args.size);
|
const suffix = getSuffixFromNextImageSize(args.size);
|
||||||
const {
|
const {
|
||||||
urlBase,
|
urlBase,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user