@tomosull/gridsome-source-wordpress 1.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +121 -0
  3. package/index.js +450 -0
  4. package/package.json +33 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Change Log
2
+
3
+ # 0.1.4.13 Toms Fork
4
+
5
+ Create copy for use in own repo
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # gridsome-source-wordpress
2
+
3
+ > WordPress source for Gridsome with support for downloading images.
4
+
5
+ ## Install
6
+ - `yarn add gridsome-source-wordpress`
7
+ - `npm install gridsome-source-wordpress`
8
+
9
+ ## Usage
10
+
11
+ Add `wp-images` to your `.gitignore`
12
+
13
+ ```js
14
+ module.exports = {
15
+ plugins: [
16
+ {
17
+ use: 'gridsome-source-wordpress',
18
+ options: {
19
+ baseUrl: 'WEBSITE_URL', // required
20
+ apiBase: 'wp-json',
21
+ typeName: 'WordPress',
22
+ perPage: 100,
23
+ concurrent: 10,
24
+ routes: {
25
+ post: '/:year/:month/:day/:slug',
26
+ post_tag: '/tag/:slug'
27
+ },
28
+ splitPostsIntoFragments: true, // default false
29
+ downloadRemoteImagesFromPosts: true, // default false
30
+ downloadRemoteFeaturedImages: true, // default false
31
+ downloadACFImages: true, // default false
32
+ }
33
+ }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ ## Use with Advanced Custom Fields
39
+
40
+ Install the [ACF to REST API](https://github.com/airesvsg/acf-to-rest-api) plugin to make ACF fields available in the GraphQL schema.
41
+
42
+
43
+ `downloadACFImages: true` when downloading images, the result will like this object:
44
+
45
+ ```
46
+ image: {
47
+ src: ...//GRIDSOME IMAGE OBJECT,
48
+ alt: 'Any description text',
49
+ title: 'Title text of the image'
50
+ }
51
+ ```
52
+
53
+ Usage: `<g-image :src="image.src" :alt="image.alt" />`
54
+
55
+ ## Splitting Posts Into Fragments
56
+
57
+ `splitPostsIntoFragments: true` This will expose the following on posts that you can request via GraphQL:
58
+
59
+ **Query**
60
+ ```
61
+ postFragments {
62
+ type
63
+ fragmentData {
64
+ image
65
+ alt
66
+ html
67
+ }
68
+ }
69
+ ```
70
+
71
+ Each fragment is either of type `img` or `html` so you can render like so:
72
+
73
+ **Render**
74
+ ```
75
+ <template v-if="$page.wordPressPost.postFragments">
76
+ <template v-for="(fragment, i) in $page.wordPressPost.postFragments">
77
+ <!-- Fragment is a html block -->
78
+ <template v-if="fragment.type == 'html'">
79
+ <div :key="html-${i}" v-html="fragment.fragmentData.html" class="entry-content"></div>
80
+ </template>
81
+
82
+ <!-- Fragment is a image -->
83
+ <template v-if="fragment.type == 'img' && fragment.fragmentData.image">
84
+ <g-image :key="img-${i}" :src="fragment.fragmentData.image" :alt="fragment.fragmentData.alt"/>
85
+ </template>
86
+ </template>
87
+ </template>
88
+ ```
89
+
90
+ ### Caveats
91
+
92
+ **Folders not existing?**
93
+
94
+ Manually create the following directories in your project:
95
+
96
+ ```
97
+ /
98
+ .temp/
99
+ downloads/
100
+ wp-images/
101
+ ```
102
+
103
+
104
+ ### Tips
105
+
106
+ **Exclude unnecessary data from ACF fields**
107
+
108
+ Gridsome needs the `Return format` set to `Post Object` for `Post Object` relations in order to resolve references automatically. But Gridsome only need the `post_type` and `id` to set up a working GraphQL reference. Use the filter below to exclude all other fields.
109
+
110
+ ```php
111
+ add_filter( 'acf/format_value', function ( $value ) {
112
+ if ( $value instanceof WP_Post ) {
113
+ return [
114
+ 'post_type' => $value->post_type,
115
+ 'id' => $value->ID,
116
+ ];
117
+ }
118
+
119
+ return $value;
120
+ }, 100 );
121
+ ```
package/index.js ADDED
@@ -0,0 +1,450 @@
1
+ const pMap = require('p-map')
2
+ const axios = require('axios')
3
+ const fs = require('fs')
4
+ const https = require('https')
5
+ const path = require('path')
6
+ const camelCase = require('camelcase')
7
+ const { mapKeys, isPlainObject, trimEnd, map, find } = require('lodash')
8
+
9
+ const TYPE_AUTHOR = 'author'
10
+ const TYPE_ATTACHEMENT = 'attachment'
11
+ const TMPDIR = '.temp/downloads'
12
+ const DOWNLOAD_DIR = 'wp-images'
13
+
14
+ function mkdirSyncRecursive (absDirectory) {
15
+ const paths = absDirectory.replace(/\/$/, '').split('/')
16
+ paths.splice(0, 1)
17
+
18
+ let dirPath = '/'
19
+ paths.forEach(segment => {
20
+ dirPath += segment + '/'
21
+ if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath)
22
+ })
23
+ }
24
+
25
+ class WordPressSource {
26
+ static defaultOptions () {
27
+ return {
28
+ baseUrl: '',
29
+ apiBase: 'wp-json',
30
+ perPage: 100,
31
+ concurrent: 10,
32
+ routes: {
33
+ post: '/:slug',
34
+ post_tag: '/tag/:slug',
35
+ category: '/category/:slug',
36
+ author: '/author/:slug'
37
+ },
38
+ typeName: 'WordPress',
39
+ splitPostsIntoFragments: false,
40
+ downloadRemoteImagesFromPosts: false,
41
+ downloadRemoteFeaturedImages: false,
42
+ downloadACFImages: false
43
+ }
44
+ }
45
+
46
+ constructor (api, options) {
47
+ const opts = this.options = { ...WordPressSource.defaultOptions, ...options }
48
+ this.restBases = { posts: {}, taxonomies: {}}
49
+
50
+ if (!opts.typeName) {
51
+ throw new Error(`Missing typeName option.`)
52
+ }
53
+
54
+ if (opts.perPage > 100 || opts.perPage < 1) {
55
+ throw new Error(`${opts.typeName}: perPage cannot be more than 100 or less than 1.`)
56
+ }
57
+
58
+ const baseUrl = trimEnd(opts.baseUrl, '/')
59
+
60
+ this.client = axios.create({
61
+ baseURL: `${baseUrl}/${opts.apiBase}`
62
+ })
63
+
64
+ this.routes = this.options.routes || {}
65
+
66
+ /* Create image directories */
67
+ mkdirSyncRecursive(path.resolve(DOWNLOAD_DIR))
68
+ mkdirSyncRecursive(path.resolve(TMPDIR))
69
+ this.tmpCount = 0
70
+
71
+ this.slugify = str => api.store.slugify(str).replace(/-([^-]*)$/, '.$1')
72
+
73
+ api.loadSource(async actions => {
74
+ this.store = actions
75
+
76
+ console.log(`Loading data from ${baseUrl}`)
77
+
78
+ await this.getPostTypes(actions)
79
+ await this.getUsers(actions)
80
+ await this.getTaxonomies(actions)
81
+ await this.getPosts(actions)
82
+ })
83
+ }
84
+
85
+ async getPostTypes (actions) {
86
+ const { data } = await this.fetch('wp/v2/types', {}, {})
87
+ const addCollection = actions.addCollection || actions.addContentType
88
+
89
+ for (const type in data) {
90
+ const options = data[type]
91
+
92
+ this.restBases.posts[type] = options.rest_base
93
+
94
+ addCollection({
95
+ typeName: this.createTypeName(type),
96
+ route: this.routes[type] || `/${type}/:slug`
97
+ })
98
+ }
99
+ }
100
+
101
+ async getUsers (actions) {
102
+ const { data } = await this.fetch('wp/v2/users')
103
+ const addCollection = actions.addCollection || actions.addContentType
104
+
105
+ const authors = addCollection({
106
+ typeName: this.createTypeName(TYPE_AUTHOR),
107
+ route: this.routes.author
108
+ })
109
+
110
+ for (const author of data) {
111
+ const fields = this.normalizeFields(author)
112
+ const avatars = mapKeys(author.avatar_urls, (v, key) => `avatar${key}`)
113
+
114
+ authors.addNode({
115
+ ...fields,
116
+ id: author.id,
117
+ title: author.name,
118
+ avatars
119
+ })
120
+ }
121
+ }
122
+
123
+ async getTaxonomies (actions) {
124
+ const { data } = await this.fetch('wp/v2/taxonomies', {}, {})
125
+ const addCollection = actions.addCollection || actions.addContentType
126
+
127
+ for (const type in data) {
128
+ const options = data[type]
129
+ const taxonomy = addCollection({
130
+ typeName: this.createTypeName(type),
131
+ route: this.routes[type]
132
+ })
133
+
134
+ this.restBases.taxonomies[type] = options.rest_base
135
+
136
+ const terms = await this.fetchPaged(`wp/v2/${options.rest_base}`)
137
+
138
+ for (const term of terms) {
139
+ taxonomy.addNode({
140
+ id: term.id,
141
+ title: term.name,
142
+ slug: term.slug,
143
+ content: term.description,
144
+ count: term.count
145
+ })
146
+ }
147
+ }
148
+ }
149
+
150
+ extractImagesFromPostHtml (string) {
151
+ const regex = /<img[^>]* src=\"([^\"]*)\" alt=\"([^\"]*)\"[^>]*>/gm
152
+
153
+ const matches = []
154
+ let m
155
+ while ((m = regex.exec(string)) !== null) {
156
+ // This is necessary to avoid infinite loops with zero-width matches
157
+ if (m.index === regex.lastIndex) {
158
+ regex.lastIndex++
159
+ }
160
+
161
+ // The result can be accessed through the `m`-variable.
162
+ m.forEach((match, groupIndex) => {
163
+ matches.push({
164
+ url: match[1],
165
+ alt: match[2]
166
+ })
167
+ })
168
+ }
169
+
170
+ return matches
171
+ }
172
+
173
+ async downloadImage (url, destPath, fileName) {
174
+ const imagePath = path.resolve(destPath, fileName)
175
+
176
+ try {
177
+ if (fs.existsSync(imagePath)) return
178
+ } catch (err) {
179
+ console.log(err)
180
+ }
181
+
182
+ const tmpPath = path.resolve(TMPDIR, `${++this.tmpCount}.tmp`)
183
+
184
+ return new Promise(function (resolve, reject) {
185
+ const file = fs.createWriteStream(tmpPath)
186
+ https.get(url, (response) => {
187
+ response.pipe(file)
188
+ file.on('finish', () => {
189
+ file.close()
190
+ fs.rename(tmpPath, imagePath, resolve)
191
+ })
192
+ }).on('error', (err) => {
193
+ console.error(err.message)
194
+ fs.unlinkSync(tmpPath) // Cleanup blank file
195
+ reject(err)
196
+ })
197
+ })
198
+ }
199
+
200
+ processPostFragments (post) {
201
+ const postImages = this.extractImagesFromPostHtml(post)
202
+
203
+ const regex = /<img[^>]* src=\"([^\"]*)\"[^>]*>/
204
+ const fragments = post.split(regex)
205
+
206
+ return map(fragments, (fragment, index) => {
207
+ const image = find(postImages, (image) => { return image.url === fragment })
208
+ if (image && this.options.downloadRemoteImagesFromPosts) {
209
+ const fileName = this.slugify(fragment.split('/').pop())
210
+ const imageData = {
211
+ type: 'img',
212
+ order: index + 1,
213
+ fragmentData: {
214
+ remoteUrl: fragment,
215
+ fileName: fileName,
216
+ image: path.resolve(DOWNLOAD_DIR, fileName),
217
+ alt: image.alt
218
+ }
219
+ }
220
+ this.downloadImage(
221
+ fragment,
222
+ DOWNLOAD_DIR,
223
+ fileName
224
+ )
225
+ return imageData
226
+ } else {
227
+ return {
228
+ type: 'html',
229
+ order: index + 1,
230
+ fragmentData: {
231
+ html: fragment
232
+ }
233
+ }
234
+ }
235
+ })
236
+ }
237
+
238
+ async getPosts (actions) {
239
+ const { createReference } = actions
240
+ const getCollection = actions.getCollection || actions.getContentType
241
+
242
+ const AUTHOR_TYPE_NAME = this.createTypeName(TYPE_AUTHOR)
243
+ const ATTACHEMENT_TYPE_NAME = this.createTypeName(TYPE_ATTACHEMENT)
244
+
245
+ for (const type in this.restBases.posts) {
246
+ const restBase = this.restBases.posts[type]
247
+ const typeName = this.createTypeName(type)
248
+ const posts = getCollection(typeName)
249
+
250
+ const data = await this.fetchPaged(`wp/v2/${restBase}?_embed`)
251
+
252
+ for (const post of data) {
253
+ const fields = this.normalizeFields(post)
254
+ fields.author = createReference(AUTHOR_TYPE_NAME, post.author || '0')
255
+
256
+ if (post.type !== TYPE_ATTACHEMENT) {
257
+ fields.featuredMedia = createReference(ATTACHEMENT_TYPE_NAME, post.featured_media)
258
+ }
259
+
260
+ // add references if post has any taxonomy rest bases as properties
261
+ for (const type in this.restBases.taxonomies) {
262
+ const propName = this.restBases.taxonomies[type]
263
+
264
+ if (post.hasOwnProperty(propName)) {
265
+ const typeName = this.createTypeName(type)
266
+ const ref = createReference(typeName, post[propName])
267
+ const key = camelCase(propName)
268
+
269
+ fields[key] = ref
270
+ }
271
+ }
272
+
273
+ if (this.options.splitPostsIntoFragments && fields['content']) { fields.postFragments = this.processPostFragments(fields['content']) }
274
+
275
+ // download the featured image
276
+ if (this.options.downloadRemoteFeaturedImages && post._embedded && post._embedded['wp:featuredmedia']) {
277
+ try {
278
+ const featuredImageFileName = this.slugify(post._embedded['wp:featuredmedia']['0'].source_url.split('/').pop())
279
+ await this.downloadImage(
280
+ post._embedded['wp:featuredmedia']['0'].source_url,
281
+ DOWNLOAD_DIR,
282
+ featuredImageFileName
283
+ )
284
+ fields.featuredMediaImage = path.resolve(DOWNLOAD_DIR, featuredImageFileName)
285
+ } catch (err) {
286
+ console.log(err)
287
+ console.log('WARNING - No featured image for post ' + post.slug)
288
+ }
289
+ }
290
+
291
+ posts.addNode({
292
+ ...fields,
293
+ id: post.id
294
+ })
295
+ }
296
+ }
297
+ }
298
+
299
+ async fetch (url, params = {}, fallbackData = []) {
300
+ let res
301
+
302
+ try {
303
+ res = await this.client.request({ url, params })
304
+ } catch ({ response, code, config }) {
305
+ if (!response && code) {
306
+ throw new Error(`${code} - ${config.url}`)
307
+ }
308
+
309
+ const { url } = response.config
310
+ const { status } = response.data.data
311
+
312
+ if ([401, 403, 404].includes(status)) {
313
+ console.warn(`Error: Status ${status} - ${url}`)
314
+ return { ...response, data: fallbackData }
315
+ } else {
316
+ throw new Error(`${status} - ${url}`)
317
+ }
318
+ }
319
+
320
+ return res
321
+ }
322
+
323
+ async fetchPaged (path) {
324
+ const { perPage, concurrent } = this.options
325
+
326
+ return new Promise(async (resolve, reject) => {
327
+ let res
328
+
329
+ try {
330
+ res = await this.fetch(path, { per_page: perPage })
331
+ } catch (err) {
332
+ return reject(err)
333
+ }
334
+
335
+ const totalItems = parseInt(res.headers['x-wp-total'], 10)
336
+ const totalPages = parseInt(res.headers['x-wp-totalpages'], 10)
337
+
338
+ try {
339
+ res.data = ensureArrayData(path, res.data)
340
+ } catch (err) {
341
+ return reject(err)
342
+ }
343
+
344
+ if (!totalItems || totalPages <= 1) {
345
+ return resolve(res.data)
346
+ }
347
+
348
+ const queue = []
349
+
350
+ for (let page = 2; page <= totalPages; page++) {
351
+ queue.push({ per_page: perPage, page })
352
+ }
353
+
354
+ await pMap(queue, async params => {
355
+ try {
356
+ const { data } = await this.fetch(path, params)
357
+ res.data.push(...ensureArrayData(path, data))
358
+ } catch (err) {
359
+ console.log(err.message)
360
+ }
361
+ }, { concurrency: concurrent })
362
+
363
+ resolve(res.data)
364
+ })
365
+ }
366
+
367
+ normalizeFields (fields, isACF) {
368
+ const res = {}
369
+
370
+ for (const key in fields) {
371
+ if (key.startsWith('_')) continue // skip links and embeds etc
372
+ res[camelCase(key)] = this.normalizeFieldValue(fields[key], isACF || key === 'acf')
373
+ }
374
+
375
+ return res
376
+ }
377
+
378
+ normalizeFieldValue (value, isACF) {
379
+ if (value === null) return null
380
+ if (value === undefined) return null
381
+
382
+ if (Array.isArray(value)) {
383
+ return value.map(v => this.normalizeFieldValue(v, isACF))
384
+ }
385
+
386
+ if (isPlainObject(value)) {
387
+ if (value.type === 'image' && value.filename && value.url && isACF && this.options.downloadACFImages) {
388
+ const filename = this.slugify(value.filename)
389
+ this.downloadImage(
390
+ value.url,
391
+ DOWNLOAD_DIR,
392
+ filename
393
+ )
394
+ return {
395
+ src: path.resolve(DOWNLOAD_DIR, filename),
396
+ title: value.title,
397
+ alt: value.alt
398
+ }
399
+ } else if (value.post_type && (value.ID || value.id)) {
400
+ const typeName = this.createTypeName(value.post_type)
401
+ const id = value.ID || value.id
402
+
403
+ return this.store.createReference(typeName, id)
404
+ } else if (value.filename && (value.ID || value.id)) {
405
+ const typeName = this.createTypeName(TYPE_ATTACHEMENT)
406
+ const id = value.ID || value.id
407
+
408
+ return this.store.createReference(typeName, id)
409
+ } else if (value.hasOwnProperty('rendered')) {
410
+ return value.rendered
411
+ }
412
+
413
+ return this.normalizeFields(value, isACF)
414
+ }
415
+
416
+ if (isACF && this.options.downloadACFImages && String(value).match(/^https:\/\/.*\/.*\.(jpg|png|svg|jpeg)($|\?)/i)) {
417
+ const filename = this.slugify(value.split('/').pop())
418
+ console.log(`Downloading ${filename}`)
419
+ this.downloadImage(
420
+ value,
421
+ DOWNLOAD_DIR,
422
+ filename
423
+ )
424
+ return path.resolve(DOWNLOAD_DIR, filename)
425
+ }
426
+
427
+ return value
428
+ }
429
+
430
+ createTypeName (name = '') {
431
+ return camelCase(`${this.options.typeName} ${name}`, { pascalCase: true })
432
+ }
433
+ }
434
+
435
+ function ensureArrayData (url, data) {
436
+ if (!Array.isArray(data)) {
437
+ try {
438
+ data = JSON.parse(data)
439
+ } catch (err) {
440
+ throw new Error(
441
+ `Failed to fetch ${url}\n` +
442
+ `Expected JSON response but received:\n` +
443
+ `${data.trim().substring(0, 150)}...\n`
444
+ )
445
+ }
446
+ }
447
+ return data
448
+ }
449
+
450
+ module.exports = WordPressSource
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": "1.4.13",
3
+ "name": "@tomosull/gridsome-source-wordpress",
4
+ "description": "Tomosull Fork of gridsome-source-wordpress 4.13 - WordPress source for Gridsome with image processing support. This fixes minor issues with the original plugin; namely 403 and image alt tags not loading.",
5
+ "homepage": "https://github.com/gridsome/gridsome/tree/master/packages/source-wordpress#readme",
6
+ "repository": {
7
+ "type": "git"
8
+ },
9
+ "main": "index.js",
10
+ "keywords": [
11
+ "tomosull",
12
+ "gridsome",
13
+ "wordpress"
14
+ ],
15
+ "dependencies": {
16
+ "axios": "^0.19.0",
17
+ "camelcase": "^5.0.0",
18
+ "lodash": "^4.17.11",
19
+ "p-map": "^1.2.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": ">=8.3"
26
+ },
27
+ "devDependencies": {},
28
+ "scripts": {
29
+ "test": "echo \"Error: no test specified\" && exit 1"
30
+ },
31
+ "author": "Tom O'Sullivan",
32
+ "license": "ISC"
33
+ }