Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
SearchProvider,
} from '@faststore/sdk'
import { BreadcrumbJsonLd, NextSeo } from 'next-seo'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useMemo } from 'react'

Expand All @@ -15,12 +14,13 @@ import type {
ServerManyProductsQueryQueryVariables,
} from '@generated/graphql'
import { ITEMS_PER_PAGE } from 'src/constants'
import { getCriticalProductImagePreload } from 'src/sdk/head/getCriticalProductImagePreload'
import useServerHeadComponents from 'src/sdk/head/useServerHeadComponents'
import { useApplySearchState } from 'src/sdk/search/state'

import type { PLPContentType } from 'src/server/cms/plp'

import storeConfig from '../../../../discovery.config'
import { faststoreLoader } from 'src/components/ui/Image/loader'
import ProductListing from './ProductListing'
import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'

Expand Down Expand Up @@ -131,15 +131,12 @@ export default function ProductListingPage({
// 30vw × 412 × 2 = 247px → browser picks 320 (first step ≥ 247 in the srcset).
// Using 320 here makes the preload URL exactly match the <img> srcset selection,
// so the browser can reuse the preloaded response instead of fetching a second URL.
const rawLcpImageUrl: string | undefined =
const lcpImagePreload = getCriticalProductImagePreload(
server?.search?.products?.edges?.[0]?.node?.image?.[0]?.url
const lcpImageUrl = rawLcpImageUrl
? faststoreLoader({
src: rawLcpImageUrl,
width: 320,
quality: 75,
})
: undefined
)
const serverHeadComponents = useServerHeadComponents(
lcpImagePreload ? <link {...lcpImagePreload} /> : null
)

return (
<SearchProvider
Expand All @@ -148,16 +145,7 @@ export default function ProductListingPage({
shouldResetInfiniteScroll={!storeConfig.experimental?.scrollRestoration}
{...searchParams}
>
{lcpImageUrl && (
<Head>
<link
rel="preload"
as="image"
href={lcpImageUrl}
fetchPriority="high"
/>
</Head>
)}
{serverHeadComponents}
{/* SEO */}
<NextSeo
title={title}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/ui/Logo/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
loading?: ImageProps['loading']
}

function Logo({ alt, src, loading = 'lazy' }: LogoProps) {
function Logo({ alt, src, loading = 'eager' }: LogoProps) {

Check notice on line 10 in packages/core/src/components/ui/Logo/Logo.tsx

View check run for this annotation

Sonar - Workflows / SonarQube Code Analysis

packages/core/src/components/ui/Logo/Logo.tsx#L10

Mark the props of the component as read-only.
if (!src) {
console.error('Logo image src is required.')
return null
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/sdk/head/getCriticalProductImagePreload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { faststoreLoader } from 'src/components/ui/Image/loader'

export interface CriticalProductImagePreload {
rel: 'preload'
as: 'image'
href: string
fetchPriority: 'high'
}

export function getCriticalProductImagePreload(
imageUrl?: string
): CriticalProductImagePreload | null {
if (!imageUrl) {
return null
}

return {
rel: 'preload',
as: 'image',
href: faststoreLoader({
src: imageUrl,
width: 320,
quality: 75,
}),
fetchPriority: 'high',
}
}
10 changes: 10 additions & 0 deletions packages/core/src/sdk/head/useServerHeadComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Head from 'next/head'
import type { ReactNode } from 'react'

export default function useServerHeadComponents(components: ReactNode) {
if (!components) {
return null
}

return <Head>{components}</Head>
}
40 changes: 40 additions & 0 deletions packages/core/test/components/ui/Logo.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render } from '@testing-library/react'

import Logo from 'src/components/ui/Logo/Logo'

const imageSpy = vi.fn()

vi.mock('src/components/ui/Image', () => ({
Image: (props: Record<string, unknown>) => {
imageSpy(props)
return <div data-testid="logo-image" />
},
}))

describe('Logo', () => {
beforeEach(() => {
imageSpy.mockClear()
})

it('uses eager loading by default', () => {
render(<Logo alt="FastStore" src="/logo.svg" />)

expect(imageSpy).toHaveBeenCalledTimes(1)
expect(imageSpy).toHaveBeenCalledWith(
expect.objectContaining({
loading: 'eager',
})
)
})

it('keeps explicit loading overrides', () => {
render(<Logo alt="FastStore" src="/logo.svg" loading="lazy" />)

expect(imageSpy).toHaveBeenCalledTimes(1)
expect(imageSpy).toHaveBeenCalledWith(
expect.objectContaining({
loading: 'lazy',
})
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getCriticalProductImagePreload } from 'src/sdk/head/getCriticalProductImagePreload'

describe('getCriticalProductImagePreload', () => {
it('returns null when no image URL is provided', () => {
expect(getCriticalProductImagePreload()).toBeNull()
})

it('builds preload metadata for a valid image URL', () => {
expect(getCriticalProductImagePreload('/product-image.jpg')).toEqual({
rel: 'preload',
as: 'image',
href: '/product-image.jpg',
fetchPriority: 'high',
})
})
})
Loading