diff --git a/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap b/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap index 3a3037af00f00..7e5d8c5881db4 100644 --- a/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap +++ b/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap @@ -3915,6 +3915,13 @@ Array [ "parent", "children", "internal", + "filename", + "filesize", + "width", + "height", + "publicUrl", + "resize", + "gatsbyImageData", ], "name": "WpMediaItem", }, diff --git a/integration-tests/gatsby-source-wordpress/__tests__/index.js b/integration-tests/gatsby-source-wordpress/__tests__/index.js index 421c2dfef5ae4..6f20b16ad07dc 100644 --- a/integration-tests/gatsby-source-wordpress/__tests__/index.js +++ b/integration-tests/gatsby-source-wordpress/__tests__/index.js @@ -60,6 +60,10 @@ describe(`[gatsby-source-wordpress] Run tests on develop build`, () => { let gatsbyDevelopProcess beforeAll(async () => { + if (process.env.SKIP_BEFORE_ALL) { + return + } + if (!isWarmCache) { await gatsbyCleanBeforeAll() } @@ -105,7 +109,10 @@ describe(`[gatsby-source-wordpress] Run tests on develop build`, () => { require(`../test-fns/index`) afterAll(done => { - gatsbyDevelopProcess.kill() + if (gatsbyDevelopProcess) { + gatsbyDevelopProcess.kill() + } + done() }) }) diff --git a/integration-tests/gatsby-source-wordpress/docker-compose.yml b/integration-tests/gatsby-source-wordpress/docker-compose.yml index d3d168f113cb2..7ced65baebc45 100644 --- a/integration-tests/gatsby-source-wordpress/docker-compose.yml +++ b/integration-tests/gatsby-source-wordpress/docker-compose.yml @@ -2,6 +2,7 @@ version: "3.2" services: db: + platform: linux/x86_64 image: mysql:8 environment: MYSQL_ROOT_PASSWORD: gtsb-wp-dckr diff --git a/integration-tests/gatsby-source-wordpress/test-fns/data-resolution.js b/integration-tests/gatsby-source-wordpress/test-fns/data-resolution.js index ee7736045c3d4..5d024d09b52f6 100644 --- a/integration-tests/gatsby-source-wordpress/test-fns/data-resolution.js +++ b/integration-tests/gatsby-source-wordpress/test-fns/data-resolution.js @@ -531,4 +531,44 @@ describe(`data resolution`, () => { expect(wpMediaItem).toBeNull() } }) + + it(`Resolves Gatsby Image CDN data`, async () => { + const { + data: { allWpPost }, + } = await fetchGraphql({ + url, + query: /* GraphQL */ ` + query { + allWpPost { + nodes { + featuredImage { + node { + mediaItemUrl + resize(width: 100, height: 100, quality: 100) { + width + height + src + } + } + } + } + } + } + `, + }) + + allWpPost.nodes.forEach(node => { + if (!node.featuredImage?.node) { + return + } + + const { resize } = node.featuredImage.node + const [, , , sourceUrl64, _args64, filename] = resize.src.split(`/`) + + const sourceUrl = Buffer.from(sourceUrl64, `base64`).toString(`ascii`) + + expect(node.featuredImage.node.mediaItemUrl).toEqual(sourceUrl) + expect(node.featuredImage.node.mediaItemUrl).toContain(filename) + }) + }) }) diff --git a/packages/gatsby-source-wordpress/__tests__/process-node.fixture.js b/packages/gatsby-source-wordpress/__tests__/process-node.fixture.js new file mode 100644 index 0000000000000..96f0ed3be6f00 --- /dev/null +++ b/packages/gatsby-source-wordpress/__tests__/process-node.fixture.js @@ -0,0 +1,77 @@ +export const referencedMediaItems = [ + { + "id": `cG9zdDoxMTU=`, + "mediaDetails": { + "file": `2022/02/sasha-set-GURzQwO8Li0-unsplash-scaled.jpg`, + "height": 1920, + "sizes": [ + { + "file": `sasha-set-GURzQwO8Li0-unsplash-300x225.jpg`, + "fileSize": 18876, + "height": `225`, + "mimeType": `image/jpeg`, + "name": `medium`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-300x225.jpg`, + "width": `300` + }, + { + "file": `sasha-set-GURzQwO8Li0-unsplash-1024x768.jpg`, + "fileSize": 214590, + "height": `768`, + "mimeType": `image/jpeg`, + "name": `large`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-1024x768.jpg`, + "width": `1024` + }, + { + "file": `sasha-set-GURzQwO8Li0-unsplash-150x150.jpg`, + "fileSize": 7088, + "height": `150`, + "mimeType": `image/jpeg`, + "name": `thumbnail`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-150x150.jpg`, + "width": `150` + }, + { + "file": `sasha-set-GURzQwO8Li0-unsplash-768x576.jpg`, + "fileSize": 118086, + "height": `576`, + "mimeType": `image/jpeg`, + "name": `medium_large`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-768x576.jpg`, + "width": `768` + }, + { + "file": `sasha-set-GURzQwO8Li0-unsplash-1536x1152.jpg`, + "fileSize": 490613, + "height": `1152`, + "mimeType": `image/jpeg`, + "name": `1536x1536`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-1536x1152.jpg`, + "width": `1536` + }, + { + "file": `sasha-set-GURzQwO8Li0-unsplash-2048x1536.jpg`, + "fileSize": 858627, + "height": `1536`, + "mimeType": `image/jpeg`, + "name": `2048x2048`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-2048x1536.jpg`, + "width": `2048` + } + ], + "width": 2560 + }, + "mediaItemUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-scaled.jpg`, + "mediaType": `image`, + "mimeType": `image/jpeg`, + "sourceUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-scaled.jpg`, + "srcSet": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-300x225.jpg 300w, http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-1024x768.jpg 1024w, http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-768x576.jpg 768w, http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-1536x1152.jpg 1536w, http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-2048x1536.jpg 2048w`, + "width": 2560, + "height": 1920, + "placeholderUrl": `http://wpgatsby.local/wp-content/uploads/2022/02/sasha-set-GURzQwO8Li0-unsplash-300x225.jpg`, + "internal": { + "contentDigest": `5fa24d43c7fc603f9016933ad6bb3ee4`, + } + } +] diff --git a/packages/gatsby-source-wordpress/__tests__/process-node.test.js b/packages/gatsby-source-wordpress/__tests__/process-node.test.js index b61376194def5..bf8f38e0026be 100644 --- a/packages/gatsby-source-wordpress/__tests__/process-node.test.js +++ b/packages/gatsby-source-wordpress/__tests__/process-node.test.js @@ -1,8 +1,10 @@ import execall from "execall" +import { replaceNodeHtmlImages } from "../dist/steps/source-nodes/create-nodes/process-node" + import { getImgSrcRemoteFileMatchesFromNodeString, - getImgTagMatchesWithUrl, + getImgTagMatches, getWpLinkRegex, searchAndReplaceNodeStrings, } from "../dist/steps/source-nodes/create-nodes/process-node" @@ -20,7 +22,7 @@ test(`HTML image transformation regex matches images`, async () => { expect(matches.length).toBe(3) - const imgTagMatches = getImgTagMatchesWithUrl({ + const imgTagMatches = getImgTagMatches({ nodeString, wpUrl: `https://${wpUrl}`, }) @@ -69,3 +71,68 @@ test(`Search and replace node strings using regex matches`, async () => { We need to test some link as well!`) }) + +jest.mock(`../dist/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js`, () => { + return { + __esModule: true, + ...jest.requireActual(`../dist/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js`), + default: jest.fn(() => require(`./process-node.fixture`).referencedMediaItems) + } +}) + + +test(`Gatsby Image service works in html fields via replaceNodeHtmlImages`, async () => { + const node = { + content: `\n

Welcome to WordPress. This is your first post. Edit or deleteit, then start writing!

\n\n\n\n

\n\n\n\n\n`, + id: `cG9zdDox`, + modifiedGmt: `2022-02-18T23:18:00`, + __typename: `Post` + } + + const gatsbyImageUrl = `/_gatsby/image` + + const nodeString = JSON.stringify(node) + + const updatedNodeString = await replaceNodeHtmlImages({ + nodeString, + node, + helpers: { + reporter: console + }, + wpUrl: `http://wpgatsby.local/`, + pluginOptions: { + html: { + useGatsbyImage: true + } + } + }) + + expect(updatedNodeString).toInclude(gatsbyImageUrl) + expect(updatedNodeString).not.toEqual(nodeString) + + const imageMatches = execall(/\/_gatsby\/image/gm, updatedNodeString) + expect(imageMatches.length).toBe(39) + + + const transformedNodeStringNoHtmlImages = await replaceNodeHtmlImages({ + nodeString, + node, + helpers: { + reporter: console + }, + wpUrl: `http://wpgatsby.local/`, + pluginOptions: { + html: { + useGatsbyImage: false + } + } + }) + + expect(transformedNodeStringNoHtmlImages).toEqual(nodeString) + + const noImageMatches = execall(/\/_gatsby\/image/gm, transformedNodeStringNoHtmlImages) + + expect(noImageMatches.length).toBe(0) + + expect(transformedNodeStringNoHtmlImages).not.toInclude(gatsbyImageUrl) +}) diff --git a/packages/gatsby-source-wordpress/docs/plugin-options.md b/packages/gatsby-source-wordpress/docs/plugin-options.md index 64c7a08c17958..538a5371d04f4 100644 --- a/packages/gatsby-source-wordpress/docs/plugin-options.md +++ b/packages/gatsby-source-wordpress/docs/plugin-options.md @@ -50,6 +50,7 @@ - [html.imageQuality](#htmlimagequality) - [html.createStaticFiles](#htmlcreatestaticfiles) - [html.generateWebpImages](#htmlgeneratewebpimages) + - [html.placeholderType](#htmlplaceholdertype) - [type](#type) - [type.\_\_all](#type__all) - [type.\_\_all.where](#type__allwhere) @@ -60,6 +61,7 @@ - [type.\_\_all.beforeChangeNode](#type__allbeforechangenode) - [type.RootQuery](#typerootquery) - [type.MediaItem](#typemediaitem) + - [type.MediaItem.placeholderSizeName](#typemediaitemplaceholdersizename) - [type.MediaItem.createFileNodes](#typemediaitemcreatefilenodes) - [type.MediaItem.lazyNodes](#typemediaitemlazynodes) - [type.MediaItem.localFile](#typemediaitemlocalfile) @@ -951,7 +953,7 @@ If a max width can't be inferred from html this value will be passed to Sharp. I **Field type**: `Number` -**Default value**: `100` +**Default value**: `1024` ```js { @@ -1025,6 +1027,14 @@ When this is true, .webp images will be generated for images in html fields in a ``` +### html.placeholderType + +This can be either "blurred" or "dominantColor". This is the type of placeholder image to be used in Gatsby Images in HTML fields. + +**Field type**: `String` + +**Default value**: `blurred` + ## type Options related to specific types in the remote schema. @@ -1179,6 +1189,14 @@ A special type which is applied to any non-node root fields that are ingested an **Field type**: `Object` +#### type.MediaItem.placeholderSizeName + +This option allows you to choose the placeholder size used in the new Gatsby image service (currently in ALPHA/BETA) for the small placeholder image. Please make this image size very small for better performance. 20px or smaller width is recommended. To use, create a new image size in WP and name it "gatsby-image-placeholder" (or the name that you pass to this option) and that new size will be used automatically for placeholder images in the Gatsby build. + +**Field type**: `String` + +**Default value**: `gatsby-image-placeholder` + #### type.MediaItem.createFileNodes This option controls whether or not a File node will be automatically created for each MediaItem node (available on MediaItem.localFile). Set this to false if you don't want Gatsby to download the corresponding file for each media item. diff --git a/packages/gatsby-source-wordpress/package.json b/packages/gatsby-source-wordpress/package.json index c8ae36bc56a4a..dcfa3f04eb34b 100644 --- a/packages/gatsby-source-wordpress/package.json +++ b/packages/gatsby-source-wordpress/package.json @@ -31,6 +31,7 @@ "gatsby-core-utils": "^3.10.0-next.0", "gatsby-plugin-catch-links": "^4.10.0-next.0", "gatsby-source-filesystem": "^4.10.0-next.0", + "gatsby-plugin-utils": "^3.4.0-next.0", "glob": "^7.2.0", "got": "^11.8.3", "lodash": "^4.17.21", diff --git a/packages/gatsby-source-wordpress/src/gatsby-node.ts b/packages/gatsby-source-wordpress/src/gatsby-node.ts index f795fa8ad2837..ab3a96673402c 100644 --- a/packages/gatsby-source-wordpress/src/gatsby-node.ts +++ b/packages/gatsby-source-wordpress/src/gatsby-node.ts @@ -37,6 +37,7 @@ module.exports = runApisInSteps({ ], onCreateDevServer: [ + steps.imageRoutes, steps.setImageNodeIdCache, steps.logPostBuildWarnings, steps.startPollingForContentUpdates, diff --git a/packages/gatsby-source-wordpress/src/models/gatsby-api.ts b/packages/gatsby-source-wordpress/src/models/gatsby-api.ts index 1222e1c081fe9..57ec6bf4b1535 100644 --- a/packages/gatsby-source-wordpress/src/models/gatsby-api.ts +++ b/packages/gatsby-source-wordpress/src/models/gatsby-api.ts @@ -104,6 +104,7 @@ export interface IPluginOptions { fallbackImageMaxWidth?: number imageQuality?: number createStaticFiles?: boolean + placeholderType?: `blurred` | `dominantColor` } presets?: Array type?: { @@ -123,6 +124,8 @@ export interface IPluginOptions { maxFileSizeBytes?: number requestConcurrency?: number } + + placeholderSizeName?: string } } } @@ -191,6 +194,8 @@ const defaultPluginOptions: IPluginOptions = { // // this adds image options to images in HTML fields when html.useGatsbyImage is also set gatsbyImageOptions: {}, + + placeholderType: `blurred`, }, presets: [previewOptimizationPreset], type: { @@ -226,6 +231,7 @@ const defaultPluginOptions: IPluginOptions = { exclude: true, }, MediaItem: { + placeholderSizeName: `gatsby-image-placeholder`, lazyNodes: false, createFileNodes: true, localFile: { diff --git a/packages/gatsby-source-wordpress/src/steps/create-schema-customization/index.js b/packages/gatsby-source-wordpress/src/steps/create-schema-customization/index.js index fa6dd48b747f2..794e7065f5aa5 100644 --- a/packages/gatsby-source-wordpress/src/steps/create-schema-customization/index.js +++ b/packages/gatsby-source-wordpress/src/steps/create-schema-customization/index.js @@ -7,6 +7,7 @@ import { getGatsbyNodeTypeNames } from "../source-nodes/fetch-nodes/fetch-nodes" import { typeIsExcluded } from "~/steps/ingest-remote-schema/is-excluded" import { formatLogMessage } from "../../utils/format-log-message" import { CODES } from "../../utils/report" +import { addRemoteFilePolyfillInterface } from "gatsby-plugin-utils/polyfill-remote-file" /** * createSchemaCustomization @@ -89,6 +90,20 @@ const customizeSchema = async ({ actions, schema }) => { isAGatsbyNode: true, }) + typeDefs.push( + addRemoteFilePolyfillInterface( + schema.buildObjectType({ + name: pluginOptions.schema.typePrefix + `MediaItem`, + fields: {}, + interfaces: [`Node`, `RemoteFile`], + }), + { + schema, + actions, + } + ) + ) + typeDefs.push(wpType) actions.createTypes(typeDefs) diff --git a/packages/gatsby-source-wordpress/src/steps/create-schema-customization/type-filters.js b/packages/gatsby-source-wordpress/src/steps/create-schema-customization/type-filters.js index cedd9fe0e4b38..93a2f4988d851 100644 --- a/packages/gatsby-source-wordpress/src/steps/create-schema-customization/type-filters.js +++ b/packages/gatsby-source-wordpress/src/steps/create-schema-customization/type-filters.js @@ -77,28 +77,8 @@ export const typeDefinitionFilters = [ objectType.fields.localFile = { type: `File`, - resolve: (mediaItemNode, _, context) => { - if (!mediaItemNode) { - return null - } - - const localMediaNodeId = mediaItemNode?.localFile?.id - - if (localMediaNodeId) { - const node = context.nodeModel.getNodeById({ - id: mediaItemNode.localFile.id, - type: `File`, - }) - - if (node) { - return node - } - } - - return createLocalFileNode({ - mediaItemNode, - parentName: `Creating File node while resolving missing MediaItem.localFile`, - }) + extensions: { + link: { from: `localFile` }, }, } diff --git a/packages/gatsby-source-wordpress/src/steps/declare-plugin-options-schema.js b/packages/gatsby-source-wordpress/src/steps/declare-plugin-options-schema.js index 528dd82576a7f..1f6ccd91738b7 100644 --- a/packages/gatsby-source-wordpress/src/steps/declare-plugin-options-schema.js +++ b/packages/gatsby-source-wordpress/src/steps/declare-plugin-options-schema.js @@ -631,7 +631,7 @@ When using this option, be sure to gitignore the wordpress-cache directory in th fallbackImageMaxWidth: Joi.number() .integer() .allow(null) - .default(100) + .default(1024) .description( `If a max width can't be inferred from html this value will be passed to Sharp. If the image is smaller than this, the image file's width will be used instead.` ) @@ -682,6 +682,18 @@ When using this option, be sure to gitignore the wordpress-cache directory in th }, `), }), + placeholderType: Joi.string() + .default(`blurred`) + .description( + `This can be either "blurred" or "dominantColor". This is the type of placeholder image to be used in Gatsby Images in HTML fields.` + ) + .example( + wrapOptions(` + html: { + placeholderType: \`dominantColor\` + } + `) + ), }) .description(`Options related to html field processing.`) .meta({ @@ -725,6 +737,11 @@ When using this option, be sure to gitignore the wordpress-cache directory in th `), }), MediaItem: Joi.object({ + placeholderSizeName: Joi.string() + .default(`gatsby-image-placeholder`) + .description( + `This option allows you to choose the placeholder size used in the new Gatsby image service (currently in ALPHA/BETA) for the small placeholder image. Please make this image size very small for better performance. 20px or smaller width is recommended. To use, create a new image size in WP and name it "gatsby-image-placeholder" (or the name that you pass to this option) and that new size will be used automatically for placeholder images in the Gatsby build.` + ), createFileNodes: Joi.boolean() .default(true) .description( diff --git a/packages/gatsby-source-wordpress/src/steps/image-routes.ts b/packages/gatsby-source-wordpress/src/steps/image-routes.ts new file mode 100644 index 0000000000000..b925d27d4d237 --- /dev/null +++ b/packages/gatsby-source-wordpress/src/steps/image-routes.ts @@ -0,0 +1,6 @@ +import { polyfillImageServiceDevRoutes } from "gatsby-plugin-utils/polyfill-remote-file" +import { Step } from "./../utils/run-steps" + +export const imageRoutes: Step = async ({ app }): Promise => { + polyfillImageServiceDevRoutes(app) +} diff --git a/packages/gatsby-source-wordpress/src/steps/index.ts b/packages/gatsby-source-wordpress/src/steps/index.ts index 0fbe794ae24d9..1e7536973c9dd 100644 --- a/packages/gatsby-source-wordpress/src/steps/index.ts +++ b/packages/gatsby-source-wordpress/src/steps/index.ts @@ -19,3 +19,4 @@ export { export { pluginOptionsSchema } from "~/steps/declare-plugin-options-schema" export { logPostBuildWarnings } from "~/steps/log-post-build-warnings" +export { imageRoutes } from "~/steps/image-routes" diff --git a/packages/gatsby-source-wordpress/src/steps/source-nodes/create-nodes/process-node.js b/packages/gatsby-source-wordpress/src/steps/source-nodes/create-nodes/process-node.js index 6da5fcdd28023..737d0c2601ce4 100644 --- a/packages/gatsby-source-wordpress/src/steps/source-nodes/create-nodes/process-node.js +++ b/packages/gatsby-source-wordpress/src/steps/source-nodes/create-nodes/process-node.js @@ -1,6 +1,5 @@ /* eslint-disable no-useless-escape */ import { isWebUri } from "valid-url" -import { generateImageData } from "gatsby-plugin-sharp" import { GatsbyImage } from "gatsby-plugin-image" import React from "react" import ReactDOMServer from "react-dom/server" @@ -13,6 +12,7 @@ import fs from "fs-extra" import { supportedExtensions } from "gatsby-transformer-sharp/supported-extensions" import replaceAll from "replaceall" import { usingGatsbyV4OrGreater } from "~/utils/gatsby-version" +import { gatsbyImageResolver } from "gatsby-plugin-utils/dist/polyfill-remote-file/graphql/gatsby-image-resolver" import { formatLogMessage } from "~/utils/format-log-message" @@ -21,13 +21,31 @@ import fetchReferencedMediaItemsAndCreateNodes, { } from "../fetch-nodes/fetch-referenced-media-items" import btoa from "btoa" import store from "~/store" -import { createLocalFileNode } from "./create-local-file-node" -const getNodeEditLink = node => { - const { protocol, hostname } = url.parse(node.link) - const editUrl = `${protocol}//${hostname}/wp-admin/post.php?post=${node.databaseId}&action=edit` +/** + * Takes in a MediaItem node from WPGraphQL as well as Gatsby plugin options and returns the correct placeholder URL for GatsbyImage + * + * The user must set the placeholderSizeName plugin option, or otherwise create an image size in WP where the name is `gatsby-image-placeholder` + */ +export function getPlaceholderUrlFromMediaItemNode(node, pluginOptions) { + let placeholderSizeByWidth + let placeholderSizeByName + + node.mediaDetails?.sizes?.forEach(size => { + if ( + size.name === + (pluginOptions?.type?.MediaItem?.placeholderSizeName || + `gatsby-image-placeholder`) + ) { + placeholderSizeByName = size + } else if (Number(size.width) <= 20) { + placeholderSizeByWidth = size + } + }) + + const placeHolderSize = placeholderSizeByName || placeholderSizeByWidth - return editUrl + return placeHolderSize?.sourceUrl } const findReferencedImageNodeIds = ({ nodeString, pluginOptions, node }) => { @@ -239,52 +257,12 @@ const fetchNodeHtmlImageMediaItemNodes = async ({ wpUrl, }) - let imageNode = pickNodeBySourceUrlOrCheerioImg({ + const imageNode = pickNodeBySourceUrlOrCheerioImg({ url: htmlImgSrc, cheerioImg, mediaItemNodes, }) - if (!imageNode && htmlImgSrc) { - // if we didn't get a media item node for this image, - // we need to fetch it and create a file node for it with no - // media item node. - try { - imageNode = await createLocalFileNode({ - skipExistingNode: true, - parentName: `Creating File node from URL where we couldn't find a MediaItem node`, - mediaItemNode: { - id: node.id, - mediaItemUrl: htmlImgSrc, - modifiedGmt: null, - mimeType: null, - title: null, - fileSize: null, - parentHtmlNode: node, - }, - }) - } catch (e) { - const sharedError = `when trying to fetch\n${htmlImgSrc}\nfrom ${ - node.__typename - } #${node.databaseId} "${node.title ?? node.id}"` - const nodeEditLink = getNodeEditLink(node) - - if (typeof e === `string` && e.includes(`404`)) { - helpers.reporter.warn( - formatLogMessage( - `\n\nReceived a 404 ${sharedError}\n\nMost likely this image was uploaded to this ${node.__typename} and then deleted from the media library.\nYou'll need to fix this and re-save this ${node.__typename} to remove this warning at\n${nodeEditLink}.\n\n` - ) - ) - imageNode = null - } else { - helpers.reporter.warn( - `Received the below error ${sharedError}\n\n${nodeEditLink}\n\n` - ) - helpers.reporter.panic(formatLogMessage(e)) - } - } - } - cacheCreatedFileNodeBySrc({ node: imageNode, src: htmlImgSrc }) if (imageNode) { @@ -476,17 +454,17 @@ export const getImgSrcRemoteFileMatchesFromNodeString = nodeString => return !isInJSON }) -export const getImgTagMatchesWithUrl = ({ nodeString, wpUrl }) => +export const getImgTagMatches = ({ nodeString }) => execall( //gim, nodeString // we don't want to match images inside pre - .replace(/.*(<\/pre>)/gim, ``) + .replace(/(?:(?!<\/pre>).)+(<\/pre>)/gim, ``) // and code tags, so temporarily remove those tags and everything inside them - .replace(/.*(<\/code>)/gim, ``) - ).filter(filterMatches(wpUrl)) + .replace(/(?:(?!<\/code>).)+(<\/code>)/gim, ``) + ) -const replaceNodeHtmlImages = async ({ +export const replaceNodeHtmlImages = async ({ nodeString, node, helpers, @@ -500,7 +478,7 @@ const replaceNodeHtmlImages = async ({ const imageUrlMatches = getImgSrcRemoteFileMatchesFromNodeString(nodeString) - const imgTagMatches = getImgTagMatchesWithUrl({ nodeString, wpUrl }) + const imgTagMatches = getImgTagMatches({ nodeString }) if (imageUrlMatches.length && imgTagMatches.length) { const cheerioImages = getCheerioElementsFromMatches({ @@ -541,11 +519,12 @@ const replaceNodeHtmlImages = async ({ ? // this will already be a file node imageNode : // otherwise grab the file node - helpers.getNode(imageNode.localFile.id) + helpers.getNode(imageNode?.localFile?.id) - if (!fileNode) { - return null - } + const extension = imageNode?.mimeType?.replace( + `${imageNode?.mediaType}/`, + `` + ) const imgTagMaxWidth = findImgTagMaxWidthFromCheerioImg(cheerioImg) @@ -575,7 +554,7 @@ const replaceNodeHtmlImages = async ({ // and we have a media item node to know it's full size max width mediaItemNodeWidth && // and this isn't an svg which has no maximum width - fileNode.extension !== `svg` && + extension !== `svg` && // and the media item node max width is smaller than what we inferred // from html mediaItemNodeWidth < imgTagMaxWidth @@ -597,31 +576,50 @@ const replaceNodeHtmlImages = async ({ const quality = pluginOptions?.html?.imageQuality - const { reporter, cache, pathPrefix } = helpers + const { reporter } = helpers const gatsbyTransformerSharpSupportsThisFileType = - supportedExtensions[fileNode?.extension] + supportedExtensions[extension] || extension === `gif` - let sharpImageData = null + let imageResize = null if (gatsbyTransformerSharpSupportsThisFileType) { + const placeholderUrl = getPlaceholderUrlFromMediaItemNode( + imageNode, + pluginOptions + ) + + const imageUrl = + imageNode.mediaItemUrl || imageNode.sourceUrl || imageNode.url + try { - sharpImageData = await generateImageData({ - file: fileNode, - args: { + imageResize = await gatsbyImageResolver( + { + url: imageUrl, + placeholderUrl, + mimeType: imageNode.mimeType, + width: imageNode.mediaDetails.width, + height: imageNode.mediaDetails.height, + filename: path.basename(imageNode.mediaDetails.file), + internal: { + contentDigest: imageNode.modifiedGmt, + }, + }, + { width: maxWidth, + layout: `constrained`, + placeholder: !placeholderUrl + ? `none` + : pluginOptions?.html?.placeholderType || `blurred`, quality, - ...pluginOptions?.html?.gatsbyImageOptions, }, - pathPrefix, - reporter, - cache, - }) + helpers.actions + ) } catch (e) { reporter.error(e) reporter.warn( formatLogMessage( - `${node.__typename} ${node.id} couldn't process inline html image ${fileNode.url}` + `${node.__typename} ${node.id} couldn't process inline html image ${imageUrl}` ) ) return null @@ -632,7 +630,7 @@ const replaceNodeHtmlImages = async ({ match, cheerioImg, fileNode, - imageResize: sharpImageData, + imageResize, maxWidth, } }) @@ -645,14 +643,12 @@ const replaceNodeHtmlImages = async ({ continue } - const { match, imageResize, cheerioImg, maxWidth } = matchResize - - // @todo retain img tag classes and attributes from cheerioImg + const { match, imageResize, cheerioImg } = matchResize let ReactGatsbyImage // used to create hydration data for images let gatsbyImageHydrationData = null - if (imageResize) { + if (imageResize && imageResize.images.sources.length > 0) { gatsbyImageHydrationData = { image: imageResize, alt: cheerioImg?.attribs?.alt, @@ -666,49 +662,28 @@ const replaceNodeHtmlImages = async ({ gatsbyImageHydrationData, null ) - } else { - const { fileNode } = matchResize - const relativeUrl = await copyFileToStaticAndReturnUrlPath( - fileNode, - helpers - ) - - const imgOptions = { - style: { - // these styles make it so that the image wont be stretched - // beyond it's max width, but it also wont exceed the width - // of it's parent element - maxWidth: `100%`, - width: `${maxWidth}px`, - }, - className: `${ - cheerioImg?.attribs?.class || `` - } inline-gatsby-image-wrapper`, - loading: `eager`, - alt: cheerioImg?.attribs?.alt, - src: relativeUrl, - } - - ReactGatsbyImage = React.createElement(`img`, imgOptions, null) } - let gatsbyImageStringRaw = ReactDOMServer.renderToString(ReactGatsbyImage) + if (ReactGatsbyImage) { + let gatsbyImageStringRaw = + ReactDOMServer.renderToString(ReactGatsbyImage) - // gatsby-plugin-image needs hydration data to work on navigations - we add the hydration data to the DOM to use it in gatsby-browser.ts - if (gatsbyImageHydrationData) { - gatsbyImageStringRaw += `` - } - // need to remove the JSON stringify quotes around our image since we're - // threading this JSON string back into a larger JSON object string - const gatsbyImageStringJSON = JSON.stringify(gatsbyImageStringRaw) - const gatsbyImageString = gatsbyImageStringJSON.substring( - 1, - gatsbyImageStringJSON.length - 1 - ) + // gatsby-plugin-image needs hydration data to work on navigations - we add the hydration data to the DOM to use it in gatsby-browser.ts + if (gatsbyImageHydrationData) { + gatsbyImageStringRaw += `` + } + // need to remove the JSON stringify quotes around our image since we're + // threading this JSON string back into a larger JSON object string + const gatsbyImageStringJSON = JSON.stringify(gatsbyImageStringRaw) + const gatsbyImageString = gatsbyImageStringJSON.substring( + 1, + gatsbyImageStringJSON.length - 1 + ) - nodeString = replaceAll(match, gatsbyImageString, nodeString) + nodeString = replaceAll(match, gatsbyImageString, nodeString) + } } } diff --git a/packages/gatsby-source-wordpress/src/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js b/packages/gatsby-source-wordpress/src/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js index 385114e89a23a..526b3e13d3d22 100644 --- a/packages/gatsby-source-wordpress/src/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js +++ b/packages/gatsby-source-wordpress/src/steps/source-nodes/fetch-nodes/fetch-referenced-media-items.js @@ -13,6 +13,7 @@ import urlUtil from "url" import path from "path" import { getPluginOptions } from "~/utils/get-gatsby-api" import { formatLogMessage } from "~/utils/format-log-message" +import { getPlaceholderUrlFromMediaItemNode } from "../create-nodes/process-node" const nodeFetchConcurrency = 2 @@ -179,11 +180,28 @@ export const createMediaItemNode = async ({ ) } + const placeholderUrl = getPlaceholderUrlFromMediaItemNode( + node, + pluginOptions + ) + + const url = node.sourceUrl || node.mediaItemUrl + + const filename = + node?.mediaDetails?.file?.split(`/`)?.pop() || + path.basename(urlUtil.parse(url).pathname) + node = { ...node, - localFile: { - id: localFileNode?.id, - }, + url, + contentType: node.contentType, + mimeType: node.mimeType, + filename, + filesize: node?.mediaDetails?.fileSize, + width: node?.mediaDetails?.width, + height: node?.mediaDetails?.height, + placeholderUrl: + placeholderUrl ?? node?.mediaDetails?.sizes?.[0]?.sourceUrl ?? url, parent: null, internal: { contentDigest: createContentDigest(node), @@ -191,6 +209,10 @@ export const createMediaItemNode = async ({ }, } + if (localFileNode) { + node.localFile = localFileNode?.id + } + const normalizedNode = normalizeNode({ node, nodeTypeName: `MediaItem` }) await actions.createNode(normalizedNode) @@ -394,7 +416,7 @@ export const fetchMediaItemsBySourceUrl = async ({ ) nodes.forEach((node, index) => { - if (!node) { + if (!node || !node?.localFile?.id) { return } diff --git a/packages/gatsby-source-wordpress/src/utils/gatsby-types.ts b/packages/gatsby-source-wordpress/src/utils/gatsby-types.ts index 66d757f2d6fcf..e562360824aac 100644 --- a/packages/gatsby-source-wordpress/src/utils/gatsby-types.ts +++ b/packages/gatsby-source-wordpress/src/utils/gatsby-types.ts @@ -1,5 +1,6 @@ import { IPreviewData } from "./../steps/preview/index" import type { NodePluginArgs, Reporter } from "gatsby" +import { Application } from "express" export type GatsbyNodeApiHelpers = NodePluginArgs & { Joi?: any webhookBody?: IPreviewData @@ -10,6 +11,7 @@ export type GatsbyNodeApiHelpers = NodePluginArgs & { updatedAt: number } refetchAll?: boolean + app?: Application } export type GatsbyHelpers = GatsbyNodeApiHelpers export type GatsbyReporter = Reporter