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 25c3f0cf7f..2e6426ba81 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 @@ -1,8 +1,13 @@ -import { GraphQLSitemapService } from '@sitecore-jss/sitecore-jss-nextjs'; +import { + GraphQLSitemapService, + StaticPath, + constants, + siteInfo, +} from '@sitecore-jss/sitecore-jss-nextjs'; import config from 'temp/config'; import { SitemapFetcherPlugin } from '..'; import { GetStaticPathsContext } from 'next'; -import { StaticPath, constants } from '@sitecore-jss/sitecore-jss-nextjs'; +import siteResolver from 'lib/site-resolver'; class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { _graphqlSitemapService: GraphQLSitemapService; @@ -11,7 +16,7 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { this._graphqlSitemapService = new GraphQLSitemapService({ endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, - siteName: config.jssAppName, + sites: siteResolver.sites.map((site: siteInfo) => site.name), }); } 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 8d26e06aec..f517e37496 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 @@ -1,4 +1,4 @@ -import { expect } from 'chai'; +import { expect } from 'chai'; import nock from 'nock'; import { getSiteEmptyError, @@ -51,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 siteName = 'site-name'; + const sites = ['site-name']; afterEach(() => { nock.cleanAll(); @@ -86,7 +86,7 @@ describe('GraphQLSitemapService', () => { it('should work when 1 language is requested', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal(sitemapServiceResult); @@ -155,7 +155,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - siteName, + sites, includedPaths, excludedPaths, }); @@ -181,7 +181,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - siteName, + sites, includePersonalizedRoutes: true, }); const sitemap = await service.fetchSSGSitemap([lang]); @@ -227,6 +227,236 @@ 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'; @@ -297,7 +527,7 @@ describe('GraphQLSitemapService', () => { }, }); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap([lang1, lang2]); expect(sitemap).to.deep.equal([ @@ -386,7 +616,7 @@ describe('GraphQLSitemapService', () => { }, }); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchSSGSitemap([lang]); expect(sitemap).to.deep.equal([ @@ -408,17 +638,17 @@ describe('GraphQLSitemapService', () => { }); it('should throw error if valid language is not provided', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + 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, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); nock(endpoint) .post('/', (body) => { - return body.variables.siteName === siteName; + return body.variables.siteName === sites[0]; }) .reply(200, { data: { @@ -428,14 +658,14 @@ describe('GraphQLSitemapService', () => { }, }); await service.fetchSSGSitemap(['en']).catch((error: RangeError) => { - expect(error.message).to.equal(getSiteEmptyError(siteName)); + expect(error.message).to.equal(getSiteEmptyError(sites[0])); }); }); it('should throw error if empty language is provided', async () => { mockPathsRequest(); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + 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'); }); @@ -453,7 +683,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - siteName, + sites, pageSize: customPageSize, }); const sitemap = await service.fetchSSGSitemap(['ua']); @@ -474,7 +704,7 @@ describe('GraphQLSitemapService', () => { const service = new GraphQLSitemapService({ endpoint, apiKey, - siteName, + sites, pageSize: undefined, }); const sitemap = await service.fetchSSGSitemap(['ua']); @@ -486,7 +716,7 @@ describe('GraphQLSitemapService', () => { it('should work if sitemap has 0 pages', async () => { mockPathsRequest([]); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + 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; @@ -497,7 +727,7 @@ describe('GraphQLSitemapService', () => { .post('/', /DefaultSitemapQuery/gi) .reply(500, 'Error 😥'); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + 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 😥'); @@ -532,54 +762,43 @@ describe('GraphQLSitemapService', () => { describe('Fetch sitemap in export mode', () => { it('should fetch sitemap', async () => { mockPathsRequest(); - - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); const sitemap = await service.fetchExportSitemap('ua'); - expect(sitemap).to.deep.equal(expectedExportSitemap); return expect(nock.isDone()).to.be.true; }); - it('should work if endpoint returns 0 pages', async () => { mockPathsRequest([]); - - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ 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 GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ 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 GraphQLSitemapService({ endpoint, apiKey, siteName }); + 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; }); }); - it('should throw error if query returns nothing for a provided site name', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + const service = new GraphQLSitemapService({ endpoint, apiKey, sites }); nock(endpoint) .post('/', (body) => { - return body.variables.siteName === siteName; + return body.variables.siteName === sites[0]; }) .reply(200, { data: { @@ -589,17 +808,15 @@ describe('GraphQLSitemapService', () => { }, }); await service.fetchExportSitemap('en').catch((error: RangeError) => { - expect(error.message).to.equal(getSiteEmptyError(siteName)); + expect(error.message).to.equal(getSiteEmptyError(sites[0])); }); }); - it('should provide a default GraphQL client', () => { const service = new TestService({ endpoint, apiKey, - siteName, + sites, }); - const graphQLClient = service.client as GraphQLClient; const graphQLRequestClient = service.client as GraphQLRequestClient; // eslint-disable-next-line no-unused-expressions 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 4ec7453d0f..dba978ea96 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -1,9 +1,11 @@ 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'; /** * @param {string} siteName to inject into error text @@ -44,7 +46,7 @@ query ${usesPersonalize ? 'PersonalizeSitemapQuery' : 'DefaultSitemapQuery'}( hasNext } results { - path: routePath + path: routePath ${ usesPersonalize ? ` @@ -59,7 +61,7 @@ query ${usesPersonalize ? 'PersonalizeSitemapQuery' : 'DefaultSitemapQuery'}( } } } -} +} `; /** * type for input variables for the site routes query @@ -123,7 +125,8 @@ export type RouteListQueryResult = { /** * Configuration options for @see GraphQLSitemapService instances */ -export interface GraphQLSitemapServiceConfig extends Omit { +export interface GraphQLSitemapServiceConfig + extends Omit { /** * Your Graphql endpoint */ @@ -134,6 +137,11 @@ export interface GraphQLSitemapServiceConfig extends Omit StaticPath ): Promise { + const paths = new Array(); if (!languages.length) { throw new RangeError(languageError); } - // Fetch paths using all locales - const paths = await Promise.all( - languages.map((language) => { - if (language === '') { - throw new RangeError(languageEmptyError); - } - debug.sitemap('fetching sitemap data for %s', language); - return this.fetchLanguageSitePaths(language).then((results) => - this.transformLanguageSitePaths(results, formatStaticPath, language) - ); - }) - ); - - // merge promises results into single result + + // 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 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); + }) + ); + } + return ([] as StaticPath[]).concat(...paths); } protected async transformLanguageSitePaths( sitePaths: RouteListQueryResult[], formatStaticPath: (path: string[], language: string) => StaticPath, - language: string + language: string, + multiSiteName?: 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)); + if (!multiSiteName) { + aggregatedPaths.push(formatPath(item.path)); + } else { + aggregatedPaths.push(formatPath(getSiteRewrite(item.path, { siteName: multiSiteName }))); + } + // 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 })) - ) - ); + 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 })) + ) + ); } }); return aggregatedPaths; } - protected async fetchLanguageSitePaths(language: string): Promise { + protected async fetchLanguageSitePaths( + language: string, + siteName: string + ): Promise { const args: SiteRouteQueryVariables = { - siteName: this.options.siteName, + siteName: siteName, language: language, pageSize: this.options.pageSize, includedPaths: this.options.includedPaths, @@ -287,7 +333,7 @@ export class GraphQLSitemapService { }); if (!fetchResponse?.site?.siteInfo) { - throw new RangeError(getSiteEmptyError(this.options.siteName)); + throw new RangeError(getSiteEmptyError(siteName)); } else { results = results.concat(fetchResponse.site.siteInfo.routes?.results); hasNext = fetchResponse.site.siteInfo.routes?.pageInfo.hasNext;