Skip to content

Commit

Permalink
Feature/487176 implementation sitemap (#991)
Browse files Browse the repository at this point in the history
* #487176 added sitemap middleware

* #487176 implemented sitemap middleware

* #487176 refactoring sitemap service

* #487176 refactoring and added unit test for sitemap service

* #487176 refactoring

* #487176 removed deprecated package(request)
  • Loading branch information
matkovskyi committed Apr 22, 2022
1 parent a40db2a commit 723a382
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 4 deletions.
Expand Up @@ -2,4 +2,4 @@
"dependencies": {
"bootstrap": "^5.1.3"
}
}
}
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { RedirectsMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/middleware';
import { RedirectsMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/edge';
import config from 'temp/config';
import { MiddlewarePlugin } from '..';

Expand All @@ -21,7 +21,7 @@ class RedirectsPlugin implements MiddlewarePlugin {
* @returns Promise<NextResponse>
*/
async exec(req: NextRequest): Promise<NextResponse> {
return this.redirectsMiddleware.getHandler(req);
return this.redirectsMiddleware.getHandler()(req);
}
}

Expand Down
@@ -0,0 +1,16 @@
const sitemapPlugin = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
async rewrites() {
return [
...await nextConfig.rewrites(),
// sitemap route
{
source: '/sitemap:id([\\w-]{0,}).xml',
destination: '/api/sitemap'
},
];
},
});
};

module.exports = sitemapPlugin;
@@ -0,0 +1,37 @@
import { AxiosResponse } from 'axios';
import type { NextApiRequest, NextApiResponse } from 'next';
import config from 'temp/config';
import { GraphQLSitemapService } from '@sitecore-jss/sitecore-jss/site';
import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss';

const ABSOLUTE_URL_REGEXP = '^(?:[a-z]+:)?//';

const sitemapApi = async (req: NextApiRequest, res: NextApiResponse): Promise<NextApiResponse | void> => {
const { query: { id } } = req;
// create sitemap graphql service
const sitemapService = new GraphQLSitemapService({
endpoint: config.graphQLEndpoint,
apiKey: config.sitecoreApiKey,
siteName: config.jssAppName,
});

const sitemapPath = await sitemapService.getSitemap(id as string);

if (sitemapPath) {
const isAbsoluteUrl = sitemapPath.match(ABSOLUTE_URL_REGEXP);
const sitemapUrl = isAbsoluteUrl ? sitemapPath : `${config.sitecoreApiHost}${sitemapPath}`;
res.setHeader('Content-Type', 'text/xml;charset=utf-8');

return new AxiosDataFetcher().get(sitemapUrl, {
responseType: 'stream',
})
.then((response: AxiosResponse) => {
response.data.pipe(res);
})
.catch(() => res.redirect('/404'));
}

res.redirect('/404');
};

export default sitemapApi;
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/edge.d.ts
@@ -0,0 +1 @@
export * from './types/edge/index';
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/edge.js
@@ -0,0 +1 @@
module.exports = require('./dist/cjs/edge/index');
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/edge/index.ts
@@ -0,0 +1 @@
export { RedirectsMiddleware } from './redirects-middleware';
1 change: 0 additions & 1 deletion packages/sitecore-jss-nextjs/src/middleware/index.ts
Expand Up @@ -4,4 +4,3 @@ export {
EditingRenderMiddleware,
EditingRenderMiddlewareConfig,
} from './editing-render-middleware';
export { RedirectsMiddleware } from './redirects-middleware';
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/tsconfig.json
Expand Up @@ -30,6 +30,7 @@
"typings",
"dist",
"middleware.d.ts",
"edge.d.ts",
"src/tests/*",
"src/testData/*",
"**/*.test.ts",
Expand Down
105 changes: 105 additions & 0 deletions packages/sitecore-jss/src/site/graphql-sitemap-service.test.ts
@@ -0,0 +1,105 @@
import { expect } from 'chai';
import nock from 'nock';
import { GraphQLSitemapService } from './graphql-sitemap-service';
import { siteNameError } from '../constants';

const sitemapQueryResultNull = {
site: {
siteInfo: null,
},
};

describe('GraphQLSitemapService', () => {
const endpoint = 'http://site';
const apiKey = 'some-api-key';
const siteName = 'site-name';
const mockSitemap = ['sitemap.xml'];
const mockSitemaps = ['sitemap-1.xml', 'sitemap-2.xml', 'sitemap-3.xml'];

afterEach(() => {
nock.cleanAll();
});

const mockSitemapRequest = (sitemapUrls?: string[]) => {
nock(endpoint)
.post('/')
.reply(
200,
siteName
? {
data: {
site: {
siteInfo: {
sitemap: sitemapUrls,
},
},
},
}
: {
data: sitemapQueryResultNull,
}
);
};

describe('Fetch sitemap', () => {
it('should get error if sitemap has empty sitename', async () => {
mockSitemapRequest();

const service = new GraphQLSitemapService({ endpoint, apiKey, siteName: '' });
await service.fetchSitemaps().catch((error: Error) => {
expect(error.message).to.equal(siteNameError);
});

return expect(nock.isDone()).to.be.false;
});

it('should fetch sitemap', async () => {
mockSitemapRequest(mockSitemap);

const service = new GraphQLSitemapService({ endpoint, apiKey, siteName });
const sitemaps = await service.fetchSitemaps();

expect(sitemaps.length).to.equal(1);
expect(sitemaps).to.deep.equal(mockSitemap);

return expect(nock.isDone()).to.be.true;
});

it('should fetch sitemaps', async () => {
mockSitemapRequest(mockSitemaps);

const service = new GraphQLSitemapService({ endpoint, apiKey, siteName });
const sitemaps = await service.fetchSitemaps();

expect(sitemaps.length).to.equal(3);
expect(sitemaps).to.deep.equal(mockSitemaps);

return expect(nock.isDone()).to.be.true;
});

it('should get exists sitemap', async () => {
const mockIdSitemap = '-3';
mockSitemapRequest(mockSitemaps);

const service = new GraphQLSitemapService({ endpoint, apiKey, siteName });
const sitemap = await service.getSitemap(mockIdSitemap);

expect(sitemap).to.deep.equal(mockSitemaps[2]);

return expect(nock.isDone()).to.be.true;
});

it('should get null if sitemap not exists', async () => {
const mockIdSitemap = '-5';
mockSitemapRequest(mockSitemaps);

const service = new GraphQLSitemapService({ endpoint, apiKey, siteName });
const sitemap = await service.getSitemap(mockIdSitemap);

// eslint-disable-next-line no-unused-expressions
expect(sitemap).to.be.undefined;

return expect(nock.isDone()).to.be.true;
});
});
});
102 changes: 102 additions & 0 deletions packages/sitecore-jss/src/site/graphql-sitemap-service.ts
@@ -0,0 +1,102 @@
import { GraphQLClient, GraphQLRequestClient } from '../graphql';
import { siteNameError } from '../constants';
import debug from '../debug';

const PREFIX_NAME_SITEMAP = 'sitemap';

// The default query for request sitemaps
const defaultQuery = /* GraphQL */ `
query SitemapQuery($siteName: String!) {
site {
siteInfo(site: $siteName) {
sitemap
}
}
}
`;

export type GraphQLSitemapServiceConfig = {
/**
* Your Graphql endpoint
*/
endpoint: string;
/**
* The API key to use for authentication
*/
apiKey: string;
/**
* The JSS application name
*/
siteName: string;
};

/**
* The schema of data returned in response to sitemaps request
*/
export type SitemapQueryResult = { site: { siteInfo: { sitemap: string[] } } };

/**
* Service that fetch the sitemaps data using Sitecore's GraphQL API.
*/
export class GraphQLSitemapService {
private graphQLClient: GraphQLClient;

protected get query(): string {
return defaultQuery;
}

/**
* Creates an instance of graphQL sitemaps service with the provided options
* @param {GraphQLSitemapServiceConfig} options instance
*/
constructor(public options: GraphQLSitemapServiceConfig) {
this.graphQLClient = this.getGraphQLClient();
}

/**
* Fetch list of sitemaps for the site
* @returns {string[]} list of sitemap paths
* @throws {Error} if the siteName is empty.
*/
async fetchSitemaps(): Promise<string[]> {
const siteName: string = this.options.siteName;

if (!siteName) {
throw new Error(siteNameError);
}

const sitemapResult: Promise<SitemapQueryResult> = this.graphQLClient.request(this.query, {
siteName,
});
try {
return sitemapResult.then((result: SitemapQueryResult) => result.site.siteInfo.sitemap);
} catch (e) {
return Promise.reject(e);
}
}

/**
* Get sitemap file path for sitemap id
* @param {string} id the sitemap id (can be empty for default 'sitemap.xml' file)
* @returns {string | undefined} the sitemap file path or undefined if one doesn't exist
*/
async getSitemap(id: string): Promise<string | undefined> {
const searchSitemap = `${PREFIX_NAME_SITEMAP}${id}.xml`;
const sitemaps = await this.fetchSitemaps();

return sitemaps.find((sitemap: string) => sitemap.includes(searchSitemap));
}

/**
* 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,
});
}
}
6 changes: 6 additions & 0 deletions packages/sitecore-jss/src/site/index.ts
Expand Up @@ -12,3 +12,9 @@ export {
GraphQLRedirectsService,
GraphQLRedirectsServiceConfig,
} from './graphql-redirects-service';

export {
SitemapQueryResult,
GraphQLSitemapService,
GraphQLSitemapServiceConfig,
} from './graphql-sitemap-service';

0 comments on commit 723a382

Please sign in to comment.