Skip to content

Commit

Permalink
feat: search page with SSR (#2619)
Browse files Browse the repository at this point in the history
# (Don't merge!)

## What's the purpose of this pull request?

This POC is an alternative to indexing search pages using [server side
rendering](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props).
As the search page is dynamic and we need to set the title and
description based on the search term, we chose to use server side
rendering as it is the most suitable for this type of situation.

### Nextjs reference: 
**Like SSG,[ Server-Side
Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering)
(SSR) is pre-rendered, which also makes it great for SEO. Instead of
being generated at build time, as in SSG, SSR's HTML is generated at
request time. This is great for when you have pages that are very
dynamic.**

https://nextjs.org/learn-pages-router/seo/rendering-and-ranking/rendering-strategies

## How it works?

We use the getServerSideProps function instead of getStaticProps. We
added a cache header to the response so that it is possible to cache the
return in a CDN.




## How to test it?
Click on:
https://storeframework-cm652ufll028lmgv665a6xv0g-polemunmz.b.vtex.app/s?q=headphone
  • Loading branch information
pedromtec authored Jan 29, 2025
1 parent 03506c3 commit 6fd2d0d
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 66 deletions.
21 changes: 21 additions & 0 deletions packages/cli/src/utils/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,26 @@ function enableRedirectsMiddleware(basePath: string) {
}
}

function enableSearchSSR(basePath: string) {
const storeConfigPath = getCurrentUserStoreConfigFile(basePath)

if(!storeConfigPath) {
return
}
const storeConfig = require(storeConfigPath)
if(!storeConfig.experimental.enableSearchSSR) {
return
}

const { tmpDir } = withBasePath(basePath)
const searchPagePath = path.join(tmpDir, 'src', 'pages', 's.tsx')
const searchPageData = String(readFileSync(searchPagePath))

const searchPageWithSSR = searchPageData.replaceAll('getStaticProps', 'getServerSideProps')

writeFileSync(searchPagePath, searchPageWithSSR)
}

export async function generate(options: GenerateOptions) {
const { basePath, setup = false } = options

Expand All @@ -508,6 +528,7 @@ export async function generate(options: GenerateOptions) {
await Promise.all([
setupPromise,
checkDependencies(basePath, ['typescript']),
enableSearchSSR(basePath),
updateBuildTime(basePath),
copyUserStarterToCustomizations(basePath),
copyTheme(basePath),
Expand Down
7 changes: 6 additions & 1 deletion packages/core/discovery.config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ module.exports = {
author: 'Store Framework',
plp: {
titleTemplate: '%s | FastStore PLP',
descriptionTemplate: '%s products on FastStore Product Listing Page',
descriptionTemplate: '%s products on FastStore Product Listing Page'
},
search: {
titleTemplate: '%s: Search results title',
descriptionTemplate: '%s: Search results description',
},
},

Expand Down Expand Up @@ -106,5 +110,6 @@ module.exports = {
noRobots: false,
preact: false,
enableRedirects: false,
enableSearchSSR: false,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GetServerSideProps } from 'next'
import { SearchPageProps } from './getStaticProps'

import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
import { SearchContentType, getPage } from 'src/server/cms'
import { Locator } from '@vtex/client-cms'
import storeConfig from 'discovery.config'

export const getServerSideProps: GetServerSideProps<
SearchPageProps,
Record<string, string>,
Locator
> = async (context) => {
const { previewData, query, res } = context
const searchTerm = (query.q as string)?.split('+').join(' ')

const globalSections = await getGlobalSectionsData(previewData)

if (storeConfig.cms.data) {
const cmsData = JSON.parse(storeConfig.cms.data)
const page = cmsData['search'][0]
if (page) {
const pageData = await getPage<SearchContentType>({
contentType: 'search',
documentId: page.documentId,
versionId: page.versionId,
})
return {
props: { page: pageData, globalSections, searchTerm },
}
}
}

const page = await getPage<SearchContentType>({
...(previewData?.contentType === 'search' ? previewData : null),
contentType: 'search',
})

res.setHeader(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=31536000, stale-if-error=31536000'
) // 5 minutes of fresh content and 1 year of stale content

return {
props: {
page,
globalSections,
searchTerm,
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { GetStaticProps } from 'next'
import {
getGlobalSectionsData,
GlobalSectionsData,
} from 'src/components/cms/GlobalSections'
import { SearchContentType, getPage } from 'src/server/cms'
import { Locator } from '@vtex/client-cms'
import storeConfig from 'discovery.config'

export type SearchPageProps = {
page: SearchContentType
globalSections: GlobalSectionsData
searchTerm?: string
}

/*
Depending on the value of the storeConfig.experimental.enableSearchSSR flag, the function used will be getServerSideProps (./getServerSideProps).
Our CLI that does this process of converting from getStaticProps to getServerSideProps.
*/
export const getStaticProps: GetStaticProps<
SearchPageProps,
Record<string, string>,
Locator
> = async (context) => {
const { previewData } = context

const globalSections = await getGlobalSectionsData(previewData)

if (storeConfig.cms.data) {
const cmsData = JSON.parse(storeConfig.cms.data)
const page = cmsData['search'][0]

if (page) {
const pageData = await getPage<SearchContentType>({
contentType: 'search',
documentId: page.documentId,
versionId: page.versionId,
})

return {
props: { page: pageData, globalSections },
}
}
}

const page = await getPage<SearchContentType>({
...(previewData?.contentType === 'search' ? previewData : null),
contentType: 'search',
})

return {
props: {
page,
globalSections,
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './getServerSideProps'
export * from './getStaticProps'
126 changes: 61 additions & 65 deletions packages/core/src/pages/s.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { GetStaticProps } from 'next'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
Expand All @@ -14,19 +13,13 @@ import { SROnly as UISROnly } from '@faststore/ui'
import { ITEMS_PER_PAGE } from 'src/constants'
import { useApplySearchState } from 'src/sdk/search/state'

import { Locator } from '@vtex/client-cms'
import storeConfig from 'discovery.config'
import {
getGlobalSectionsData,
GlobalSectionsData,
} from 'src/components/cms/GlobalSections'
import { SearchWrapper } from 'src/components/templates/SearchPage'
import { getPage, SearchContentType } from 'src/server/cms'

type Props = {
page: SearchContentType
globalSections: GlobalSectionsData
}
import { SearchWrapper } from 'src/components/templates/SearchPage'
import {
getStaticProps,
SearchPageProps,
} from 'src/experimental/searchServerSideFunctions'

export interface SearchPageContextType {
title: string
Expand Down Expand Up @@ -54,41 +47,79 @@ const useSearchParams = ({
}, [asPath, defaultSort])
}

function Page({ page: searchContentType, globalSections }: Props) {
type StoreConfig = typeof storeConfig

function generateSEOData(storeConfig: StoreConfig, searchTerm?: string) {
const { search: searchSeo, ...seo } = storeConfig.seo

const isSSREnabled = storeConfig.experimental.enableSearchSSR

// default behavior without SSR
if (!isSSREnabled) {
return {
title: seo.title,
description: seo.description,
titleTemplate: seo.titleTemplate,
openGraph: {
type: 'website',
title: seo.title,
description: seo.description,
},
}
}

const title = searchTerm ?? 'Search Results'
const titleTemplate = searchSeo?.titleTemplate ?? seo.titleTemplate
const description = searchSeo?.descriptionTemplate
? searchSeo.descriptionTemplate.replace(/%s/g, () => searchTerm)
: seo.description

const canonical = searchTerm
? `${storeConfig.storeUrl}/s?q=${searchTerm}`
: undefined

return {
title,
description,
titleTemplate,
canonical,
openGraph: {
type: 'website',
title: title,
description: description,
},
}
}

function Page({
page: searchContentType,
globalSections,
searchTerm,
}: SearchPageProps) {
const { settings } = searchContentType
const applySearchState = useApplySearchState()
const searchParams = useSearchParams({
sort: settings?.productGallery?.sortBySelection as SearchState['sort'],
})

const title = 'Search Results'
const { description, titleTemplate } = storeConfig.seo
const itemsPerPage = settings?.productGallery?.itemsPerPage ?? ITEMS_PER_PAGE

if (!searchParams) {
return null
}

const seoData = generateSEOData(storeConfig, searchTerm)

return (
<SearchProvider
onChange={applySearchState}
itemsPerPage={itemsPerPage}
{...searchParams}
>
{/* SEO */}
<NextSeo
noindex
title={title}
description={description}
titleTemplate={titleTemplate}
openGraph={{
type: 'website',
title,
description,
}}
/>
<NextSeo noindex {...seoData} />

<UISROnly text={title} />
<UISROnly text={seoData.title} />

{/*
WARNING: Do not import or render components from any
Expand All @@ -105,50 +136,15 @@ function Page({ page: searchContentType, globalSections }: Props) {
itemsPerPage={itemsPerPage}
searchContentType={searchContentType}
serverData={{
title,
searchTerm: searchParams.term ?? undefined,
title: seoData.title,
searchTerm: searchTerm ?? searchParams.term ?? undefined,
}}
globalSections={globalSections.sections}
/>
</SearchProvider>
)
}

export const getStaticProps: GetStaticProps<
Props,
Record<string, string>,
Locator
> = async ({ previewData }) => {
const globalSections = await getGlobalSectionsData(previewData)

if (storeConfig.cms.data) {
const cmsData = JSON.parse(storeConfig.cms.data)
const page = cmsData['search'][0]

if (page) {
const pageData = await getPage<SearchContentType>({
contentType: 'search',
documentId: page.documentId,
versionId: page.versionId,
})

return {
props: { page: pageData, globalSections },
}
}
}

const page = await getPage<SearchContentType>({
...(previewData?.contentType === 'search' ? previewData : null),
contentType: 'search',
})

return {
props: {
page,
globalSections,
},
}
}
export { getStaticProps }

export default Page

0 comments on commit 6fd2d0d

Please sign in to comment.