perf/a11y: replace raw img tags, fix alt text, aria-hidden, avif/webp

This commit is contained in:
Jeffrey 2026-03-13 13:39:16 +01:00
parent 15c3194eb6
commit 534a5644bf
7 changed files with 27 additions and 10 deletions

View File

@ -77,6 +77,8 @@ const nextConfig = {
cpus: 1, cpus: 1,
}, },
images: { images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 3600,
remotePatterns: [ remotePatterns: [
...[NEXT_PUBLIC_SERVER_URL].map((item) => { ...[NEXT_PUBLIC_SERVER_URL].map((item) => {
const url = new URL(item) const url = new URL(item)
@ -85,6 +87,8 @@ const nextConfig = {
protocol: url.protocol.replace(':', ''), protocol: url.protocol.replace(':', ''),
} }
}), }),
{ hostname: 'img.youtube.com', protocol: 'https' },
{ hostname: 'i.vimeocdn.com', protocol: 'https' },
], ],
}, },
webpack: (webpackConfig) => { webpack: (webpackConfig) => {

View File

@ -72,7 +72,7 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
priority priority
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
sizes="100vw" sizes="100vw"
fallbackAlt="" fallbackAlt={heading || ''}
/> />
<div className={`absolute inset-0 ${overlayClass}`} aria-hidden="true" /> <div className={`absolute inset-0 ${overlayClass}`} aria-hidden="true" />
</> </>

View File

@ -83,6 +83,7 @@ export const FDLinkCardsBlockComponent: React.FC<Props> = ({
className="w-12 h-12 md:w-16 md:h-16 object-contain" className="w-12 h-12 md:w-16 md:h-16 object-contain"
sizes="64px" sizes="64px"
fallbackAlt="" fallbackAlt=""
aria-hidden="true"
/> />
)} )}
<h2 className={`font-joey-heavy text-fd-h1 max-w-[700px] ${hClr}`}> <h2 className={`font-joey-heavy text-fd-h1 max-w-[700px] ${hClr}`}>

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import type { FDLocationsGridBlock as Props, Media } from '@/payload-types' import type { FDLocationsGridBlock as Props, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { fdCardRadius, fdContainer} from '@/utilities/fdTheme' import { fdCardRadius, fdContainer} from '@/utilities/fdTheme'
export const FDLocationsGridBlockComponent: React.FC<Props> = ({ export const FDLocationsGridBlockComponent: React.FC<Props> = ({
@ -65,10 +66,13 @@ export const FDLocationsGridBlockComponent: React.FC<Props> = ({
const inner = ( const inner = (
<> <>
{media?.url && ( {media?.url && (
<img <FDImage
src={media.url} media={media}
alt={(media as any).alt || card.locationName} size="large"
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
fallbackAlt={card.locationName || ''}
/> />
)} )}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent transition-opacity duration-300 group-hover:opacity-0" /> <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent transition-opacity duration-300 group-hover:opacity-0" />

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import type { FDPartnersLogosBlock as FDPartnersLogosBlockProps, Media } from '@/payload-types' import type { FDPartnersLogosBlock as FDPartnersLogosBlockProps, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { fdContainer } from '@/utilities/fdTheme' import { fdContainer } from '@/utilities/fdTheme'
export const FDPartnersLogosBlockComponent: React.FC<FDPartnersLogosBlockProps> = ({ export const FDPartnersLogosBlockComponent: React.FC<FDPartnersLogosBlockProps> = ({
@ -39,10 +40,12 @@ export const FDPartnersLogosBlockComponent: React.FC<FDPartnersLogosBlockProps>
if (!media?.url) return null if (!media?.url) return null
const logoEl = ( const logoEl = (
<img <FDImage
src={media.url} media={media}
alt={item.alt || ''} size="medium"
className={`h-14 sm:h-12 md:h-16 lg:h-[72px] w-auto max-w-[160px] sm:max-w-[180px] md:max-w-[220px] object-contain ${imgFilter}`} className={`h-14 sm:h-12 md:h-16 lg:h-[72px] w-auto max-w-[160px] sm:max-w-[180px] md:max-w-[220px] object-contain ${imgFilter}`}
sizes="220px"
fallbackAlt={item.alt || ''}
/> />
) )

View File

@ -18,6 +18,8 @@ interface FDImageProps {
sizes?: string sizes?: string
/** Fallback alt text if media has none */ /** Fallback alt text if media has none */
fallbackAlt?: string fallbackAlt?: string
/** Hide from assistive tech (decorative images) */
'aria-hidden'?: boolean | 'true' | 'false'
} }
export const FDImage: React.FC<FDImageProps> = ({ export const FDImage: React.FC<FDImageProps> = ({
@ -28,6 +30,7 @@ export const FDImage: React.FC<FDImageProps> = ({
priority = false, priority = false,
sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px', sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px',
fallbackAlt = '', fallbackAlt = '',
'aria-hidden': ariaHidden,
}) => { }) => {
// If media is not a populated object, bail // If media is not a populated object, bail
if (!media || typeof media === 'string' || typeof media === 'number') return null if (!media || typeof media === 'string' || typeof media === 'number') return null
@ -47,7 +50,7 @@ export const FDImage: React.FC<FDImageProps> = ({
if (isSvg) { if (isSvg) {
return ( return (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt} className={className} loading="lazy" /> <img src={src} alt={alt} className={className} loading="lazy" aria-hidden={ariaHidden} />
) )
} }
@ -60,6 +63,7 @@ export const FDImage: React.FC<FDImageProps> = ({
className={className} className={className}
priority={priority} priority={priority}
sizes={sizes} sizes={sizes}
aria-hidden={ariaHidden}
/> />
) )
} }
@ -78,6 +82,7 @@ export const FDImage: React.FC<FDImageProps> = ({
className={className} className={className}
priority={priority} priority={priority}
sizes={sizes} sizes={sizes}
aria-hidden={ariaHidden}
/> />
) )
} }

File diff suppressed because one or more lines are too long