diff --git a/packages/cli/src/utils/generate.ts b/packages/cli/src/utils/generate.ts index 16ca0bf065..8b7093b75c 100644 --- a/packages/cli/src/utils/generate.ts +++ b/packages/cli/src/utils/generate.ts @@ -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 @@ -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), diff --git a/packages/core/discovery.config.default.js b/packages/core/discovery.config.default.js index 7fc9631f7f..80019b6fda 100644 --- a/packages/core/discovery.config.default.js +++ b/packages/core/discovery.config.default.js @@ -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', }, }, @@ -106,5 +110,6 @@ module.exports = { noRobots: false, preact: false, enableRedirects: false, + enableSearchSSR: false, }, } diff --git a/packages/core/src/experimental/searchServerSideFunctions/getServerSideProps.ts b/packages/core/src/experimental/searchServerSideFunctions/getServerSideProps.ts new file mode 100644 index 0000000000..40c652fa2a --- /dev/null +++ b/packages/core/src/experimental/searchServerSideFunctions/getServerSideProps.ts @@ -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, + 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({ + contentType: 'search', + documentId: page.documentId, + versionId: page.versionId, + }) + return { + props: { page: pageData, globalSections, searchTerm }, + } + } + } + + const page = await getPage({ + ...(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, + }, + } +} diff --git a/packages/core/src/experimental/searchServerSideFunctions/getStaticProps.ts b/packages/core/src/experimental/searchServerSideFunctions/getStaticProps.ts new file mode 100644 index 0000000000..5e6a805377 --- /dev/null +++ b/packages/core/src/experimental/searchServerSideFunctions/getStaticProps.ts @@ -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, + 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({ + contentType: 'search', + documentId: page.documentId, + versionId: page.versionId, + }) + + return { + props: { page: pageData, globalSections }, + } + } + } + + const page = await getPage({ + ...(previewData?.contentType === 'search' ? previewData : null), + contentType: 'search', + }) + + return { + props: { + page, + globalSections, + }, + } +} diff --git a/packages/core/src/experimental/searchServerSideFunctions/index.ts b/packages/core/src/experimental/searchServerSideFunctions/index.ts new file mode 100644 index 0000000000..962debee2b --- /dev/null +++ b/packages/core/src/experimental/searchServerSideFunctions/index.ts @@ -0,0 +1,2 @@ +export * from './getServerSideProps' +export * from './getStaticProps' diff --git a/packages/core/src/pages/s.tsx b/packages/core/src/pages/s.tsx index 5e8187ff60..2e01d8f1dc 100644 --- a/packages/core/src/pages/s.tsx +++ b/packages/core/src/pages/s.tsx @@ -1,4 +1,3 @@ -import type { GetStaticProps } from 'next' import { NextSeo } from 'next-seo' import { useRouter } from 'next/router' import { useMemo } from 'react' @@ -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 @@ -54,21 +47,69 @@ 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 ( {/* SEO */} - + - + {/* WARNING: Do not import or render components from any @@ -105,8 +136,8 @@ 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} /> @@ -114,41 +145,6 @@ function Page({ page: searchContentType, globalSections }: Props) { ) } -export const getStaticProps: GetStaticProps< - Props, - Record, - 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({ - contentType: 'search', - documentId: page.documentId, - versionId: page.versionId, - }) - - return { - props: { page: pageData, globalSections }, - } - } - } - - const page = await getPage({ - ...(previewData?.contentType === 'search' ? previewData : null), - contentType: 'search', - }) - - return { - props: { - page, - globalSections, - }, - } -} +export { getStaticProps } export default Page