Skip to content

Commit 07319d0

Browse files
moonmeisterLB
and
LB
authored
feat(gatsby-plugin-sitemap): handle different query structures and allow custom siteUrl resolution (#21948)
* fix: handle allSitePage.nodes query stil. feat: allow custom resolver for siteUrl * docs(plugin-sitemap): add doces for updates * Update packages/gatsby-plugin-sitemap/README.md Co-Authored-By: LB <[email protected]> Co-authored-by: LB <[email protected]>
1 parent 306cadd commit 07319d0

File tree

4 files changed

+168
-61
lines changed

4 files changed

+168
-61
lines changed

packages/gatsby-plugin-sitemap/README.md

+12-9
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ The `defaultOptions` [here](https://github.com/gatsbyjs/gatsby/blob/master/packa
2727

2828
The options are as follows:
2929

30-
- `query` (GraphQL Query) The query for the data you need to generate the sitemap. It's required to get the `site.siteMetadata.siteUrl`. If you override the query, you probably will need to set a `serializer` to return the correct data for the sitemap.
30+
- `query` (GraphQL Query) The query for the data you need to generate the sitemap. It's required to get the site's URL, if you are not fetching it from `site.siteMetadata.siteUrl`, you will need to set a custom `resolveSiteUrl` function. If you override the query, you probably will also need to set a `serializer` to return the correct data for the sitemap. Due to how this plugin was built it is currently expected/required to fetch the page paths from `allSitePage`, but you may use the `allSitePage.edges.node` or `allSitePage.nodes` query structure.
3131
- `output` (string) The filepath and name. Defaults to `/sitemap.xml`.
3232
- `exclude` (array of strings) An array of paths to exclude from the sitemap.
3333
- `createLinkInHead` (boolean) Whether to populate the `<head>` of your site with a link to the sitemap.
3434
- `serialize` (function) Takes the output of the data query and lets you return an array of sitemap entries.
35+
- `resolveSiteUrl` (function) Takes the output of the data query and lets you return the site URL.
3536

3637
We _ALWAYS_ exclude the following pages: `/dev-404-page`,`/404` &`/offline-plugin-app-shell-fallback`, this cannot be changed.
3738

@@ -53,24 +54,26 @@ plugins: [
5354
exclude: [`/category/*`, `/path/to/page`],
5455
query: `
5556
{
56-
site {
57-
siteMetadata {
57+
wp {
58+
generalSettings {
5859
siteUrl
5960
}
6061
}
6162
6263
allSitePage {
63-
edges {
64-
node {
65-
path
66-
}
64+
node {
65+
path
6766
}
6867
}
6968
}`,
69+
resolveSiteUrl: ({site, allSitePage}) => {
70+
//Alternativly, you may also pass in an environment variable (or any location) at the beginning of your `gatsby-config.js`.
71+
return site.wp.generalSettings.siteUrl
72+
},
7073
serialize: ({ site, allSitePage }) =>
71-
allSitePage.edges.map(edge => {
74+
allSitePage.nodes.map(node => {
7275
return {
73-
url: site.siteMetadata.siteUrl + edge.node.path,
76+
url: `${site.wp.generalSettings.siteUrl}${node.path}`,
7477
changefreq: `daily`,
7578
priority: 0.7,
7679
}

packages/gatsby-plugin-sitemap/src/__tests__/internals.js

+64-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
const {
2-
runQuery,
2+
filterQuery,
33
defaultOptions: { serialize },
44
} = require(`../internals`)
55

66
beforeEach(() => {
77
global.__PATH_PREFIX__ = ``
88
})
99

10+
const verifyUrlsExistInResults = (results, urls) => {
11+
expect(results.map(result => result.url)).toEqual(urls)
12+
}
13+
1014
describe(`results using default settings`, () => {
1115
const generateQueryResultsMock = (
1216
{ siteUrl } = { siteUrl: `http://dummy.url` }
@@ -36,18 +40,15 @@ describe(`results using default settings`, () => {
3640
}
3741
}
3842

39-
const verifyUrlsExistInResults = (results, urls) => {
40-
expect(results.map(result => result.url)).toEqual(urls)
41-
}
42-
4343
const runTests = (pathPrefix = ``) => {
4444
beforeEach(() => {
4545
global.__PATH_PREFIX__ = pathPrefix
4646
})
4747

4848
it(`prepares all urls correctly`, async () => {
4949
const graphql = () => Promise.resolve(generateQueryResultsMock())
50-
const queryRecords = await runQuery(graphql, ``, [], pathPrefix)
50+
const results = await graphql(``)
51+
const queryRecords = filterQuery(results, [], pathPrefix)
5152
const urls = serialize(queryRecords)
5253

5354
verifyUrlsExistInResults(urls, [
@@ -61,7 +62,9 @@ describe(`results using default settings`, () => {
6162
Promise.resolve(
6263
generateQueryResultsMock({ siteUrl: `http://dummy.url/` })
6364
)
64-
const queryRecords = await runQuery(graphql, ``, [], pathPrefix)
65+
66+
const data = await graphql(``)
67+
const queryRecords = filterQuery(data, [], pathPrefix)
6568
const urls = serialize(queryRecords)
6669

6770
verifyUrlsExistInResults(urls, [
@@ -72,15 +75,17 @@ describe(`results using default settings`, () => {
7275

7376
it(`excludes pages without trailing slash`, async () => {
7477
const graphql = () => Promise.resolve(generateQueryResultsMock())
75-
const queryRecords = await runQuery(graphql, ``, [`/page-2`], pathPrefix)
78+
const data = await graphql(``)
79+
const queryRecords = filterQuery(data, [`/page-2`], pathPrefix)
7680
const urls = serialize(queryRecords)
7781

7882
verifyUrlsExistInResults(urls, [`http://dummy.url${pathPrefix}/page-1`])
7983
})
8084

8185
it(`excludes pages with trailing slash`, async () => {
8286
const graphql = () => Promise.resolve(generateQueryResultsMock())
83-
const queryRecords = await runQuery(graphql, ``, [`/page-2/`], pathPrefix)
87+
const data = await graphql(``)
88+
const queryRecords = filterQuery(data, [`/page-2/`], pathPrefix)
8489
const urls = serialize(queryRecords)
8590

8691
verifyUrlsExistInResults(urls, [`http://dummy.url${pathPrefix}/page-1`])
@@ -92,7 +97,8 @@ describe(`results using default settings`, () => {
9297
expect.assertions(1)
9398

9499
try {
95-
await runQuery(graphql, ``, [], pathPrefix)
100+
const data = await graphql(``)
101+
filterQuery(data, [], pathPrefix)
96102
} catch (err) {
97103
expect(err.message).toEqual(
98104
expect.stringContaining(`SiteMetaData 'siteUrl' property is required`)
@@ -109,3 +115,51 @@ describe(`results using default settings`, () => {
109115
runTests(`/path-prefix`)
110116
})
111117
})
118+
119+
describe(`results using non default alternatives`, () => {
120+
const generateQueryResultsMockNodes = (
121+
{ siteUrl } = { siteUrl: `http://dummy.url` }
122+
) => {
123+
return {
124+
data: {
125+
site: {
126+
siteMetadata: {
127+
siteUrl: siteUrl,
128+
},
129+
},
130+
allSitePage: {
131+
nodes: [
132+
{
133+
path: `/page-1`,
134+
},
135+
{
136+
path: `/page-2`,
137+
},
138+
],
139+
},
140+
},
141+
}
142+
}
143+
144+
it(`handles allSitePage.nodes type query properly`, async () => {
145+
const graphql = () => Promise.resolve(generateQueryResultsMockNodes())
146+
const results = await graphql(``)
147+
const queryRecords = filterQuery(results, [], ``)
148+
const urls = serialize(queryRecords)
149+
150+
verifyUrlsExistInResults(urls, [
151+
`http://dummy.url/page-1`,
152+
`http://dummy.url/page-2`,
153+
])
154+
})
155+
156+
it(`handles custom siteUrl Resolver Properly type query properly`, async () => {
157+
const customUrl = `https://another.dummy.url`
158+
const customSiteResolver = () => customUrl
159+
const graphql = () => Promise.resolve(generateQueryResultsMockNodes())
160+
const results = await graphql(``)
161+
const queryRecords = filterQuery(results, [], ``, customSiteResolver)
162+
163+
expect(queryRecords.site.siteMetadata.siteUrl).toEqual(customUrl)
164+
})
165+
})

packages/gatsby-plugin-sitemap/src/gatsby-node.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from "path"
22
import sitemap from "sitemap"
33
import {
44
defaultOptions,
5-
runQuery,
5+
filterQuery,
66
writeFile,
77
renameFile,
88
withoutTrailingSlash,
@@ -18,7 +18,15 @@ exports.onPostBuild = async (
1818
delete options.plugins
1919
delete options.createLinkInHead
2020

21-
const { query, serialize, output, exclude, hostname, ...rest } = {
21+
const {
22+
query,
23+
serialize,
24+
output,
25+
exclude,
26+
hostname,
27+
resolveSiteUrl,
28+
...rest
29+
} = {
2230
...defaultOptions,
2331
...options,
2432
}
@@ -28,8 +36,15 @@ exports.onPostBuild = async (
2836
// Paths we're excluding...
2937
const excludeOptions = exclude.concat(defaultOptions.exclude)
3038

31-
const queryRecords = await runQuery(graphql, query, excludeOptions, basePath)
32-
const urls = serialize(queryRecords)
39+
const queryRecords = await graphql(query)
40+
41+
const filteredRecords = filterQuery(
42+
queryRecords,
43+
excludeOptions,
44+
basePath,
45+
resolveSiteUrl
46+
)
47+
const urls = serialize(filteredRecords)
3348

3449
if (!rest.sitemapSize || urls.length <= rest.sitemapSize) {
3550
const map = sitemap.createSitemap(rest)
@@ -41,7 +56,7 @@ exports.onPostBuild = async (
4156
site: {
4257
siteMetadata: { siteUrl },
4358
},
44-
} = queryRecords
59+
} = filteredRecords
4560
return new Promise(resolve => {
4661
// sitemap-index.xml is default file name. (https://git.io/fhNgG)
4762
const indexFilePath = path.join(

packages/gatsby-plugin-sitemap/src/internals.js

+72-37
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,62 @@ export const withoutTrailingSlash = path =>
88
export const writeFile = pify(fs.writeFile)
99
export const renameFile = pify(fs.rename)
1010

11-
export const runQuery = (handler, query, excludes, pathPrefix) =>
12-
handler(query).then(r => {
13-
if (r.errors) {
14-
throw new Error(r.errors.join(`, `))
15-
}
11+
export function filterQuery(
12+
results,
13+
excludes,
14+
pathPrefix,
15+
resolveSiteUrl = defaultOptions.resolveSiteUrl
16+
) {
17+
const { errors, data } = results
1618

17-
// Removing excluded paths
18-
r.data.allSitePage.edges = r.data.allSitePage.edges.filter(
19-
page =>
20-
!excludes.some(excludedRoute =>
21-
minimatch(
22-
withoutTrailingSlash(page.node.path),
23-
withoutTrailingSlash(excludedRoute)
24-
)
25-
)
26-
)
19+
if (errors) {
20+
throw new Error(errors.join(`, `))
21+
}
2722

28-
// Add path prefix
29-
r.data.allSitePage.edges = r.data.allSitePage.edges.map(page => {
30-
page.node.path = (pathPrefix + page.node.path).replace(/^\/\//g, `/`)
31-
return page
32-
})
23+
let { allPages, originalType } = getNodes(data.allSitePage)
3324

34-
// siteUrl Validation
35-
if (
36-
!r.data.site.siteMetadata.siteUrl ||
37-
r.data.site.siteMetadata.siteUrl.trim().length == 0
38-
) {
39-
throw new Error(
40-
`SiteMetaData 'siteUrl' property is required and cannot be left empty. Check out the documentation to see a working example: https://www.gatsbyjs.org/packages/gatsby-plugin-sitemap/#how-to-use`
25+
// Removing excluded paths
26+
allPages = allPages.filter(
27+
page =>
28+
!excludes.some(excludedRoute =>
29+
minimatch(
30+
withoutTrailingSlash(page.path),
31+
withoutTrailingSlash(excludedRoute)
32+
)
4133
)
42-
}
34+
)
35+
36+
// Add path prefix
37+
allPages = allPages.map(page => {
38+
page.path = (pathPrefix + page.path).replace(/^\/\//g, `/`)
39+
return page
40+
})
41+
42+
// siteUrl Validation
4343

44-
// remove trailing slash of siteUrl
45-
r.data.site.siteMetadata.siteUrl = withoutTrailingSlash(
46-
r.data.site.siteMetadata.siteUrl
44+
let siteUrl = resolveSiteUrl(data)
45+
46+
if (!siteUrl || siteUrl.trim().length == 0) {
47+
throw new Error(
48+
`SiteMetaData 'siteUrl' property is required and cannot be left empty. Check out the documentation to see a working example: https://www.gatsbyjs.org/packages/gatsby-plugin-sitemap/#how-to-use`
4749
)
50+
}
4851

49-
return r.data
50-
})
52+
// remove trailing slash of siteUrl
53+
siteUrl = withoutTrailingSlash(siteUrl)
54+
55+
return {
56+
allSitePage: {
57+
[originalType]:
58+
originalType === `nodes`
59+
? allPages
60+
: allPages.map(page => {
61+
return { node: page }
62+
}),
63+
},
64+
site: { siteMetadata: { siteUrl } },
65+
}
66+
}
5167

5268
export const defaultOptions = {
5369
query: `
@@ -74,12 +90,31 @@ export const defaultOptions = {
7490
`/offline-plugin-app-shell-fallback`,
7591
],
7692
createLinkInHead: true,
77-
serialize: ({ site, allSitePage }) =>
78-
allSitePage.edges.map(edge => {
93+
serialize: ({ site, allSitePage }) => {
94+
const { allPages } = getNodes(allSitePage)
95+
return allPages?.map(page => {
7996
return {
80-
url: site.siteMetadata.siteUrl + edge.node.path,
97+
url: `${site.siteMetadata?.siteUrl ?? ``}${page.path}`,
8198
changefreq: `daily`,
8299
priority: 0.7,
83100
}
84-
}),
101+
})
102+
},
103+
resolveSiteUrl: data => data.site.siteMetadata.siteUrl,
104+
}
105+
106+
function getNodes(results) {
107+
if (`nodes` in results) {
108+
return { allPages: results.nodes, originalType: `nodes` }
109+
}
110+
111+
if (`edges` in results) {
112+
return {
113+
allPages: results?.edges?.map(edge => edge.node),
114+
originalType: `edges`,
115+
}
116+
}
117+
throw new Error(
118+
`[gatsby-plugin-sitemap]: Plugin is unsure how to handle the results of your query, you'll need to write custom page filter and serilizer in your gatsby conig`
119+
)
85120
}

0 commit comments

Comments
 (0)