diff --git a/next.config.ts b/next.config.ts
index 4f2d549..ea73e8f 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,13 +2,14 @@ import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
import path from 'path'
import { fileURLToPath } from 'url'
-
+
const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
import { redirects } from './redirects'
-
+
const NEXT_PUBLIC_SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3002'
-
+const S3_BUCKET_URL = process.env.NEXT_PUBLIC_S3_URL || ''
+
const nextConfig: NextConfig = {
output: 'standalone',
sassOptions: {
@@ -20,14 +21,14 @@ const nextConfig: NextConfig = {
pathname: '/api/media/file/**',
},
],
- qualities: [100],
+ qualities: [25, 50, 75, 100],
remotePatterns: [
- ...[NEXT_PUBLIC_SERVER_URL].map((item) => {
+ ...[NEXT_PUBLIC_SERVER_URL, S3_BUCKET_URL].filter(Boolean).map((item) => {
const url = new URL(item)
-
return {
hostname: url.hostname,
protocol: url.protocol.replace(':', '') as 'http' | 'https',
+ pathname: '/**',
}
}),
],
@@ -38,22 +39,21 @@ const nextConfig: NextConfig = {
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
-
+
const replaceHash = (val: unknown) =>
typeof val === 'string' ? val.replace('[chunkhash]', '[contenthash]') : val
-
+
webpackConfig.output.filename = replaceHash(webpackConfig.output.filename)
webpackConfig.output.chunkFilename = replaceHash(webpackConfig.output.chunkFilename)
-
+
return webpackConfig
},
-
+
reactStrictMode: true,
redirects,
turbopack: {
root: path.resolve(dirname),
},
}
-
+
export default withPayload(nextConfig, { devBundleServerPackages: false })
-
\ No newline at end of file
diff --git a/src/blocks/Showcase/Component.tsx b/src/blocks/Showcase/Component.tsx
index 7e73a7e..d8b1dce 100644
--- a/src/blocks/Showcase/Component.tsx
+++ b/src/blocks/Showcase/Component.tsx
@@ -1,15 +1,10 @@
import React from 'react'
import Image from 'next/image'
-
-type MediaObject = {
- url?: string
- alt?: string
- width?: number
- height?: number
-}
+// Import the generated Media type from your Payload config
+import type { Media as MediaType } from '@/payload-types'
type ShowcaseItem = {
- image?: MediaObject | null
+ image?: MediaType | null
imageUrl?: string
title: string
description?: string
@@ -23,39 +18,47 @@ type ShowcaseBlockProps = {
items?: ShowcaseItem[]
}
-function ShowcaseImage(props: { item: ShowcaseItem }): React.ReactElement {
- const item = props.item
- const imageUrl = item.image != null && typeof item.image === 'object' ? item.image.url : null
- const imageAlt = item.image != null && typeof item.image === 'object' ? item.image.alt : item.title
+function ShowcaseImage({ item }: { item: ShowcaseItem }) {
+ // Payload 3.0 returns the object directly if depth is configured
+ const image = item.image
- if (imageUrl != null && item.imageUrl != null) {
- return (
-
- href={item.imageUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="block relative w-full aspect-video overflow-hidden rounded-t-xl group"
- >
-
+ // Use the image object if available, otherwise fallback
+ if (image && typeof image === 'object' && 'url' in image) {
+ const content = (
+
+
+ {/* Hover Overlay */}
-
-
-
-
- )
- }
-
- if (imageUrl != null) {
- return (
-
-
+ {item.imageUrl && (
+
+
+
+ )}
)
+
+ if (item.imageUrl) {
+ return (
+
+ {content}
+
+ )
+ }
+ return content
}
+ // Placeholder if no image
return (
-
+
Coming soon
)
@@ -82,59 +85,54 @@ export function ShowcaseBlock(props: ShowcaseBlockProps): React.ReactElement | n
)}
- {items.map(function (item, i) {
- return (
-
-
+ {items.map((item, i) => (
+
+
-
-
{item.title}
+
+
{item.title}
- {item.description && (
-
{item.description}
- )}
+ {item.description && (
+
{item.description}
+ )}
- {Array.isArray(item.tags) && item.tags.length > 0 && (
-
- {item.tags.map(function (t, j) {
- return (
-
- {t.tag}
-
- )
- })}
-
- )}
+ {item.tags && item.tags.length > 0 && (
+
+ {item.tags.map((t, j) => (
+
+ {t.tag}
+
+ ))}
+
+ )}
- {Array.isArray(item.links) && item.links.length > 0 && (
-
- {item.links.map(function (link, k) {
- return (
-
- key={k}
- href={link.url}
- target={link.newTab ? '_blank' : '_self'}
- rel="noopener noreferrer"
- className="flex items-center gap-1.5 text-xs text-foreground/50 hover:text-foreground/80 transition-colors"
- >
-
- {link.label}
-
- )
- })}
-
- )}
-
+ {item.links && item.links.length > 0 && (
+
+ )}
- )
- })}
+
+ ))}
)
-}
\ No newline at end of file
+}
diff --git a/src/components/Media/ImageMedia/index.tsx b/src/components/Media/ImageMedia/index.tsx
index 0caf826..351655f 100644
--- a/src/components/Media/ImageMedia/index.tsx
+++ b/src/components/Media/ImageMedia/index.tsx
@@ -1,50 +1,11 @@
'use client'
-import type { StaticImageData } from 'next/image'
-
import { cn } from '@/utilities/ui'
import NextImage from 'next/image'
import React from 'react'
-
import type { Props as MediaProps } from '../types'
-
-import { cssVariables } from '@/cssVariables'
import { getMediaUrl } from '@/utilities/getMediaUrl'
-const { breakpoints } = cssVariables
-
-// A base64 encoded image to use as a placeholder while the image is loading
-const placeholderBlur =
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABchJREFUWEdtlwtTG0kMhHtGM7N+AAdcDsjj///EBLzenbtuadbLJaZUTlHB+tRqSesETB3IABqQG1KbUFqDlQorBSmboqeEBcC1d8zrCixXYGZcgMsFmH8B+AngHdurAmXKOE8nHOoBrU6opcGswPi5KSP9CcBaQ9kACJH/ALAA1xm4zMD8AczvQCcAQeJVAZsy7nYApTSUzwCHUKACeUJi9TsFci7AHmDtuHYqQIC9AgQYKnSwNAig4NyOOwXq/xU47gDYggarjIpsRSEA3Fqw7AGkwgW4fgALAdiC2btKgNZwbgdMbEFpqFR2UyCR8xwAhf8bUHIGk1ckMyB5C1YkeWAdAPQBAeiD6wVYPoD1HUgXwFagZAGc6oSpTmilopoD5GzISQD3odcNIFca0BUQQM5YA2DpHV0AYURBDIAL0C+ugC0C4GedSsVUmwC8/4w8TPiwU6AClJ5RWL1PgQNkrABWdKB3YF3cBwRY5lsI4ApkKpCQi+FIgFJU/TDgDuAxAAwonJuKpGD1rkCXCR1ALyrAUSSEQAhwBdYZ6DPAgSUA2c1wKIZmRcHxMzMYR9DH8NlbkAwwApSAcABwBwTAbb6owAr0AFiZPILVEyCtMmK2jCkTwFDNUNj7nJETQx744gCUmgkZVGJUHyakEZE4W91jtGFA9KsD8Z3JFYDlhGYZLWcllwJMnplcPy+csFAgAAaIDOgeuAGoB96GLZg4kmtfMjnr6ig5oSoySsoy3ya/FMivXZWxwr0KIf9nACbfqcBEgmBSAtAlIT83R+70IWpyACamIjf5E1Iqb9ECVmnoI/FvAIRk8s2J0Y5IquQDgB+5wpScw5AUTC75VTmTs+72NUzoCvQIaAXv5Q8PDAZKLD+MxLv3RFE7KlsQChgBIlKiCv5ByaZv3gJZNm8AnVMhAN+EjrtTYQMICJpu6/0aiQnhClANlz+Bw0cIWa8ev0sBrtrhAyaXEnrfGfATQJiRKih5vKeOHNXXPFrgyamAADh0Q4F2/sESojomDS9o9k0b0H83xjB8qL+JNoTjN+enjpaBpingRh4e8MSugudM030A8FeqMI6PFIgNyPehkpZWGFEAARIQdH5LcAAqIACHkAJqg4OoBccHAuz76wr4BbzFOEa8iBuAZB8AtJHLP2VgMgJw/EIBowo7HxCAH3V6dAXEE/vZ5aZIA8BP8RKhm7Cp8BnAMnAQADdgQDA520AVIpScP+enHz0Gwp25h4i2dPg5FkDXrbsdJikQwXuWgaM5gEMk1AgH4DKKFjDf3bMD+FjEeIxLlRKYnBk2BbquvSDCAQ4gwZiMAAmH4gBTyRtEsYxi7gP6QSrc//39BrDNqG8rtYTmC4BV1SfMhOhaumFCT87zy4pPhQBZEK1kQVRjJBBi7AOlePgyAPYjwlvtagx9e/dnQraAyS894TIkkAIEYMKEc8k4EqJ68lZ5jjNqcQC2QteQOf7659umwBgPybNtK4dg9WvnMyFwXYGP7uEO1lwJgAnPNeMYMVXbIIYKFioI4PGFt+BWPVfmWJdjW2lTUnLGCswECAgaUy86iwA1464ajo0QhgMBFGyBoZahANsMpMfXr1JA1SN29m5lqgXj+UPV85uRA7yv/KYUO4Tk7Hc1AZwbIRzg0AyNj2UlAMwfSLSMnl7fdAbcxHuA27YaAMvaQ4GOjwX4RTUGAG8Ge14N963g1AynqUiFqRX9noasxT4b8entNRQYyamk/3tYcHsO7R3XJRRYOn4tw4iUnwBM5gDnySGOreAwAGo8F9IDHEcq8Pz2Kg/oXCpuIL6tOPD8LsDn0ABYQoGFRowlsAEUPPDrGAGowAbgKsgDMmE8mDy/vXQ9IAwI7u4wta+gAdAdgB64Ah9SgD4IgGKhwACoAjgNgFDhtxY8f33ZTMjqdTAiHMBPrn8ZWkEfzFdX4Oc1AHg3+ADbvN8PU8WdFKg4Tt6CQy2+D4YHaMT/JP4XzbAq98cPDIUAAAAASUVORK5CYII='
-
-/**
- * ImageMedia
- *
- * This component passes a **relative** `src` (e.g. `/media/...`) to Next.js Image.
- * The `getMediaUrl` utility constructs the full URL by prepending the base URL from env vars
- * (NEXT_PUBLIC_SERVER_URL). Next.js then optimizes this using `remotePatterns` configured
- * in next.config.js — no custom `loader` needed.
- *
- * Flow:
- * 1. Resource URL from Payload: `/media/image-123.jpg`
- * 2. getMediaUrl() adds base URL: `https://yourdomain.com/media/image-123.jpg`
- * 3. Next.js Image optimizes via remotePatterns: `/_next/image?url=...&w=1200&q=75`
- *
- * If your storage/plugin returns **external CDN URLs** (e.g. `https://cdn.example.com/...`),
- * choose ONE of the following:
- * A) Allow the remote host in next.config.js:
- * images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }
- * B) Provide a **custom loader** for CDN-specific transforms:
- * const imageLoader: ImageLoader = ({ src, width, quality }) =>
- * `https://cdn.example.com${src}?w=${width}&q=${quality ?? 75}`
- *
- * C) Skip optimization:
- *
- *
- * TL;DR: Template uses relative URLs + getMediaUrl() to construct full URLs, then relies on
- * remotePatterns for optimization. Only add `loader` if using external CDNs with custom transforms.
- */
-
export const ImageMedia: React.FC
= (props) => {
const {
alt: altFromProps,
@@ -61,28 +22,23 @@ export const ImageMedia: React.FC = (props) => {
let width: number | undefined
let height: number | undefined
let alt = altFromProps
- let src: StaticImageData | string = srcFromProps || ''
+ let src: string = srcFromProps || ''
if (!src && resource && typeof resource === 'object') {
const { alt: altFromResource, height: fullHeight, url, width: fullWidth } = resource
- width = fullWidth!
- height = fullHeight!
+ width = fullWidth || undefined
+ height = fullHeight || undefined
alt = altFromResource || ''
- const cacheTag = resource.updatedAt
-
- src = getMediaUrl(url, cacheTag)
+ src = getMediaUrl(url)
}
+ if (!src) return null
+
const loading = loadingFromProps || (!priority ? 'lazy' : undefined)
- // NOTE: this is used by the browser to determine which image to download at different screen sizes
- const sizes = sizeFromProps
- ? sizeFromProps
- : Object.entries(breakpoints)
- .map(([, value]) => `(max-width: ${value}px) ${value * 2}w`)
- .join(', ')
+ const sizes = sizeFromProps || '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
return (
@@ -91,10 +47,8 @@ export const ImageMedia: React.FC = (props) => {
className={cn(imgClassName)}
fill={fill}
height={!fill ? height : undefined}
- placeholder="blur"
- blurDataURL={placeholderBlur}
priority={priority}
- quality={100}
+ quality={75}
loading={loading}
sizes={sizes}
src={src}