From 33a30c5169c3659da08e0d099f20aaea2e6169c6 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Tue, 14 Feb 2023 17:26:16 -0500 Subject: [PATCH 1/6] Ensure SSG always includes site prefix with multisite on --- .../plugins/graphql-sitemap-service.ts | 34 ++++ .../plugins/graphql-sitemap-service.ts | 7 +- .../services/graphql-sitemap-service.test.ts | 173 +++++++++++++++--- .../src/services/graphql-sitemap-service.ts | 85 +++++---- .../sitemapServiceMultisiteResult.ts | 26 +++ ...t.ts => sitemapServiceSinglesiteResult.ts} | 0 6 files changed, 270 insertions(+), 55 deletions(-) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts create mode 100644 packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts rename packages/sitecore-jss-nextjs/src/test-data/{sitemapServiceResult.ts => sitemapServiceSinglesiteResult.ts} (100%) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts new file mode 100644 index 0000000000..b5e67c0ea4 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -0,0 +1,34 @@ +import { + GraphQLSitemapService, + StaticPath, + constants, + SiteInfo, +} from '@sitecore-jss/sitecore-jss-nextjs'; +import config from 'temp/config'; +import { SitemapFetcherPlugin } from '..'; +import { GetStaticPathsContext } from 'next'; +import { siteResolver } from 'lib/site-resolver'; + +class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { + _graphqlSitemapService: GraphQLSitemapService; + + constructor() { + this._graphqlSitemapService = new GraphQLSitemapService({ + endpoint: config.graphQLEndpoint, + apiKey: config.sitecoreApiKey, + sites: siteResolver.sites.map((site: SiteInfo) => site.name), + }); + } + + async exec(context?: GetStaticPathsContext): Promise { + if (process.env.EXPORT_MODE) { + // Disconnected Export mode + if (process.env.JSS_MODE !== constants.JSS_MODE.DISCONNECTED) { + return this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage); + } + } + return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + } +} + +export const graphqlSitemapServicePlugin = new GraphqlSitemapServicePlugin(); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index b5e67c0ea4..0614f9946e 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -24,10 +24,13 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { if (process.env.EXPORT_MODE) { // Disconnected Export mode if (process.env.JSS_MODE !== constants.JSS_MODE.DISCONNECTED) { - return this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage); + return this._graphqlSitemapService.fetchExportSitemap( + config.defaultLanguage, + config.jssAppName + ); } } - return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || [], config.jssAppName); } } diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index f517e37496..443c76a899 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -8,7 +8,8 @@ import { } from './graphql-sitemap-service'; import sitemapDefaultQueryResult from '../test-data/sitemapDefaultQueryResult.json'; import sitemapPersonalizeQueryResult from '../test-data/sitemapPersonalizeQueryResult.json'; -import sitemapServiceResult from '../test-data/sitemapServiceResult'; +import sitemapServiceMultisiteResult from '../test-data/sitemapServiceMultisiteResult'; +import sitemapServiceSinglesiteResult from '../test-data/sitemapServiceSinglesiteResult'; import { GraphQLClient, GraphQLRequestClient } from '@sitecore-jss/sitecore-jss/graphql'; class TestService extends GraphQLSitemapService { @@ -88,7 +89,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); return expect(nock.isDone()).to.be.true; }); @@ -164,14 +165,113 @@ describe('GraphQLSitemapService', () => { return expect(sitemap).to.deep.equal([ { params: { - path: ['y1', 'y2'], + path: ['_site_site-name', 'y1', 'y2'], }, locale: 'en', }, ]); }); - it('should return personalized paths when personalize data is requested and returned', async () => { + describe('Fetch sitemap in SSG mode', () => { + it('should work when 1 language is requested', async () => { + mockPathsRequest(); + + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua']); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + + return expect(nock.isDone()).to.be.true; + }); + + it('should work for single site when 1 language is requested', async () => { + mockPathsRequest(); + + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua'], sites[0]); + expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); + + return expect(nock.isDone()).to.be.true; + }); + + it('should work when includePaths and excludePaths are provided', async () => { + const includedPaths = ['/y1/']; + const excludedPaths = ['/y1/y2/y3/y4']; + + nock(endpoint) + .post('/', (body) => { + return body.variables.includedPaths && body.variables.excludedPaths; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 1, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/y1/y2/', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/') + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites, + includedPaths, + excludedPaths, + }); + const sitemap = await service.fetchSSGSitemap(['en']); + + return expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + locale: 'en', + }, + ]); + }); + + it('should return personalized paths when personalize data is requested and returned for single site', async () => { const lang = 'ua'; nock(endpoint) @@ -184,7 +284,7 @@ describe('GraphQLSitemapService', () => { sites, includePersonalizedRoutes: true, }); - const sitemap = await service.fetchSSGSitemap([lang]); + const sitemap = await service.fetchSSGSitemap([lang], 'site-name'); expect(sitemap).to.deep.equal([ { @@ -533,49 +633,49 @@ describe('GraphQLSitemapService', () => { expect(sitemap).to.deep.equal([ { params: { - path: [''], + path: ['_site_site-name'], }, locale: 'ua', }, { params: { - path: ['x1'], + path: ['_site_site-name', 'x1'], }, locale: 'ua', }, { params: { - path: ['y1', 'y2', 'y3', 'y4'], + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], }, locale: 'ua', }, { params: { - path: ['y1', 'y2'], + path: ['_site_site-name', 'y1', 'y2'], }, locale: 'ua', }, { params: { - path: [''], + path: ['_site_site-name'], }, locale: 'da-DK', }, { params: { - path: ['x1-da-DK'], + path: ['_site_site-name', 'x1-da-DK'], }, locale: 'da-DK', }, { params: { - path: ['y1', 'y2', 'y3', 'y4-da-DK'], + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4-da-DK'], }, locale: 'da-DK', }, { params: { - path: ['y1', 'y2-da-DK'], + path: ['_site_site-name', 'y1', 'y2-da-DK'], }, locale: 'da-DK', }, @@ -622,13 +722,13 @@ describe('GraphQLSitemapService', () => { expect(sitemap).to.deep.equal([ { params: { - path: [''], + path: ['_site_site-name'], }, locale: 'en', }, { params: { - path: ['x1'], + path: ['_site_site-name', 'x1'], }, locale: 'en', }, @@ -688,7 +788,7 @@ describe('GraphQLSitemapService', () => { }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); return expect(nock.isDone()).to.be.true; }); @@ -709,7 +809,7 @@ describe('GraphQLSitemapService', () => { }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); return expect(nock.isDone()).to.be.true; }); @@ -735,8 +835,32 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); }); +}); + + const expectedMultisiteExportSitemap = [ + { + params: { + path: ['_site_site-name'], + }, + }, + { + params: { + path: ['_site_site-name', 'x1'], + }, + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], + }, + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + }, + ]; - const expectedExportSitemap = [ + const expectedSinglesiteExportSitemap = [ { params: { path: [''], @@ -760,11 +884,18 @@ describe('GraphQLSitemapService', () => { ]; describe('Fetch sitemap in export mode', () => { - it('should fetch sitemap', async () => { + it('should fetch multisite sitemap', async () => { mockPathsRequest(); const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchExportSitemap('ua'); - expect(sitemap).to.deep.equal(expectedExportSitemap); + expect(sitemap).to.deep.equal(expectedMultisiteExportSitemap); + return expect(nock.isDone()).to.be.true; + }); + it('should fetch singlesite sitemap', async () => { + mockPathsRequest(); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchExportSitemap('ua',sites[0]); + expect(sitemap).to.deep.equal(expectedSinglesiteExportSitemap); return expect(nock.isDone()).to.be.true; }); it('should work if endpoint returns 0 pages', async () => { @@ -824,4 +955,4 @@ describe('GraphQLSitemapService', () => { // eslint-disable-next-line no-unused-expressions expect(graphQLRequestClient).to.exist; }); -}); +}); \ No newline at end of file diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 3a7d18e164..6ab69fb3c4 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -140,7 +140,7 @@ export interface GraphQLSitemapServiceConfig /** * Names of the configured sites */ - sites: string[]; + sites?: string[]; /** * A flag for whether to include personalized routes in service output - only works on XM Cloud @@ -161,6 +161,7 @@ export type StaticPath = { /** * Service that fetches the list of site pages using Sitecore's GraphQL API. + * Used to handle multiple sites by default but can retrieve paths for an individual site * This list is used for SSG and Export functionality. * @mixes SearchQueryService */ @@ -187,24 +188,26 @@ export class GraphQLSitemapService { * The `locale` parameter will be used in the item query, but since i18n is not supported, * the output paths will not include a `language` property. * @param {string} locale which application supports + * @param {string | undefined} siteName used when only paths for a single site are needed, without site prefix * @returns an array of @see StaticPath objects */ - async fetchExportSitemap(locale: string): Promise { + async fetchExportSitemap(locale: string, siteName?: string): Promise { const formatPath = (path: string[]) => ({ params: { path, }, }); - return this.fetchSitemap([locale], formatPath); + return this.fetchSitemap([locale], formatPath, siteName); } /** * Fetch sitemap which could be used for generation of static pages using SSG mode * @param {string[]} locales locales which application supports + * @param {string | undefined} siteName used when only paths for a single site are needed, without site prefix * @returns an array of @see StaticPath objects */ - async fetchSSGSitemap(locales: string[]): Promise { + async fetchSSGSitemap(locales: string[], siteName?: string): Promise { const formatPath = (path: string[], locale: string) => ({ params: { path, @@ -212,7 +215,7 @@ export class GraphQLSitemapService { locale, }); - return this.fetchSitemap(locales, formatPath); + return this.fetchSitemap(locales, formatPath, siteName); } /** @@ -220,52 +223,70 @@ export class GraphQLSitemapService { * version in the specified language(s). * @param {string[]} languages Fetch pages that have versions in this language(s). * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. + * @param {string | undefined} singleSite used when only paths for a single site are needed, without site prefix * @returns list of pages * @throws {RangeError} if the list of languages is empty. * @throws {RangeError} if the any of the languages is an empty string. */ protected async fetchSitemap( languages: string[], - formatStaticPath: (path: string[], language: string) => StaticPath + formatStaticPath: (path: string[], language: string) => StaticPath, + singleSite?: string ): Promise { const paths = new Array(); if (!languages.length) { throw new RangeError(languageError); } - // Get all sites - const sites = this.options.sites; - if (!sites || !sites.length) { - throw new RangeError(sitesError); - } + if (singleSite) { + paths.push( + ...(await this.getTranformedPaths(singleSite, languages, formatStaticPath, false)) + ); + } else { + // Get all sites + const sites = this.options.sites; + if (!sites || !sites.length) { + throw new RangeError(sitesError); + } - // Fetch paths for each site - for (let i = 0; i < sites.length; i++) { - const siteName = sites[i]; - const multiSiteName = sites.length > 1 ? siteName : undefined; + // Fetch paths for each site + for (let i = 0; i < sites.length; i++) { + const siteName = sites[i]; - // Fetch paths using all locales - await Promise.all( - languages.map(async (language) => { - if (language === '') { - throw new RangeError(languageEmptyError); - } - debug.sitemap('fetching sitemap data for %s %s', language, siteName); - const results = await this.fetchLanguageSitePaths(language, siteName); - const transformedPaths = await this.transformLanguageSitePaths( - results, - formatStaticPath, - language, - multiSiteName - ); - paths.push(...transformedPaths); - }) - ); + // Fetch paths using all locales + paths.push(...(await this.getTranformedPaths(siteName, languages, formatStaticPath, true))); + } } return ([] as StaticPath[]).concat(...paths); } + protected async getTranformedPaths( + siteName: string, + languages: string[], + formatStaticPath: (path: string[], language: string) => StaticPath, + multisite?: boolean + ) { + const paths = new Array(); + await Promise.all( + languages.map(async (language) => { + if (language === '') { + throw new RangeError(languageEmptyError); + } + debug.sitemap('fetching sitemap data for %s %s', language, siteName); + const results = await this.fetchLanguageSitePaths(language, siteName); + const transformedPaths = await this.transformLanguageSitePaths( + results, + formatStaticPath, + language, + multisite ? siteName : undefined + ); + paths.push(...transformedPaths); + }) + ); + return paths; + } + protected async transformLanguageSitePaths( sitePaths: RouteListQueryResult[], formatStaticPath: (path: string[], language: string) => StaticPath, diff --git a/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts b/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts new file mode 100644 index 0000000000..ca70797110 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts @@ -0,0 +1,26 @@ +export default [ + { + params: { + path: ['_site_site-name'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name','x1'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name','y1', 'y2', 'y3', 'y4'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name','y1', 'y2'], + }, + locale: 'ua', + }, +]; diff --git a/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceResult.ts b/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceSinglesiteResult.ts similarity index 100% rename from packages/sitecore-jss-nextjs/src/test-data/sitemapServiceResult.ts rename to packages/sitecore-jss-nextjs/src/test-data/sitemapServiceSinglesiteResult.ts From d7b6ca8345f4d7af88acb3dd9f486ff35010ac78 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Tue, 14 Feb 2023 17:30:43 -0500 Subject: [PATCH 2/6] Changelog entry for GraphqlSitemapService --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a0848a40..53131002d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Our versioning strategy is as follows: * `import { editingDataService } from '@sitecore-jss/sitecore-jss-nextjs/editing';` * `import { EditingRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';` +* `[sitecore-jss-nextjs]` GraphqlSitemapService's constructor accepts a sites array by default, instead of siteName. fetchExportSitemap and fetchSSGSitemap methods will now return paths with site prefix applies by default. Provide a siteName parameter into the methods to get paths for a single site without prefix. + * `[sitecore-jss-angular][templates/angular]` jss-angular package and sample has been updated to version 14. This means several changes: * JSS Angular sample is now using Ivy * IE11 no longer supported by JSS Angular From 4b8604a251b1abe7266333df8a6d8470f70d3c04 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 15 Feb 2023 08:31:19 -0500 Subject: [PATCH 3/6] lint fix --- .../services/graphql-sitemap-service.test.ts | 950 +++++++++--------- .../sitemapServiceMultisiteResult.ts | 6 +- 2 files changed, 478 insertions(+), 478 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 443c76a899..fa65690ee1 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -175,28 +175,28 @@ describe('GraphQLSitemapService', () => { describe('Fetch sitemap in SSG mode', () => { it('should work when 1 language is requested', async () => { mockPathsRequest(); - + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); - + return expect(nock.isDone()).to.be.true; }); it('should work for single site when 1 language is requested', async () => { mockPathsRequest(); - + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap(['ua'], sites[0]); expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); - + return expect(nock.isDone()).to.be.true; }); - + it('should work when includePaths and excludePaths are provided', async () => { const includedPaths = ['/y1/']; const excludedPaths = ['/y1/y2/y3/y4']; - + nock(endpoint) .post('/', (body) => { return body.variables.includedPaths && body.variables.excludedPaths; @@ -220,7 +220,7 @@ describe('GraphQLSitemapService', () => { }, }, }); - + nock(endpoint) .post('/') .reply(200, { @@ -251,7 +251,7 @@ describe('GraphQLSitemapService', () => { }, }, }); - + const service = new GraphQLSitemapService({ endpoint, apiKey, @@ -260,7 +260,7 @@ describe('GraphQLSitemapService', () => { excludedPaths, }); const sitemap = await service.fetchSSGSitemap(['en']); - + return expect(sitemap).to.deep.equal([ { params: { @@ -271,571 +271,571 @@ describe('GraphQLSitemapService', () => { ]); }); - it('should return personalized paths when personalize data is requested and returned for single site', async () => { - const lang = 'ua'; + it('should return personalized paths when personalize data is requested and returned for single site', async () => { + const lang = 'ua'; - nock(endpoint) - .post('/', /PersonalizeSitemapQuery/gi) - .reply(200, sitemapPersonalizeQueryResult); + nock(endpoint) + .post('/', /PersonalizeSitemapQuery/gi) + .reply(200, sitemapPersonalizeQueryResult); - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites, - includePersonalizedRoutes: true, - }); - const sitemap = await service.fetchSSGSitemap([lang], 'site-name'); + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites, + includePersonalizedRoutes: true, + }); + const sitemap = await service.fetchSSGSitemap([lang], 'site-name'); - expect(sitemap).to.deep.equal([ - { - params: { - path: [''], + expect(sitemap).to.deep.equal([ + { + params: { + path: [''], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_green'], + { + params: { + path: ['_variantId_green'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_green', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_green', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_red', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_red', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_purple', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_purple', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - ]); - return expect(nock.isDone()).to.be.true; - }); + ]); + return expect(nock.isDone()).to.be.true; + }); - it('should return aggregated paths for multiple sites when no personalized site', async () => { - const multipleSites = ['site1', 'site2']; - const lang = 'ua'; + it('should return aggregated paths for multiple sites when no personalized site', async () => { + const multipleSites = ['site1', 'site2']; + const lang = 'ua'; - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[0]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[0]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, }, - ], + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, }, }, }, - }, - }); + }); - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[1]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 2, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/y1', - }, - { - path: '/x1/x2', + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[1]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 2, + pageInfo: { + hasNext: false, }, - ], + results: [ + { + path: '/y1', + }, + { + path: '/x1/x2', + }, + ], + }, }, }, }, - }, - }); + }); - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites: multipleSites, - }); - const sitemap = await service.fetchSSGSitemap([lang]); + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites: multipleSites, + }); + const sitemap = await service.fetchSSGSitemap([lang]); - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site1'], + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'x1'], + { + params: { + path: ['_site_site1', 'x1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2'], + { + params: { + path: ['_site_site1', 'y1', 'y2'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1'], + { + params: { + path: ['_site_site2', 'y1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'x1', 'x2'], + { + params: { + path: ['_site_site2', 'x1', 'x2'], + }, + locale: lang, }, - locale: lang, - }, - ]); - return expect(nock.isDone()).to.be.true; - }); + ]); + return expect(nock.isDone()).to.be.true; + }); - it('should return aggregated paths for multiple sites and personalized sites', async () => { - const multipleSites = ['site1', 'site2']; - const lang = 'ua'; + it('should return aggregated paths for multiple sites and personalized sites', async () => { + const multipleSites = ['site1', 'site2']; + const lang = 'ua'; - nock(endpoint) - .post('/', /PersonalizeSitemapQuery/gi) - .reply(200, sitemapPersonalizeQueryResult); + nock(endpoint) + .post('/', /PersonalizeSitemapQuery/gi) + .reply(200, sitemapPersonalizeQueryResult); - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[1]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[1]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, }, - ], + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, }, }, }, - }, - }); + }); - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites: multipleSites, - includePersonalizedRoutes: true, - }); - const sitemap = await service.fetchSSGSitemap([lang]); - console.log(sitemap.map((item) => item.params.path)); + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites: multipleSites, + includePersonalizedRoutes: true, + }); + const sitemap = await service.fetchSSGSitemap([lang]); + console.log(sitemap.map((item) => item.params.path)); - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site1'], - }, - locale: lang, - }, - { - params: { - path: ['_variantId_green', '_site_site1'], + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_green', '_site_site1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_green', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_red', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_green', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_variantId_purple', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_variantId_red', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2'], + { + params: { + path: ['_variantId_purple', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'x1'], + { + params: { + path: ['_site_site2'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_site_site2', 'x1'], + }, + locale: lang, }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1', 'y2'], + { + params: { + path: ['_site_site2', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, }, - locale: lang, - }, - ]); - return expect(nock.isDone()).to.be.true; - }); - - it('should work when multiple languages are requested', async () => { - const lang1 = 'ua'; - const lang2 = 'da-DK'; - - nock(endpoint) - .post('/', (body) => { - return body.variables.language === lang1; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', - }, - ], - }, - }, + { + params: { + path: ['_site_site2', 'y1', 'y2'], }, + locale: lang, }, - }); + ]); + return expect(nock.isDone()).to.be.true; + }); - nock(endpoint) - .post('/', (body) => { - return body.variables.language === lang2; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1-da-DK', - }, - { - path: '/y1/y2/y3/y4-da-DK', + it('should work when multiple languages are requested', async () => { + const lang1 = 'ua'; + const lang2 = 'da-DK'; + + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang1; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, }, - { - path: '/y1/y2-da-DK', + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang2; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, }, - ], + results: [ + { + path: '/', + }, + { + path: '/x1-da-DK', + }, + { + path: '/y1/y2/y3/y4-da-DK', + }, + { + path: '/y1/y2-da-DK', + }, + ], + }, }, }, }, - }, - }); + }); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchSSGSitemap([lang1, lang2]); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap([lang1, lang2]); - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site-name'], + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name'], + }, + locale: 'ua', }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'x1'], + { + params: { + path: ['_site_site-name', 'x1'], + }, + locale: 'ua', }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], + }, + locale: 'ua', }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2'], + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + locale: 'ua', }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name'], + { + params: { + path: ['_site_site-name'], + }, + locale: 'da-DK', }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'x1-da-DK'], + { + params: { + path: ['_site_site-name', 'x1-da-DK'], + }, + locale: 'da-DK', }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4-da-DK'], + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4-da-DK'], + }, + locale: 'da-DK', }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2-da-DK'], + { + params: { + path: ['_site_site-name', 'y1', 'y2-da-DK'], + }, + locale: 'da-DK', }, - locale: 'da-DK', - }, - ]); + ]); - return expect(nock.isDone()).to.be.true; - }); + return expect(nock.isDone()).to.be.true; + }); - it('should work when null results are present', async () => { - const lang = 'en'; + it('should work when null results are present', async () => { + const lang = 'en'; - nock(endpoint) - .post('/', (body) => { - return body.variables.language === lang; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, }, - null, - null, - ], + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + null, + null, + ], + }, }, }, }, - }, - }); + }); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchSSGSitemap([lang]); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap([lang]); - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site-name'], + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name'], + }, + locale: 'en', }, - locale: 'en', - }, - { - params: { - path: ['_site_site-name', 'x1'], + { + params: { + path: ['_site_site-name', 'x1'], + }, + locale: 'en', }, - locale: 'en', - }, - ]); + ]); - return expect(nock.isDone()).to.be.true; - }); + return expect(nock.isDone()).to.be.true; + }); - it('should throw error if valid language is not provided', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - await service.fetchSSGSitemap([]).catch((error: RangeError) => { - expect(error.message).to.equal(languageError); + it('should throw error if valid language is not provided', async () => { + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchSSGSitemap([]).catch((error: RangeError) => { + expect(error.message).to.equal(languageError); + }); }); - }); - it('should throw error if query returns nothing for a provided site name', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - nock(endpoint) - .post('/', (body) => { - return body.variables.siteName === sites[0]; - }) - .reply(200, { - data: { - site: { - siteInfo: null, + it('should throw error if query returns nothing for a provided site name', async () => { + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + nock(endpoint) + .post('/', (body) => { + return body.variables.siteName === sites[0]; + }) + .reply(200, { + data: { + site: { + siteInfo: null, + }, }, - }, + }); + await service.fetchSSGSitemap(['en']).catch((error: RangeError) => { + expect(error.message).to.equal(getSiteEmptyError(sites[0])); }); - await service.fetchSSGSitemap(['en']).catch((error: RangeError) => { - expect(error.message).to.equal(getSiteEmptyError(sites[0])); }); - }); - it('should throw error if empty language is provided', async () => { - mockPathsRequest(); + it('should throw error if empty language is provided', async () => { + mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - await service.fetchExportSitemap('').catch((error: RangeError) => { - expect(error.message).to.equal('The language must be a non-empty string'); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchExportSitemap('').catch((error: RangeError) => { + expect(error.message).to.equal('The language must be a non-empty string'); + }); + + return expect(nock.isDone()).to.be.false; }); - return expect(nock.isDone()).to.be.false; - }); + it('should use a custom pageSize, if provided', async () => { + const customPageSize = 20; - it('should use a custom pageSize, if provided', async () => { - const customPageSize = 20; + nock(endpoint) + .post('/', (body) => body.variables.pageSize === customPageSize) + .reply(200, sitemapDefaultQueryResult); - nock(endpoint) - .post('/', (body) => body.variables.pageSize === customPageSize) - .reply(200, sitemapDefaultQueryResult); + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites, + pageSize: customPageSize, + }); + const sitemap = await service.fetchSSGSitemap(['ua']); - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites, - pageSize: customPageSize, + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + return expect(nock.isDone()).to.be.true; }); - const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); - return expect(nock.isDone()).to.be.true; - }); + it('should use default value if pageSize is not specified', async () => { + nock(endpoint) + .post( + '/', + (body) => + body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined + ) + .reply(200, sitemapDefaultQueryResult); - it('should use default value if pageSize is not specified', async () => { - nock(endpoint) - .post( - '/', - (body) => - body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined - ) - .reply(200, sitemapDefaultQueryResult); + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + sites, + pageSize: undefined, + }); + const sitemap = await service.fetchSSGSitemap(['ua']); - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites, - pageSize: undefined, + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + return expect(nock.isDone()).to.be.true; }); - const sitemap = await service.fetchSSGSitemap(['ua']); - - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); - return expect(nock.isDone()).to.be.true; - }); - it('should work if sitemap has 0 pages', async () => { - mockPathsRequest([]); + it('should work if sitemap has 0 pages', async () => { + mockPathsRequest([]); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal([]); - return expect(nock.isDone()).to.be.true; - }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua']); + expect(sitemap).to.deep.equal([]); + return expect(nock.isDone()).to.be.true; + }); - it('should throw error if SitemapQuery fails', async () => { - nock(endpoint) - .post('/', /DefaultSitemapQuery/gi) - .reply(500, 'Error 😥'); + it('should throw error if SitemapQuery fails', async () => { + nock(endpoint) + .post('/', /DefaultSitemapQuery/gi) + .reply(500, 'Error 😥'); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - await service.fetchSSGSitemap(['ua']).catch((error: RangeError) => { - expect(error.message).to.contain('SitemapQuery'); - expect(error.message).to.contain('Error 😥'); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchSSGSitemap(['ua']).catch((error: RangeError) => { + expect(error.message).to.contain('SitemapQuery'); + expect(error.message).to.contain('Error 😥'); + }); + return expect(nock.isDone()).to.be.true; }); - return expect(nock.isDone()).to.be.true; }); }); -}); const expectedMultisiteExportSitemap = [ { @@ -894,7 +894,7 @@ describe('GraphQLSitemapService', () => { it('should fetch singlesite sitemap', async () => { mockPathsRequest(); const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchExportSitemap('ua',sites[0]); + const sitemap = await service.fetchExportSitemap('ua', sites[0]); expect(sitemap).to.deep.equal(expectedSinglesiteExportSitemap); return expect(nock.isDone()).to.be.true; }); @@ -955,4 +955,4 @@ describe('GraphQLSitemapService', () => { // eslint-disable-next-line no-unused-expressions expect(graphQLRequestClient).to.exist; }); -}); \ No newline at end of file +}); diff --git a/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts b/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts index ca70797110..f0c6d7c9ec 100644 --- a/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts +++ b/packages/sitecore-jss-nextjs/src/test-data/sitemapServiceMultisiteResult.ts @@ -7,19 +7,19 @@ export default [ }, { params: { - path: ['_site_site-name','x1'], + path: ['_site_site-name', 'x1'], }, locale: 'ua', }, { params: { - path: ['_site_site-name','y1', 'y2', 'y3', 'y4'], + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], }, locale: 'ua', }, { params: { - path: ['_site_site-name','y1', 'y2'], + path: ['_site_site-name', 'y1', 'y2'], }, locale: 'ua', }, From c66ca8f44a465049f6b273e5eb5c789997893eaf Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Thu, 16 Feb 2023 14:10:36 -0500 Subject: [PATCH 4/6] introduce a separate class for multisite path fetching --- .../plugins/graphql-sitemap-service.ts | 16 +- .../plugins/graphql-sitemap-service.ts | 17 +- packages/sitecore-jss-nextjs/src/index.ts | 5 + .../services/graphql-sitemap-service.test.ts | 460 +--------- .../src/services/graphql-sitemap-service.ts | 81 +- .../mutisite-graphql-sitemap-service.test.ts | 830 ++++++++++++++++++ .../mutisite-graphql-sitemap-service.ts | 91 ++ 7 files changed, 1002 insertions(+), 498 deletions(-) create mode 100644 packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.test.ts create mode 100644 packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index b5e67c0ea4..775a6b3591 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -1,5 +1,5 @@ import { - GraphQLSitemapService, + MultisiteGraphQLSitemapService, StaticPath, constants, SiteInfo, @@ -10,22 +10,24 @@ import { GetStaticPathsContext } from 'next'; import { siteResolver } from 'lib/site-resolver'; class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { - _graphqlSitemapService: GraphQLSitemapService; + _graphqlSitemapService: MultisiteGraphQLSitemapService; constructor() { - this._graphqlSitemapService = new GraphQLSitemapService({ + this._graphqlSitemapService = new MultisiteGraphQLSitemapService({ endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, - sites: siteResolver.sites.map((site: SiteInfo) => site.name), + sites: [...new Set(siteResolver.sites.map((site: SiteInfo) => site.name))], }); } async exec(context?: GetStaticPathsContext): Promise { if (process.env.EXPORT_MODE) { - // Disconnected Export mode - if (process.env.JSS_MODE !== constants.JSS_MODE.DISCONNECTED) { - return this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage); + if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { + return []; } + return process.env.EXPORT_MODE + ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) + : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index 0614f9946e..97d305f7eb 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -2,12 +2,10 @@ GraphQLSitemapService, StaticPath, constants, - SiteInfo, } from '@sitecore-jss/sitecore-jss-nextjs'; import config from 'temp/config'; import { SitemapFetcherPlugin } from '..'; import { GetStaticPathsContext } from 'next'; -import { siteResolver } from 'lib/site-resolver'; class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { _graphqlSitemapService: GraphQLSitemapService; @@ -16,21 +14,20 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { this._graphqlSitemapService = new GraphQLSitemapService({ endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, - sites: siteResolver.sites.map((site: SiteInfo) => site.name), + siteName: config.jssAppName, }); } async exec(context?: GetStaticPathsContext): Promise { if (process.env.EXPORT_MODE) { - // Disconnected Export mode - if (process.env.JSS_MODE !== constants.JSS_MODE.DISCONNECTED) { - return this._graphqlSitemapService.fetchExportSitemap( - config.defaultLanguage, - config.jssAppName - ); + if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { + return []; } + return process.env.EXPORT_MODE + ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) + : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } - return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || [], config.jssAppName); + return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } } diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 93b1e290c8..097630aeaa 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -80,6 +80,11 @@ export { GraphQLSitemapServiceConfig, } from './services/graphql-sitemap-service'; +export { + MultisiteGraphQLSitemapService, + MultisiteGraphQLSitemapServiceConfig, +} from './services/mutisite-graphql-sitemap-service'; + export { GraphQLSitemapXmlService, GraphQLSitemapXmlServiceConfig, diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index fa65690ee1..2ec30b73f8 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -8,7 +8,6 @@ import { } from './graphql-sitemap-service'; import sitemapDefaultQueryResult from '../test-data/sitemapDefaultQueryResult.json'; import sitemapPersonalizeQueryResult from '../test-data/sitemapPersonalizeQueryResult.json'; -import sitemapServiceMultisiteResult from '../test-data/sitemapServiceMultisiteResult'; import sitemapServiceSinglesiteResult from '../test-data/sitemapServiceSinglesiteResult'; import { GraphQLClient, GraphQLRequestClient } from '@sitecore-jss/sitecore-jss/graphql'; @@ -52,7 +51,7 @@ it('should return null if no app root found', async () => { describe('GraphQLSitemapService', () => { const endpoint = 'http://site'; const apiKey = 'some-api-key'; - const sites = ['site-name']; + const siteName = 'site-name'; afterEach(() => { nock.cleanAll(); @@ -87,9 +86,9 @@ describe('GraphQLSitemapService', () => { it('should work when 1 language is requested', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); return expect(nock.isDone()).to.be.true; }); @@ -156,7 +155,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - sites, + siteName, includedPaths, excludedPaths, }); @@ -165,7 +164,7 @@ describe('GraphQLSitemapService', () => { return expect(sitemap).to.deep.equal([ { params: { - path: ['_site_site-name', 'y1', 'y2'], + path: ['y1', 'y2'], }, locale: 'en', }, @@ -176,9 +175,9 @@ describe('GraphQLSitemapService', () => { it('should work when 1 language is requested', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); return expect(nock.isDone()).to.be.true; }); @@ -186,8 +185,8 @@ describe('GraphQLSitemapService', () => { it('should work for single site when 1 language is requested', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchSSGSitemap(['ua'], sites[0]); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); return expect(nock.isDone()).to.be.true; @@ -255,7 +254,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - sites, + siteName, includedPaths, excludedPaths, }); @@ -264,7 +263,7 @@ describe('GraphQLSitemapService', () => { return expect(sitemap).to.deep.equal([ { params: { - path: ['_site_site-name', 'y1', 'y2'], + path: ['y1', 'y2'], }, locale: 'en', }, @@ -281,10 +280,10 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - sites, + siteName, includePersonalizedRoutes: true, }); - const sitemap = await service.fetchSSGSitemap([lang], 'site-name'); + const sitemap = await service.fetchSSGSitemap([lang]); expect(sitemap).to.deep.equal([ { @@ -327,363 +326,6 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); - it('should return aggregated paths for multiple sites when no personalized site', async () => { - const multipleSites = ['site1', 'site2']; - const lang = 'ua'; - - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[0]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', - }, - ], - }, - }, - }, - }, - }); - - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[1]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 2, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/y1', - }, - { - path: '/x1/x2', - }, - ], - }, - }, - }, - }, - }); - - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites: multipleSites, - }); - const sitemap = await service.fetchSSGSitemap([lang]); - - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site1'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'x1'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'x1', 'x2'], - }, - locale: lang, - }, - ]); - return expect(nock.isDone()).to.be.true; - }); - - it('should return aggregated paths for multiple sites and personalized sites', async () => { - const multipleSites = ['site1', 'site2']; - const lang = 'ua'; - - nock(endpoint) - .post('/', /PersonalizeSitemapQuery/gi) - .reply(200, sitemapPersonalizeQueryResult); - - nock(endpoint) - .persist() - .post('/', (body) => { - return body.variables.siteName === multipleSites[1]; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', - }, - ], - }, - }, - }, - }, - }); - - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - sites: multipleSites, - includePersonalizedRoutes: true, - }); - const sitemap = await service.fetchSSGSitemap([lang]); - console.log(sitemap.map((item) => item.params.path)); - - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site1'], - }, - locale: lang, - }, - { - params: { - path: ['_variantId_green', '_site_site1'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_variantId_green', '_site_site1', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_variantId_red', '_site_site1', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_variantId_purple', '_site_site1', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'x1'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1', 'y2', 'y3', 'y4'], - }, - locale: lang, - }, - { - params: { - path: ['_site_site2', 'y1', 'y2'], - }, - locale: lang, - }, - ]); - return expect(nock.isDone()).to.be.true; - }); - - it('should work when multiple languages are requested', async () => { - const lang1 = 'ua'; - const lang2 = 'da-DK'; - - nock(endpoint) - .post('/', (body) => { - return body.variables.language === lang1; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1', - }, - { - path: '/y1/y2/y3/y4', - }, - { - path: '/y1/y2', - }, - ], - }, - }, - }, - }, - }); - - nock(endpoint) - .post('/', (body) => { - return body.variables.language === lang2; - }) - .reply(200, { - data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - path: '/', - }, - { - path: '/x1-da-DK', - }, - { - path: '/y1/y2/y3/y4-da-DK', - }, - { - path: '/y1/y2-da-DK', - }, - ], - }, - }, - }, - }, - }); - - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchSSGSitemap([lang1, lang2]); - - expect(sitemap).to.deep.equal([ - { - params: { - path: ['_site_site-name'], - }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'x1'], - }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], - }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2'], - }, - locale: 'ua', - }, - { - params: { - path: ['_site_site-name'], - }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'x1-da-DK'], - }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4-da-DK'], - }, - locale: 'da-DK', - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2-da-DK'], - }, - locale: 'da-DK', - }, - ]); - - return expect(nock.isDone()).to.be.true; - }); - it('should work when null results are present', async () => { const lang = 'en'; @@ -716,19 +358,19 @@ describe('GraphQLSitemapService', () => { }, }); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap([lang]); expect(sitemap).to.deep.equal([ { params: { - path: ['_site_site-name'], + path: [''], }, locale: 'en', }, { params: { - path: ['_site_site-name', 'x1'], + path: ['x1'], }, locale: 'en', }, @@ -738,17 +380,17 @@ describe('GraphQLSitemapService', () => { }); it('should throw error if valid language is not provided', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchSSGSitemap([]).catch((error: RangeError) => { expect(error.message).to.equal(languageError); }); }); it('should throw error if query returns nothing for a provided site name', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); nock(endpoint) .post('/', (body) => { - return body.variables.siteName === sites[0]; + return body.variables.siteName === siteName; }) .reply(200, { data: { @@ -758,14 +400,14 @@ describe('GraphQLSitemapService', () => { }, }); await service.fetchSSGSitemap(['en']).catch((error: RangeError) => { - expect(error.message).to.equal(getSiteEmptyError(sites[0])); + expect(error.message).to.equal(getSiteEmptyError(siteName)); }); }); it('should throw error if empty language is provided', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchExportSitemap('').catch((error: RangeError) => { expect(error.message).to.equal('The language must be a non-empty string'); }); @@ -783,12 +425,12 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - sites, + siteName, pageSize: customPageSize, }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); return expect(nock.isDone()).to.be.true; }); @@ -804,19 +446,19 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - sites, + siteName, pageSize: undefined, }); const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + expect(sitemap).to.deep.equal(sitemapServiceSinglesiteResult); return expect(nock.isDone()).to.be.true; }); it('should work if sitemap has 0 pages', async () => { mockPathsRequest([]); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal([]); return expect(nock.isDone()).to.be.true; @@ -827,7 +469,7 @@ describe('GraphQLSitemapService', () => { .post('/', /DefaultSitemapQuery/gi) .reply(500, 'Error 😥'); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchSSGSitemap(['ua']).catch((error: RangeError) => { expect(error.message).to.contain('SitemapQuery'); expect(error.message).to.contain('Error 😥'); @@ -837,29 +479,6 @@ describe('GraphQLSitemapService', () => { }); }); - const expectedMultisiteExportSitemap = [ - { - params: { - path: ['_site_site-name'], - }, - }, - { - params: { - path: ['_site_site-name', 'x1'], - }, - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], - }, - }, - { - params: { - path: ['_site_site-name', 'y1', 'y2'], - }, - }, - ]; - const expectedSinglesiteExportSitemap = [ { params: { @@ -884,23 +503,16 @@ describe('GraphQLSitemapService', () => { ]; describe('Fetch sitemap in export mode', () => { - it('should fetch multisite sitemap', async () => { - mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchExportSitemap('ua'); - expect(sitemap).to.deep.equal(expectedMultisiteExportSitemap); - return expect(nock.isDone()).to.be.true; - }); it('should fetch singlesite sitemap', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); - const sitemap = await service.fetchExportSitemap('ua', sites[0]); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const sitemap = await service.fetchExportSitemap('ua'); expect(sitemap).to.deep.equal(expectedSinglesiteExportSitemap); return expect(nock.isDone()).to.be.true; }); it('should work if endpoint returns 0 pages', async () => { mockPathsRequest([]); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchExportSitemap('ua'); expect(sitemap).to.deep.equal([]); return expect(nock.isDone()).to.be.true; @@ -909,7 +521,7 @@ describe('GraphQLSitemapService', () => { nock(endpoint) .post('/', /DefaultSitemapQuery/gi) .reply(500, 'Error 😥'); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchExportSitemap('ua').catch((error: RangeError) => { expect(error.message).to.contain('SitemapQuery'); expect(error.message).to.contain('Error 😥'); @@ -918,7 +530,7 @@ describe('GraphQLSitemapService', () => { }); it('should throw error if language is not provided', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchExportSitemap('').catch((error: RangeError) => { expect(error.message).to.equal('The language must be a non-empty string'); }); @@ -926,10 +538,10 @@ describe('GraphQLSitemapService', () => { }); }); it('should throw error if query returns nothing for a provided site name', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); nock(endpoint) .post('/', (body) => { - return body.variables.siteName === sites[0]; + return body.variables.siteName === siteName; }) .reply(200, { data: { @@ -939,14 +551,14 @@ describe('GraphQLSitemapService', () => { }, }); await service.fetchExportSitemap('en').catch((error: RangeError) => { - expect(error.message).to.equal(getSiteEmptyError(sites[0])); + expect(error.message).to.equal(getSiteEmptyError(siteName)); }); }); it('should provide a default GraphQL client', () => { const service = new TestService({ endpoint, apiKey, - sites, + siteName, }); const graphQLClient = service.client as GraphQLClient; const graphQLRequestClient = service.client as GraphQLRequestClient; diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 6ab69fb3c4..0f43d369f0 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -1,11 +1,10 @@ import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; import { getPersonalizedRewrite } from '@sitecore-jss/sitecore-jss/personalize'; -import { getSiteRewrite } from '@sitecore-jss/sitecore-jss/site'; /** @private */ export const languageError = 'The list of languages cannot be empty'; -export const sitesError = 'The list of sites cannot be empty'; +export const siteError = 'The service needs a site name'; /** * @param {string} siteName to inject into error text @@ -138,9 +137,10 @@ export interface GraphQLSitemapServiceConfig apiKey: string; /** - * Names of the configured sites + * Name of the site to retrieve site paths for + * Make it optional */ - sites?: string[]; + siteName?: string; /** * A flag for whether to include personalized routes in service output - only works on XM Cloud @@ -161,7 +161,7 @@ export type StaticPath = { /** * Service that fetches the list of site pages using Sitecore's GraphQL API. - * Used to handle multiple sites by default but can retrieve paths for an individual site + * Used to handle a single site * This list is used for SSG and Export functionality. * @mixes SearchQueryService */ @@ -188,26 +188,24 @@ export class GraphQLSitemapService { * The `locale` parameter will be used in the item query, but since i18n is not supported, * the output paths will not include a `language` property. * @param {string} locale which application supports - * @param {string | undefined} siteName used when only paths for a single site are needed, without site prefix * @returns an array of @see StaticPath objects */ - async fetchExportSitemap(locale: string, siteName?: string): Promise { + async fetchExportSitemap(locale: string): Promise { const formatPath = (path: string[]) => ({ params: { path, }, }); - return this.fetchSitemap([locale], formatPath, siteName); + return this.fetchSitemap([locale], formatPath); } /** * Fetch sitemap which could be used for generation of static pages using SSG mode * @param {string[]} locales locales which application supports - * @param {string | undefined} siteName used when only paths for a single site are needed, without site prefix * @returns an array of @see StaticPath objects */ - async fetchSSGSitemap(locales: string[], siteName?: string): Promise { + async fetchSSGSitemap(locales: string[]): Promise { const formatPath = (path: string[], locale: string) => ({ params: { path, @@ -215,7 +213,7 @@ export class GraphQLSitemapService { locale, }); - return this.fetchSitemap(locales, formatPath, siteName); + return this.fetchSitemap(locales, formatPath); } /** @@ -223,49 +221,34 @@ export class GraphQLSitemapService { * version in the specified language(s). * @param {string[]} languages Fetch pages that have versions in this language(s). * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. - * @param {string | undefined} singleSite used when only paths for a single site are needed, without site prefix * @returns list of pages * @throws {RangeError} if the list of languages is empty. * @throws {RangeError} if the any of the languages is an empty string. */ protected async fetchSitemap( languages: string[], - formatStaticPath: (path: string[], language: string) => StaticPath, - singleSite?: string + formatStaticPath: (path: string[], language: string) => StaticPath ): Promise { const paths = new Array(); if (!languages.length) { throw new RangeError(languageError); } - if (singleSite) { - paths.push( - ...(await this.getTranformedPaths(singleSite, languages, formatStaticPath, false)) - ); - } else { - // Get all sites - const sites = this.options.sites; - if (!sites || !sites.length) { - throw new RangeError(sitesError); - } - - // Fetch paths for each site - for (let i = 0; i < sites.length; i++) { - const siteName = sites[i]; + const siteName = this.options.siteName; - // Fetch paths using all locales - paths.push(...(await this.getTranformedPaths(siteName, languages, formatStaticPath, true))); - } + if (!siteName) { + throw new RangeError(siteError); } + paths.push(...(await this.getTranformedPaths(siteName, languages, formatStaticPath))); + return ([] as StaticPath[]).concat(...paths); } protected async getTranformedPaths( siteName: string, languages: string[], - formatStaticPath: (path: string[], language: string) => StaticPath, - multisite?: boolean + formatStaticPath: (path: string[], language: string) => StaticPath ) { const paths = new Array(); await Promise.all( @@ -278,8 +261,7 @@ export class GraphQLSitemapService { const transformedPaths = await this.transformLanguageSitePaths( results, formatStaticPath, - language, - multisite ? siteName : undefined + language ); paths.push(...transformedPaths); }) @@ -290,8 +272,7 @@ export class GraphQLSitemapService { protected async transformLanguageSitePaths( sitePaths: RouteListQueryResult[], formatStaticPath: (path: string[], language: string) => StaticPath, - language: string, - multiSiteName?: string + language: string ): Promise { const formatPath = (path: string) => formatStaticPath(path.replace(/^\/|\/$/g, '').split('/'), language); @@ -301,29 +282,15 @@ export class GraphQLSitemapService { sitePaths.forEach((item) => { if (!item) return; - if (!multiSiteName) { - aggregatedPaths.push(formatPath(item.path)); - } else { - aggregatedPaths.push(formatPath(getSiteRewrite(item.path, { siteName: multiSiteName }))); - } + aggregatedPaths.push(formatPath(item.path)); // check for type safety's sake - personalize may be empty depending on query type if (item.route?.personalization?.variantIds.length) { - multiSiteName - ? aggregatedPaths.push( - ...(item.route?.personalization?.variantIds.map((varId) => - formatPath( - getPersonalizedRewrite(getSiteRewrite(item.path, { siteName: multiSiteName }), { - variantId: varId, - }) - ) - ) || {}) - ) - : aggregatedPaths.push( - ...(item.route?.personalization?.variantIds.map((varId) => - formatPath(getPersonalizedRewrite(item.path, { variantId: varId })) - ) || {}) - ); + aggregatedPaths.push( + ...(item.route?.personalization?.variantIds.map((varId) => + formatPath(getPersonalizedRewrite(item.path, { variantId: varId })) + ) || {}) + ); } }); diff --git a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.test.ts new file mode 100644 index 0000000000..b62213b607 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.test.ts @@ -0,0 +1,830 @@ +import { expect } from 'chai'; +import nock from 'nock'; +import { + MultisiteGraphQLSitemapService, + MultisiteGraphQLSitemapServiceConfig, +} from './mutisite-graphql-sitemap-service'; +import { getSiteEmptyError, languageError } from './graphql-sitemap-service'; +import sitemapDefaultQueryResult from '../test-data/sitemapDefaultQueryResult.json'; +import sitemapPersonalizeQueryResult from '../test-data/sitemapPersonalizeQueryResult.json'; +import sitemapServiceMultisiteResult from '../test-data/sitemapServiceMultisiteResult'; +import { GraphQLClient, GraphQLRequestClient } from '@sitecore-jss/sitecore-jss/graphql'; + +class TestService extends MultisiteGraphQLSitemapService { + public client: GraphQLClient; + constructor(options: MultisiteGraphQLSitemapServiceConfig) { + super(options); + this.client = this.getGraphQLClient(); + } +} + +describe('MultisiteGraphQLSitemapService', () => { + const endpoint = 'http://site'; + const apiKey = 'some-api-key'; + const sites = ['site-name']; + + afterEach(() => { + nock.cleanAll(); + }); + + const mockPathsRequest = (results?: { url: { path: string } }[]) => { + nock(endpoint) + .post('/', /DefaultSitemapQuery/gi) + .reply( + 200, + results === undefined + ? sitemapDefaultQueryResult + : { + data: { + site: { + siteInfo: { + routes: { + total: results.length, + pageInfo: { + hasNext: false, + }, + results, + }, + }, + }, + }, + } + ); + }; + + describe('Fetch sitemap in SSG mode', () => { + it('should work when 1 language is requested', async () => { + mockPathsRequest(); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua']); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + + return expect(nock.isDone()).to.be.true; + }); + + it('should work when includePaths and excludePaths are provided', async () => { + const includedPaths = ['/y1/']; + const excludedPaths = ['/y1/y2/y3/y4']; + + nock(endpoint) + .post('/', (body) => { + return body.variables.includedPaths && body.variables.excludedPaths; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 1, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/y1/y2/', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/') + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites, + includedPaths, + excludedPaths, + }); + const sitemap = await service.fetchSSGSitemap(['en']); + + return expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + locale: 'en', + }, + ]); + }); + + describe('Fetch sitemap in SSG mode', () => { + it('should work when 1 language is requested', async () => { + mockPathsRequest(); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua']); + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + + return expect(nock.isDone()).to.be.true; + }); + + it('should work when includePaths and excludePaths are provided', async () => { + const includedPaths = ['/y1/']; + const excludedPaths = ['/y1/y2/y3/y4']; + + nock(endpoint) + .post('/', (body) => { + return body.variables.includedPaths && body.variables.excludedPaths; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 1, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/y1/y2/', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/') + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites, + includedPaths, + excludedPaths, + }); + const sitemap = await service.fetchSSGSitemap(['en']); + + return expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + locale: 'en', + }, + ]); + }); + + it('should return aggregated paths for multiple sites with no personalization', async () => { + const multipleSites = ['site1', 'site2']; + const lang = 'ua'; + + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[0]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[1]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 2, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/y1', + }, + { + path: '/x1/x2', + }, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites: multipleSites, + }); + const sitemap = await service.fetchSSGSitemap([lang]); + + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site1'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site1', 'x1'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site1', 'y1', 'y2'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2', 'y1'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2', 'x1', 'x2'], + }, + locale: lang, + }, + ]); + return expect(nock.isDone()).to.be.true; + }); + + it('should return aggregated paths for multiple sites and personalized sites', async () => { + const multipleSites = ['site1', 'site2']; + const lang = 'ua'; + + nock(endpoint) + .post('/', /PersonalizeSitemapQuery/gi) + .reply(200, sitemapPersonalizeQueryResult); + + nock(endpoint) + .persist() + .post('/', (body) => { + return body.variables.siteName === multipleSites[1]; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites: multipleSites, + includePersonalizedRoutes: true, + }); + const sitemap = await service.fetchSSGSitemap([lang]); + + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site1'], + }, + locale: lang, + }, + { + params: { + path: ['_variantId_green', '_site_site1'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_variantId_green', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_variantId_red', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_variantId_purple', '_site_site1', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2', 'x1'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2', 'y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + { + params: { + path: ['_site_site2', 'y1', 'y2'], + }, + locale: lang, + }, + ]); + return expect(nock.isDone()).to.be.true; + }); + + it('should work when multiple languages are requested', async () => { + const lang1 = 'ua'; + const lang2 = 'da-DK'; + + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang1; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang2; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1-da-DK', + }, + { + path: '/y1/y2/y3/y4-da-DK', + }, + { + path: '/y1/y2-da-DK', + }, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap([lang1, lang2]); + + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name', 'x1'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + locale: 'ua', + }, + { + params: { + path: ['_site_site-name'], + }, + locale: 'da-DK', + }, + { + params: { + path: ['_site_site-name', 'x1-da-DK'], + }, + locale: 'da-DK', + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4-da-DK'], + }, + locale: 'da-DK', + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2-da-DK'], + }, + locale: 'da-DK', + }, + ]); + + return expect(nock.isDone()).to.be.true; + }); + + it('should work when null results are present', async () => { + const lang = 'en'; + + nock(endpoint) + .post('/', (body) => { + return body.variables.language === lang; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + null, + null, + ], + }, + }, + }, + }, + }); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap([lang]); + + expect(sitemap).to.deep.equal([ + { + params: { + path: ['_site_site-name'], + }, + locale: 'en', + }, + { + params: { + path: ['_site_site-name', 'x1'], + }, + locale: 'en', + }, + ]); + + return expect(nock.isDone()).to.be.true; + }); + + it('should throw error if valid language is not provided', async () => { + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchSSGSitemap([]).catch((error: RangeError) => { + expect(error.message).to.equal(languageError); + }); + }); + + it('should throw error if query returns nothing for a provided site name', async () => { + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + nock(endpoint) + .post('/', (body) => { + return body.variables.siteName === sites[0]; + }) + .reply(200, { + data: { + site: { + siteInfo: null, + }, + }, + }); + await service.fetchSSGSitemap(['en']).catch((error: RangeError) => { + expect(error.message).to.equal(getSiteEmptyError(sites[0])); + }); + }); + + it('should throw error if empty language is provided', async () => { + mockPathsRequest(); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchExportSitemap('').catch((error: RangeError) => { + expect(error.message).to.equal('The language must be a non-empty string'); + }); + + return expect(nock.isDone()).to.be.false; + }); + + it('should use a custom pageSize, if provided', async () => { + const customPageSize = 20; + + nock(endpoint) + .post('/', (body) => body.variables.pageSize === customPageSize) + .reply(200, sitemapDefaultQueryResult); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites, + pageSize: customPageSize, + }); + const sitemap = await service.fetchSSGSitemap(['ua']); + + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + return expect(nock.isDone()).to.be.true; + }); + + it('should use default value if pageSize is not specified', async () => { + nock(endpoint) + .post( + '/', + (body) => + body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined + ) + .reply(200, sitemapDefaultQueryResult); + + const service = new MultisiteGraphQLSitemapService({ + endpoint, + apiKey, + sites, + pageSize: undefined, + }); + const sitemap = await service.fetchSSGSitemap(['ua']); + + expect(sitemap).to.deep.equal(sitemapServiceMultisiteResult); + return expect(nock.isDone()).to.be.true; + }); + + it('should work if sitemap has 0 pages', async () => { + mockPathsRequest([]); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchSSGSitemap(['ua']); + expect(sitemap).to.deep.equal([]); + return expect(nock.isDone()).to.be.true; + }); + + it('should throw error if SitemapQuery fails', async () => { + nock(endpoint) + .post('/', /DefaultSitemapQuery/gi) + .reply(500, 'Error 😥'); + + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchSSGSitemap(['ua']).catch((error: RangeError) => { + expect(error.message).to.contain('SitemapQuery'); + expect(error.message).to.contain('Error 😥'); + }); + return expect(nock.isDone()).to.be.true; + }); + }); + }); + + const expectedMultisiteExportSitemap = [ + { + params: { + path: ['_site_site-name'], + }, + }, + { + params: { + path: ['_site_site-name', 'x1'], + }, + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2', 'y3', 'y4'], + }, + }, + { + params: { + path: ['_site_site-name', 'y1', 'y2'], + }, + }, + ]; + + describe('Fetch sitemap in export mode', () => { + it('should fetch multisite sitemap', async () => { + mockPathsRequest(); + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchExportSitemap('ua'); + expect(sitemap).to.deep.equal(expectedMultisiteExportSitemap); + return expect(nock.isDone()).to.be.true; + }); + it('should work if endpoint returns 0 pages', async () => { + mockPathsRequest([]); + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + const sitemap = await service.fetchExportSitemap('ua'); + expect(sitemap).to.deep.equal([]); + return expect(nock.isDone()).to.be.true; + }); + it('should throw error if SitemapQuery fails', async () => { + nock(endpoint) + .post('/', /DefaultSitemapQuery/gi) + .reply(500, 'Error 😥'); + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchExportSitemap('ua').catch((error: RangeError) => { + expect(error.message).to.contain('SitemapQuery'); + expect(error.message).to.contain('Error 😥'); + }); + return expect(nock.isDone()).to.be.true; + }); + it('should throw error if language is not provided', async () => { + mockPathsRequest(); + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + await service.fetchExportSitemap('').catch((error: RangeError) => { + expect(error.message).to.equal('The language must be a non-empty string'); + }); + return expect(nock.isDone()).to.be.false; + }); + }); + it('should throw error if query returns nothing for a provided site name', async () => { + const service = new MultisiteGraphQLSitemapService({ endpoint, apiKey, sites }); + nock(endpoint) + .post('/', (body) => { + return body.variables.siteName === sites[0]; + }) + .reply(200, { + data: { + site: { + siteInfo: null, + }, + }, + }); + await service.fetchExportSitemap('en').catch((error: RangeError) => { + expect(error.message).to.equal(getSiteEmptyError(sites[0])); + }); + }); + it('should provide a default GraphQL client', () => { + const service = new TestService({ + endpoint, + apiKey, + sites, + }); + const graphQLClient = service.client as GraphQLClient; + const graphQLRequestClient = service.client as GraphQLRequestClient; + // eslint-disable-next-line no-unused-expressions + expect(graphQLClient).to.exist; + // eslint-disable-next-line no-unused-expressions + expect(graphQLRequestClient).to.exist; + }); +}); diff --git a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts new file mode 100644 index 0000000000..ae9d047af0 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts @@ -0,0 +1,91 @@ +import { getSiteRewrite } from '@sitecore-jss/sitecore-jss/site'; +import { + GraphQLSitemapService, + GraphQLSitemapServiceConfig, + languageError, + RouteListQueryResult, + StaticPath, +} from './graphql-sitemap-service'; + +export const sitesError = 'The list of sites cannot be empty'; + +/** + * Configuration options for @see GraphQLSitemapService instances + */ +export interface MultisiteGraphQLSitemapServiceConfig extends GraphQLSitemapServiceConfig { + /** + * Names of the configured sites + */ + sites: string[]; +} + +/** + * Service that fetches the list of site pages using Sitecore's GraphQL API. + * Used to handle multiple sites + * This list is used for SSG and Export functionality. + * @mixes SearchQueryService + */ +export class MultisiteGraphQLSitemapService extends GraphQLSitemapService { + /** + * Creates an instance of graphQL sitemap service with the provided options + * @param {GraphQLSitemapServiceConfig} options instance + */ + constructor(public options: MultisiteGraphQLSitemapServiceConfig) { + super(options); + } + + /** + * Fetch a flat list of all pages that belong to all the requested sites and have a + * version in the specified language(s). + * @param {string[]} languages Fetch pages that have versions in this language(s). + * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. + * @returns list of pages + * @throws {RangeError} if the list of languages is empty. + * @throws {RangeError} if the any of the languages is an empty string. + */ + protected async fetchSitemap( + languages: string[], + formatStaticPath: (path: string[], language: string) => StaticPath + ): Promise { + const paths = new Array(); + if (!languages.length) { + throw new RangeError(languageError); + } + // Get all sites + const sites = this.options.sites; + if (!sites || !sites.length) { + throw new RangeError(sitesError); + } + + // Fetch paths for each site + for (let i = 0; i < sites.length; i++) { + const siteName = sites[i]; + + // Fetch paths using all locales + paths.push(...(await this.getTranformedPaths(siteName, languages, formatStaticPath))); + } + + return ([] as StaticPath[]).concat(...paths); + } + + /** + * Fetch and return site paths for multisite implementation, with prefixes included + * @param {string} language path language + * @param {string} siteName site name + * @returns modified paths + */ + protected async fetchLanguageSitePaths( + language: string, + siteName: string + ): Promise { + const results: RouteListQueryResult[] = await super.fetchLanguageSitePaths(language, siteName); + + results.forEach((item) => { + if (item) { + item.path = getSiteRewrite(item.path, { siteName: siteName }); + } + }); + + return results; + } +} From e81f16ef4963735864f8a14ec4fd6ca6b9a8c4d5 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Fri, 17 Feb 2023 16:19:27 -0500 Subject: [PATCH 5/6] multisite-sitepaths: three class solution --- .../plugins/graphql-sitemap-service.ts | 13 +- .../plugins/graphql-sitemap-service.ts | 13 +- .../services/base-graphql-sitemap-service.ts | 333 ++++++++++++++++++ .../src/services/graphql-sitemap-service.ts | 281 +-------------- .../mutisite-graphql-sitemap-service.ts | 10 +- 5 files changed, 356 insertions(+), 294 deletions(-) create mode 100644 packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index 775a6b3591..2c258b0e50 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -21,15 +21,12 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { } async exec(context?: GetStaticPathsContext): Promise { - if (process.env.EXPORT_MODE) { - if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { - return []; - } - return process.env.EXPORT_MODE - ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) - : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { + return []; } - return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + return process.env.EXPORT_MODE + ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) + : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } } diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index 97d305f7eb..ea50d1c09e 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -19,15 +19,12 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { } async exec(context?: GetStaticPathsContext): Promise { - if (process.env.EXPORT_MODE) { - if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { - return []; - } - return process.env.EXPORT_MODE - ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) - : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) { + return []; } - return this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); + return process.env.EXPORT_MODE + ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage) + : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []); } } diff --git a/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts new file mode 100644 index 0000000000..61b1fec4fe --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts @@ -0,0 +1,333 @@ +import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; +import { debug } from '@sitecore-jss/sitecore-jss'; +import { getPersonalizedRewrite } from '@sitecore-jss/sitecore-jss/personalize'; + +/** @private */ +export const languageError = 'The list of languages cannot be empty'; +export const siteError = 'The service needs a site name'; + +/** + * @param {string} siteName to inject into error text + * @private + */ +export function getSiteEmptyError(siteName: string) { + return `Site "${siteName}" does not exist or site item tree is missing`; +} + +const languageEmptyError = 'The language must be a non-empty string'; + +/** + * GQL query made dynamic based on schema differences between SXP and XM Cloud + * @param {boolean} usesPersonalize flag to detrmine which variation of a query to run + * @returns GraphQL query to fetch site paths with + */ +const defaultQuery = (usesPersonalize?: boolean) => /* GraphQL */ ` +query ${usesPersonalize ? 'PersonalizeSitemapQuery' : 'DefaultSitemapQuery'}( + $siteName: String! + $language: String! + $includedPaths: [String] + $excludedPaths: [String] + $pageSize: Int = 10 + $after: String +) { + site { + siteInfo(site: $siteName) { + routes( + language: $language + includedPaths: $includedPaths + excludedPaths: $excludedPaths + first: $pageSize + after: $after + ){ + total + pageInfo { + endCursor + hasNext + } + results { + path: routePath + ${ + usesPersonalize + ? ` + route { + personalization { + variantIds + } + }` + : '' + } + } + } + } + } +} +`; +/** + * type for input variables for the site routes query + */ +interface SiteRouteQueryVariables { + /** + * Required. The name of the site being queried. + */ + siteName: string; + /** + * Required. The language to return routes/pages for. + */ + language: string; + /** + * Optional. Only paths starting with these provided prefixes will be returned. + */ + includedPaths?: string[]; + /** + * Optional. Paths starting with these provided prefixes will be excluded from returned results. + */ + excludedPaths?: string[]; + + /** common variable for all GraphQL queries + * it will be used for every type of query to regulate result batch size + * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. + * @default 10 + */ + pageSize?: number; +} + +/** + * Schema of data returned in response to a "site" query request + * @template T The type of objects being requested. + */ +export interface SiteRouteQueryResult { + site: { + siteInfo: { + routes: { + /** + * Data needed to paginate the site results + */ + pageInfo: PageInfo; + results: T[]; + }; + }; + }; +} + +/** + * The schema of data returned in response to a routes list query request + */ +export type RouteListQueryResult = { + path: string; + route?: { + personalization?: { + variantIds: string[]; + }; + }; +}; + +/** + * Configuration options for @see GraphQLSitemapService instances + */ +export interface BaseGraphQLSitemapServiceConfig + extends Omit { + /** + * Your Graphql endpoint + */ + endpoint: string; + + /** + * The API key to use for authentication. + */ + apiKey: string; + + /** + * A flag for whether to include personalized routes in service output - only works on XM Cloud + * turned off by default + */ + includePersonalizedRoutes?: boolean; +} + +/** + * Object model of a site page item. + */ +export type StaticPath = { + params: { + path: string[]; + }; + locale?: string; +}; + +/** + * Service that fetches the list of site pages using Sitecore's GraphQL API. + * Used to handle a single site + * This list is used for SSG and Export functionality. + * @mixes SearchQueryService + */ +export abstract class BaseGraphQLSitemapService { + private _graphQLClient: GraphQLClient; + + /** + * GraphQL client accessible by descendant classes when needed + */ + protected get graphQLClient() { + return this._graphQLClient; + } + + /** + * Gets the default query used for fetching the list of site pages + */ + protected get query(): string { + return defaultQuery(this.options.includePersonalizedRoutes); + } + + /** + * Creates an instance of graphQL sitemap service with the provided options + * @param {GraphQLSitemapServiceConfig} options instance + */ + constructor(public options: BaseGraphQLSitemapServiceConfig) { + this._graphQLClient = this.getGraphQLClient(); + } + + /** + * Fetch sitemap which could be used for generation of static pages during `next export`. + * The `locale` parameter will be used in the item query, but since i18n is not supported, + * the output paths will not include a `language` property. + * @param {string} locale which application supports + * @returns an array of @see StaticPath objects + */ + async fetchExportSitemap(locale: string): Promise { + const formatPath = (path: string[]) => ({ + params: { + path, + }, + }); + + return this.fetchSitemap([locale], formatPath); + } + + /** + * Fetch sitemap which could be used for generation of static pages using SSG mode + * @param {string[]} locales locales which application supports + * @returns an array of @see StaticPath objects + */ + async fetchSSGSitemap(locales: string[]): Promise { + const formatPath = (path: string[], locale: string) => ({ + params: { + path, + }, + locale, + }); + + return this.fetchSitemap(locales, formatPath); + } + + protected async getTranformedPaths( + siteName: string, + languages: string[], + formatStaticPath: (path: string[], language: string) => StaticPath + ) { + const paths = new Array(); + await Promise.all( + languages.map(async (language) => { + if (language === '') { + throw new RangeError(languageEmptyError); + } + debug.sitemap('fetching sitemap data for %s %s', language, siteName); + const results = await this.fetchLanguageSitePaths(language, siteName); + const transformedPaths = await this.transformLanguageSitePaths( + results, + formatStaticPath, + language + ); + paths.push(...transformedPaths); + }) + ); + return paths; + } + + protected async transformLanguageSitePaths( + sitePaths: RouteListQueryResult[], + formatStaticPath: (path: string[], language: string) => StaticPath, + language: string + ): Promise { + const formatPath = (path: string) => + formatStaticPath(path.replace(/^\/|\/$/g, '').split('/'), language); + + const aggregatedPaths: StaticPath[] = []; + + sitePaths.forEach((item) => { + if (!item) return; + + aggregatedPaths.push(formatPath(item.path)); + + // check for type safety's sake - personalize may be empty depending on query type + if (item.route?.personalization?.variantIds.length) { + aggregatedPaths.push( + ...(item.route?.personalization?.variantIds.map((varId) => + formatPath(getPersonalizedRewrite(item.path, { variantId: varId })) + ) || {}) + ); + } + }); + + return aggregatedPaths; + } + + protected async fetchLanguageSitePaths( + language: string, + siteName: string + ): Promise { + const args: SiteRouteQueryVariables = { + siteName: siteName, + language: language, + pageSize: this.options.pageSize, + includedPaths: this.options.includedPaths, + excludedPaths: this.options.excludedPaths, + }; + let results: RouteListQueryResult[] = []; + let hasNext = true; + let after = ''; + + while (hasNext) { + const fetchResponse = await this.graphQLClient.request< + SiteRouteQueryResult + >(this.query, { + ...args, + after, + }); + + if (!fetchResponse?.site?.siteInfo) { + throw new RangeError(getSiteEmptyError(siteName)); + } else { + results = results.concat(fetchResponse.site.siteInfo.routes?.results); + hasNext = fetchResponse.site.siteInfo.routes?.pageInfo.hasNext; + after = fetchResponse.site.siteInfo.routes?.pageInfo.endCursor; + } + } + + return results; + } + + /** + * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default + * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you + * want to use something else. + * @returns {GraphQLClient} implementation + */ + protected getGraphQLClient(): GraphQLClient { + return new GraphQLRequestClient(this.options.endpoint, { + apiKey: this.options.apiKey, + debugger: debug.sitemap, + }); + } + + /** + * Fetch a flat list of all pages that belong to the specificed site and have a + * version in the specified language(s). + * @param {string[]} languages Fetch pages that have versions in this language(s). + * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. + * @returns list of pages + * @throws {RangeError} if the list of languages is empty. + * @throws {RangeError} if the any of the languages is an empty string. + */ + protected abstract fetchSitemap( + languages: string[], + formatStaticPath: (path: string[], language: string) => StaticPath + ): Promise; +} diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 0f43d369f0..d6a813a265 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -1,6 +1,7 @@ -import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; -import { debug } from '@sitecore-jss/sitecore-jss'; -import { getPersonalizedRewrite } from '@sitecore-jss/sitecore-jss/personalize'; +import { + BaseGraphQLSitemapService, + BaseGraphQLSitemapServiceConfig, +} from './base-graphql-sitemap-service'; /** @private */ export const languageError = 'The list of languages cannot be empty'; @@ -14,139 +15,15 @@ export function getSiteEmptyError(siteName: string) { return `Site "${siteName}" does not exist or site item tree is missing`; } -const languageEmptyError = 'The language must be a non-empty string'; - -/** - * GQL query made dynamic based on schema differences between SXP and XM Cloud - * @param {boolean} usesPersonalize flag to detrmine which variation of a query to run - * @returns GraphQL query to fetch site paths with - */ -const defaultQuery = (usesPersonalize?: boolean) => /* GraphQL */ ` -query ${usesPersonalize ? 'PersonalizeSitemapQuery' : 'DefaultSitemapQuery'}( - $siteName: String! - $language: String! - $includedPaths: [String] - $excludedPaths: [String] - $pageSize: Int = 10 - $after: String -) { - site { - siteInfo(site: $siteName) { - routes( - language: $language - includedPaths: $includedPaths - excludedPaths: $excludedPaths - first: $pageSize - after: $after - ){ - total - pageInfo { - endCursor - hasNext - } - results { - path: routePath - ${ - usesPersonalize - ? ` - route { - personalization { - variantIds - } - }` - : '' - } - } - } - } - } -} -`; -/** - * type for input variables for the site routes query - */ -interface SiteRouteQueryVariables { - /** - * Required. The name of the site being queried. - */ - siteName: string; - /** - * Required. The language to return routes/pages for. - */ - language: string; - /** - * Optional. Only paths starting with these provided prefixes will be returned. - */ - includedPaths?: string[]; - /** - * Optional. Paths starting with these provided prefixes will be excluded from returned results. - */ - excludedPaths?: string[]; - - /** common variable for all GraphQL queries - * it will be used for every type of query to regulate result batch size - * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. - * @default 10 - */ - pageSize?: number; -} - -/** - * Schema of data returned in response to a "site" query request - * @template T The type of objects being requested. - */ -export interface SiteRouteQueryResult { - site: { - siteInfo: { - routes: { - /** - * Data needed to paginate the site results - */ - pageInfo: PageInfo; - results: T[]; - }; - }; - }; -} - -/** - * The schema of data returned in response to a routes list query request - */ -export type RouteListQueryResult = { - path: string; - route?: { - personalization?: { - variantIds: string[]; - }; - }; -}; - /** * Configuration options for @see GraphQLSitemapService instances */ -export interface GraphQLSitemapServiceConfig - extends Omit { - /** - * Your Graphql endpoint - */ - endpoint: string; - - /** - * The API key to use for authentication. - */ - apiKey: string; - +export interface GraphQLSitemapServiceConfig extends BaseGraphQLSitemapServiceConfig { /** * Name of the site to retrieve site paths for * Make it optional */ - siteName?: string; - - /** - * A flag for whether to include personalized routes in service output - only works on XM Cloud - * turned off by default - */ - includePersonalizedRoutes?: boolean; + siteName: string; } /** @@ -165,55 +42,13 @@ export type StaticPath = { * This list is used for SSG and Export functionality. * @mixes SearchQueryService */ -export class GraphQLSitemapService { - private graphQLClient: GraphQLClient; - - /** - * Gets the default query used for fetching the list of site pages - */ - protected get query(): string { - return defaultQuery(this.options.includePersonalizedRoutes); - } - +export class GraphQLSitemapService extends BaseGraphQLSitemapService { /** * Creates an instance of graphQL sitemap service with the provided options * @param {GraphQLSitemapServiceConfig} options instance */ constructor(public options: GraphQLSitemapServiceConfig) { - this.graphQLClient = this.getGraphQLClient(); - } - - /** - * Fetch sitemap which could be used for generation of static pages during `next export`. - * The `locale` parameter will be used in the item query, but since i18n is not supported, - * the output paths will not include a `language` property. - * @param {string} locale which application supports - * @returns an array of @see StaticPath objects - */ - async fetchExportSitemap(locale: string): Promise { - const formatPath = (path: string[]) => ({ - params: { - path, - }, - }); - - return this.fetchSitemap([locale], formatPath); - } - - /** - * Fetch sitemap which could be used for generation of static pages using SSG mode - * @param {string[]} locales locales which application supports - * @returns an array of @see StaticPath objects - */ - async fetchSSGSitemap(locales: string[]): Promise { - const formatPath = (path: string[], locale: string) => ({ - params: { - path, - }, - locale, - }); - - return this.fetchSitemap(locales, formatPath); + super(options); } /** @@ -244,104 +79,4 @@ export class GraphQLSitemapService { return ([] as StaticPath[]).concat(...paths); } - - protected async getTranformedPaths( - siteName: string, - languages: string[], - formatStaticPath: (path: string[], language: string) => StaticPath - ) { - const paths = new Array(); - await Promise.all( - languages.map(async (language) => { - if (language === '') { - throw new RangeError(languageEmptyError); - } - debug.sitemap('fetching sitemap data for %s %s', language, siteName); - const results = await this.fetchLanguageSitePaths(language, siteName); - const transformedPaths = await this.transformLanguageSitePaths( - results, - formatStaticPath, - language - ); - paths.push(...transformedPaths); - }) - ); - return paths; - } - - protected async transformLanguageSitePaths( - sitePaths: RouteListQueryResult[], - formatStaticPath: (path: string[], language: string) => StaticPath, - language: string - ): Promise { - const formatPath = (path: string) => - formatStaticPath(path.replace(/^\/|\/$/g, '').split('/'), language); - - const aggregatedPaths: StaticPath[] = []; - - sitePaths.forEach((item) => { - if (!item) return; - - aggregatedPaths.push(formatPath(item.path)); - - // check for type safety's sake - personalize may be empty depending on query type - if (item.route?.personalization?.variantIds.length) { - aggregatedPaths.push( - ...(item.route?.personalization?.variantIds.map((varId) => - formatPath(getPersonalizedRewrite(item.path, { variantId: varId })) - ) || {}) - ); - } - }); - - return aggregatedPaths; - } - - protected async fetchLanguageSitePaths( - language: string, - siteName: string - ): Promise { - const args: SiteRouteQueryVariables = { - siteName: siteName, - language: language, - pageSize: this.options.pageSize, - includedPaths: this.options.includedPaths, - excludedPaths: this.options.excludedPaths, - }; - let results: RouteListQueryResult[] = []; - let hasNext = true; - let after = ''; - - while (hasNext) { - const fetchResponse = await this.graphQLClient.request< - SiteRouteQueryResult - >(this.query, { - ...args, - after, - }); - - if (!fetchResponse?.site?.siteInfo) { - throw new RangeError(getSiteEmptyError(siteName)); - } else { - results = results.concat(fetchResponse.site.siteInfo.routes?.results); - hasNext = fetchResponse.site.siteInfo.routes?.pageInfo.hasNext; - after = fetchResponse.site.siteInfo.routes?.pageInfo.endCursor; - } - } - - return results; - } - - /** - * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default - * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you - * want to use something else. - * @returns {GraphQLClient} implementation - */ - protected getGraphQLClient(): GraphQLClient { - return new GraphQLRequestClient(this.options.endpoint, { - apiKey: this.options.apiKey, - debugger: debug.sitemap, - }); - } } diff --git a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts index ae9d047af0..968d0f2eea 100644 --- a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts @@ -1,18 +1,18 @@ import { getSiteRewrite } from '@sitecore-jss/sitecore-jss/site'; import { - GraphQLSitemapService, - GraphQLSitemapServiceConfig, + BaseGraphQLSitemapService, + BaseGraphQLSitemapServiceConfig, languageError, RouteListQueryResult, StaticPath, -} from './graphql-sitemap-service'; +} from './base-graphql-sitemap-service'; export const sitesError = 'The list of sites cannot be empty'; /** * Configuration options for @see GraphQLSitemapService instances */ -export interface MultisiteGraphQLSitemapServiceConfig extends GraphQLSitemapServiceConfig { +export interface MultisiteGraphQLSitemapServiceConfig extends BaseGraphQLSitemapServiceConfig { /** * Names of the configured sites */ @@ -25,7 +25,7 @@ export interface MultisiteGraphQLSitemapServiceConfig extends GraphQLSitemapServ * This list is used for SSG and Export functionality. * @mixes SearchQueryService */ -export class MultisiteGraphQLSitemapService extends GraphQLSitemapService { +export class MultisiteGraphQLSitemapService extends BaseGraphQLSitemapService { /** * Creates an instance of graphQL sitemap service with the provided options * @param {GraphQLSitemapServiceConfig} options instance From ad7a7297e86783631603ab9735cc3b3fb938043b Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 22 Feb 2023 11:21:08 -0500 Subject: [PATCH 6/6] comment cleanup --- CHANGELOG.md | 2 -- .../sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts | 1 - .../src/services/mutisite-graphql-sitemap-service.ts | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53131002d6..c1a0848a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,6 @@ Our versioning strategy is as follows: * `import { editingDataService } from '@sitecore-jss/sitecore-jss-nextjs/editing';` * `import { EditingRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';` -* `[sitecore-jss-nextjs]` GraphqlSitemapService's constructor accepts a sites array by default, instead of siteName. fetchExportSitemap and fetchSSGSitemap methods will now return paths with site prefix applies by default. Provide a siteName parameter into the methods to get paths for a single site without prefix. - * `[sitecore-jss-angular][templates/angular]` jss-angular package and sample has been updated to version 14. This means several changes: * JSS Angular sample is now using Ivy * IE11 no longer supported by JSS Angular diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index d6a813a265..bcd2402975 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -21,7 +21,6 @@ export function getSiteEmptyError(siteName: string) { export interface GraphQLSitemapServiceConfig extends BaseGraphQLSitemapServiceConfig { /** * Name of the site to retrieve site paths for - * Make it optional */ siteName: string; } diff --git a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts index 968d0f2eea..5f23d304ac 100644 --- a/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/mutisite-graphql-sitemap-service.ts @@ -28,7 +28,7 @@ export interface MultisiteGraphQLSitemapServiceConfig extends BaseGraphQLSitemap export class MultisiteGraphQLSitemapService extends BaseGraphQLSitemapService { /** * Creates an instance of graphQL sitemap service with the provided options - * @param {GraphQLSitemapServiceConfig} options instance + * @param {MultisiteGraphQLSitemapServiceConfig} options instance */ constructor(public options: MultisiteGraphQLSitemapServiceConfig) { super(options);