Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search page with SSR #2619

Merged
merged 8 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
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')
pedromtec marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of next are vulnerable to Acceptance of Extraneous Untrusted Data With Trusted Data / Authorization Bypass Through User-Controlled Key.

Fix: Upgrade this library to at least version 13.5.7 at faststore/yarn.lock:13689.

Reference(s): GHSA-gp8f-8m3g-qvj9, CVE-2024-46982

💬 To ignore this, reply with:
/fp <comment> for false positive
/ar <comment> for acceptable risk
/other <comment> for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by ssc-adb055b9-fed0-4d70-a57d-eb9825b09449.


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}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add the canonical when isSSREnabled as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have access to search term in getStaticProps

: 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
Loading