-
-
Notifications
You must be signed in to change notification settings - Fork 60
/
gatsby-node.js
317 lines (257 loc) · 9.86 KB
/
gatsby-node.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import path from 'path'
import url from 'url'
import fs from 'fs-extra'
import _ from 'lodash'
import defaultOptions from './defaults'
import Manager from './SiteMapManager'
const PUBLICPATH = `./public`
const INDEXFILE = `/sitemap.xml`
const RESOURCESFILE = `/sitemap-:resource.xml`
const XSLFILE = path.resolve(__dirname, `./static/sitemap.xsl`)
const DEFAULTQUERY = `{
allSitePage {
edges {
node {
id
slug: path
url: path
}
}
}
site {
siteMetadata {
siteUrl
}
}
}`
const DEFAULTMAPPING = {
allSitePage: {
sitemap: `pages`,
},
}
let siteUrl
const copyStylesheet = async ({ siteUrl, pathPrefix, indexOutput }) => {
const siteRegex = /(\{\{blog-url\}\})/g
// Get our stylesheet template
const data = await fs.readFile(XSLFILE)
// Replace the `{{blog-url}}` variable with our real site URL
const sitemapStylesheet = data.toString().replace(siteRegex, url.resolve(siteUrl, path.join(pathPrefix, indexOutput)))
// Save the updated stylesheet to the public folder, so it will be
// available for the xml sitemap files
await fs.writeFile(path.join(PUBLICPATH, `sitemap.xsl`), sitemapStylesheet)
}
const serializeMarkdownNodes = (node) => {
if (!node.fields.slug) {
throw Error(`\`slug\` is a required field`)
}
node.slug = node.fields.slug
delete node.fields.slug
if (node.frontmatter) {
if (node.frontmatter.published_at) {
node.published_at = node.frontmatter.published_at
delete node.frontmatter.published_at
}
if (node.frontmatter.feature_image) {
node.feature_image = node.frontmatter.feature_image
delete node.frontmatter.feature_image
}
}
return node
}
// Compare our node paths with the ones that Gatsby has generated and updated them
// with the "real" used ones.
const getNodePath = (node, allSitePage) => {
if (!node.path) {
return node
}
const slugRegex = new RegExp(`${node.path.replace(/\/$/, ``)}$`, `gi`)
for (let page of allSitePage.edges) {
if (page.node && page.node.url && page.node.url.replace(/\/$/, ``).match(slugRegex)) {
node.path = page.node.url
break
}
}
return node
}
// Add all other URLs that Gatsby generated, using siteAllPage,
// but we didn't fetch with our queries
const addPageNodes = (parsedNodesArray, allSiteNodes, siteUrl) => {
const [parsedNodes] = parsedNodesArray
const pageNodes = []
const addedPageNodes = { pages: [] }
const usedNodes = allSiteNodes.filter(({ node }) => {
let foundOne
for (let type in parsedNodes) {
parsedNodes[type].forEach(((fetchedNode) => {
if (node.url === fetchedNode.node.path) {
foundOne = true
}
}))
}
return foundOne
})
const remainingNodes = _.difference(allSiteNodes, usedNodes)
remainingNodes.forEach(({ node }) => {
addedPageNodes.pages.push({
url: url.resolve(siteUrl, node.url),
node: node,
})
})
pageNodes.push(addedPageNodes)
return pageNodes
}
const serializeSources = (mapping) => {
let sitemaps = []
for (let resourceType in mapping) {
sitemaps.push(mapping[resourceType])
}
sitemaps = _.map(sitemaps, (source) => {
// Ignore the key and only return the name and
// source as we need those to create the index
// and the belonging sources accordingly
return {
name: source.name ? source.name : source.sitemap,
sitemap: source.sitemap || `pages`,
}
})
sitemaps = _.uniqBy(sitemaps, `name`)
return sitemaps
}
const runQuery = (handler, { query, exclude }) => handler(query).then((r) => {
if (r.errors) {
throw new Error(r.errors.join(`, `))
}
for (let source in r.data) {
// Removing excluded paths
if (r.data[source] && r.data[source].edges && r.data[source].edges.length) {
r.data[source].edges = r.data[source].edges.filter(({ node }) => !exclude.some((excludedRoute) => {
const slug = (source === `allMarkdownRemark` || source === `allMdx`) ? node.fields.slug.replace(/^\/|\/$/, ``) : node.slug.replace(/^\/|\/$/, ``)
excludedRoute = typeof excludedRoute === `object` ? excludedRoute : excludedRoute.replace(/^\/|\/$/, ``)
// test if the passed regular expression is valid
if (typeof excludedRoute === `object`) {
let excludedRouteIsValidRegEx = true
try {
new RegExp(excludedRoute)
} catch (e) {
excludedRouteIsValidRegEx = false
}
if (!excludedRouteIsValidRegEx) {
throw new Error(`Excluded route is not a valid RegExp: `, excludedRoute)
}
return excludedRoute.test(slug)
} else {
return slug.indexOf(excludedRoute) >= 0
}
}))
}
}
return r.data
})
const serialize = ({ ...sources } = {},{ site, allSitePage }, mapping) => {
const nodes = []
const sourceObject = {}
siteUrl = site.siteMetadata.siteUrl
for (let type in sources) {
if (mapping[type] && mapping[type].sitemap) {
const currentSource = sources[type] ? sources[type] : []
if (currentSource) {
sourceObject[mapping[type].sitemap] = sourceObject[mapping[type].sitemap] || []
currentSource.edges.map(({ node }) => {
if (!node) {
return
}
if (type === `allMarkdownRemark` || type === `allMdx`) {
node = serializeMarkdownNodes(node)
}
// if a mapping path is set, e. g. `/blog/tag` for tags, update the path
// to reflect this. This prevents mapping issues, when we later update
// the path with the Gatsby generated one in `getNodePath`
if (mapping[type].path) {
node.path = path.resolve(mapping[type].path, node.slug)
} else {
node.path = node.slug
}
// get the real path for the node, which is generated by Gatsby
node = getNodePath(node, allSitePage)
sourceObject[mapping[type].sitemap].push({
url: url.resolve(siteUrl, node.path),
node: node,
})
})
}
}
}
nodes.push(sourceObject)
const pageNodes = addPageNodes(nodes, allSitePage.edges, siteUrl)
const allNodes = _.merge(nodes, pageNodes)
return allNodes
}
exports.onPostBuild = async ({ graphql, pathPrefix }, pluginOptions) => {
let queryRecords
// Passing the config option addUncaughtPages will add all pages which are not covered by passed mappings
// to the default `pages` sitemap. Otherwise they will be ignored.
const options = pluginOptions.addUncaughtPages ? _.merge(defaultOptions, pluginOptions) : Object.assign(defaultOptions, pluginOptions)
const indexSitemapFile = path.join(PUBLICPATH, pathPrefix, INDEXFILE)
const resourcesSitemapFile = path.join(PUBLICPATH, pathPrefix, RESOURCESFILE)
delete options.plugins
delete options.createLinkInHead
options.indexOutput = INDEXFILE
options.resourcesOutput = RESOURCESFILE
// We always query siteAllPage as well as the site query to
// get data we need and to also allow not passing any custom
// query or mapping
const defaultQueryRecords = await runQuery(
graphql,
{ query: DEFAULTQUERY, exclude: options.exclude }
)
// Don't run this query when no query and mapping is passed
if (!options.query || !options.mapping) {
options.mapping = options.mapping || DEFAULTMAPPING
} else {
queryRecords = await runQuery(graphql, options)
}
// Instanciate the Ghost Sitemaps Manager
const manager = new Manager(options)
await serialize(queryRecords, defaultQueryRecords, options.mapping).forEach((source) => {
for (let type in source) {
source[type].forEach((node) => {
// "feed" the sitemaps manager with our serialized records
manager.addUrls(type, node)
})
}
})
// The siteUrl is only available after we have the returned query results
options.siteUrl = siteUrl
options.pathPrefix = pathPrefix
await copyStylesheet(options)
const resourcesSiteMapsArray = []
// Because it's possible to map duplicate names and/or sources to different
// sources, we need to serialize it in a way that we know which source names
// we need and which types they are assigned to, independently from where they
// come from
options.sources = serializeSources(options.mapping)
options.sources.forEach((type) => {
// for each passed name we want to receive the related source type
resourcesSiteMapsArray.push({
type: type.name,
xml: manager.getSiteMapXml(type.sitemap, options),
})
})
const indexSiteMap = manager.getIndexXml(options)
// Save the generated xml files in the public folder
try {
await fs.writeFile(indexSitemapFile, indexSiteMap)
} catch (err) {
console.error(err)
}
for (let sitemap of resourcesSiteMapsArray) {
const filePath = resourcesSitemapFile.replace(/:resource/, sitemap.type)
// Save the generated xml files in the public folder
try {
await fs.writeFile(filePath, sitemap.xml)
} catch (err) {
console.error(err)
}
}
return
}