diff --git a/.changeset/big-bottles-learn.md b/.changeset/big-bottles-learn.md new file mode 100644 index 0000000000..b92595972b --- /dev/null +++ b/.changeset/big-bottles-learn.md @@ -0,0 +1,5 @@ +--- +"nextjs-website": patch +--- + +Refactor metadata caching to use generic types and improve locale support diff --git a/apps/nextjs-website/src/app/[locale]/[productSlug]/guides/[...productGuidePage]/page.tsx b/apps/nextjs-website/src/app/[locale]/[productSlug]/guides/[...productGuidePage]/page.tsx index dc85e8377c..e97f5a0e1a 100644 --- a/apps/nextjs-website/src/app/[locale]/[productSlug]/guides/[...productGuidePage]/page.tsx +++ b/apps/nextjs-website/src/app/[locale]/[productSlug]/guides/[...productGuidePage]/page.tsx @@ -51,11 +51,11 @@ export type ProductGuidePageProps = { export async function generateMetadata(props: { params: Promise; }): Promise { - const params = await props.params; + const { locale, productGuidePage, productSlug } = await props.params; const guidePageProps = await getGuidePage( - params?.productGuidePage ?? [''], - params?.locale, - params?.productSlug + productGuidePage ?? [''], + locale, + productSlug ); if (guidePageProps?.seo) { diff --git a/apps/nextjs-website/src/helpers/s3Metadata.helpers.ts b/apps/nextjs-website/src/helpers/s3Metadata.helpers.ts index eb8afca9ad..23fbd151cb 100644 --- a/apps/nextjs-website/src/helpers/s3Metadata.helpers.ts +++ b/apps/nextjs-website/src/helpers/s3Metadata.helpers.ts @@ -205,47 +205,72 @@ const S3_SOAP_API_METADATA_JSON_PATH = process.env.S3_SOAP_API_METADATA_JSON_PATH || 'soap-api/soap-api-metadata.json'; -let guidesMetadataCache: readonly JsonMetadata[] | null = null; -let solutionsMetadataCache: readonly JsonMetadata[] | null = null; -let releaseNotesMetadataCache: readonly JsonMetadata[] | null = null; -let soapApiMetadataCache: readonly SoapApiJsonMetadata[] | null = null; - -// Add timestamp-based cache invalidation -// eslint-disable-next-line functional/no-let -let guidesMetadataCacheTime = 0; - -// eslint-disable-next-line functional/no-let -let solutionsMetadataCacheTime = 0; +type MetadataCacheItem = { + readonly category: string; + readonly locale: string; + readonly data: readonly T[] | null; + readonly refreshTime: number; +}; -// eslint-disable-next-line functional/no-let -let releaseNotesMetadataCacheTime = 0; +let metadataCache: readonly MetadataCacheItem>[] = []; const METADATA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes -export const getGuidesMetadata = async (locale: string, dirName?: string) => { +async function fetchMetadataWithCache( + locale: string, + metadataCategory: string, + fetchFunction: () => Promise, + dirName?: string +): Promise> { const now = Date.now(); - - if ( - guidesMetadataCache && - now - guidesMetadataCacheTime < METADATA_CACHE_TTL && - (!dirName || guidesMetadataCache.some((m) => m.dirName === dirName)) - ) { - return guidesMetadataCache; + const cacheResult = metadataCache.find((item) => { + const categoryMatch = item.category === metadataCategory; + const localeMatch = item.locale === locale; + const timeMatch = item.data && now - item.refreshTime < METADATA_CACHE_TTL; + const dirNameMatch = + !dirName || + (Array.isArray(item.data) && + item.data.length > 0 && + 'dirName' in item.data[0] && + item.data.some((m: Record) => m.dirName === dirName)); + return categoryMatch && localeMatch && timeMatch && dirNameMatch; + }) as MetadataCacheItem | undefined; + + if (cacheResult) { + return cacheResult; } - guidesMetadataCache = await fetchMetadataFromCDN( + const fetchMetadataResult = await fetchFunction(); + + const newCacheItem: MetadataCacheItem = { + category: metadataCategory, + locale, + data: fetchMetadataResult, + refreshTime: now, + }; + + metadataCache = [ + ...metadataCache.filter( + (item) => !(item.category === metadataCategory && item.locale === locale) + ), + newCacheItem, + ]; + + return newCacheItem; +} + +export const getGuidesMetadata = async (locale: string, dirName?: string) => { + const fetchFromCdnPath = dirName + ? path.join(locale, S3_PATH_TO_GITBOOK_DOCS, dirName, S3_METADATA_JSON_PATH) + : `${locale}/${S3_GUIDES_METADATA_JSON_PATH}`; + const cacheResult = await fetchMetadataWithCache( + locale, + 'guides', + () => fetchMetadataFromCDN(fetchFromCdnPath), dirName - ? path.join( - locale, - S3_PATH_TO_GITBOOK_DOCS, - dirName, - S3_METADATA_JSON_PATH - ) - : `${locale}/${S3_GUIDES_METADATA_JSON_PATH}` ); - guidesMetadataCacheTime = now; - return guidesMetadataCache || []; + return cacheResult.data || []; }; const removeTrailingSlash = (value: string) => value.replace(/\/+$/, ''); @@ -324,29 +349,18 @@ export const getSolutionsMetadata = async ( locale: string, dirName?: string ) => { - const now = Date.now(); - - if ( - solutionsMetadataCache && - now - solutionsMetadataCacheTime < METADATA_CACHE_TTL && - (!dirName || solutionsMetadataCache.some((m) => m.dirName === dirName)) - ) { - return solutionsMetadataCache; - } - - solutionsMetadataCache = await fetchMetadataFromCDN( + const fetchFromCdnPath = dirName + ? path.join(locale, S3_PATH_TO_GITBOOK_DOCS, dirName, S3_METADATA_JSON_PATH) + : `${locale}/${S3_SOLUTIONS_METADATA_JSON_PATH}`; + + const cacheResult = await fetchMetadataWithCache( + locale, + 'solutions', + () => fetchMetadataFromCDN(fetchFromCdnPath), dirName - ? path.join( - locale, - S3_PATH_TO_GITBOOK_DOCS, - dirName, - S3_METADATA_JSON_PATH - ) - : `${locale}/${S3_SOLUTIONS_METADATA_JSON_PATH}` ); - solutionsMetadataCacheTime = now; - return solutionsMetadataCache || []; + return cacheResult.data || []; }; export const getReleaseNotesMetadataByDirNames = async ( @@ -368,36 +382,29 @@ export const getReleaseNotesMetadata = async ( locale: string, dirName?: string ) => { - const now = Date.now(); - - if ( - releaseNotesMetadataCache && - now - releaseNotesMetadataCacheTime < METADATA_CACHE_TTL && - (!dirName || releaseNotesMetadataCache.some((m) => m.dirName === dirName)) - ) { - return releaseNotesMetadataCache; - } - - releaseNotesMetadataCache = await fetchMetadataFromCDN( + const fetchFromCdnPath = dirName + ? path.join(locale, S3_PATH_TO_GITBOOK_DOCS, dirName, S3_METADATA_JSON_PATH) + : `${locale}/${S3_RELEASE_NOTES_METADATA_JSON_PATH}`; + + const cacheResult = await fetchMetadataWithCache( + locale, + 'releaseNotes', + () => fetchMetadataFromCDN(fetchFromCdnPath), dirName - ? path.join( - locale, - S3_PATH_TO_GITBOOK_DOCS, - dirName, - S3_METADATA_JSON_PATH - ) - : `${locale}/${S3_RELEASE_NOTES_METADATA_JSON_PATH}` ); - releaseNotesMetadataCacheTime = now; - return releaseNotesMetadataCache || []; + return cacheResult.data || []; }; export const getSoapApiMetadata = async (locale: string) => { - if (!soapApiMetadataCache) { - soapApiMetadataCache = await fetchMetadataFromCDN( - `${locale}/${S3_SOAP_API_METADATA_JSON_PATH}` - ); - } - return soapApiMetadataCache || []; + const cacheResult = await fetchMetadataWithCache( + locale, + 'soapApi', + () => + fetchMetadataFromCDN( + `${locale}/${S3_SOAP_API_METADATA_JSON_PATH}` + ) + ); + + return cacheResult.data || []; }; diff --git a/apps/nextjs-website/src/lib/api.ts b/apps/nextjs-website/src/lib/api.ts index e82ff1d49f..6e4c3c16b7 100644 --- a/apps/nextjs-website/src/lib/api.ts +++ b/apps/nextjs-website/src/lib/api.ts @@ -66,7 +66,6 @@ export async function getGuidePage( locale, productSlug ), - getGuidesMetadata(locale), ]); // Path construction @@ -82,6 +81,7 @@ export async function getGuidePage( }); const guidesMetadata = await getGuidesMetadata( + locale, guideToFind ? guideToFind.dirName : '' ); return manageUndefined(