Pulling Ordinary Files from WordPress Media Library Using Gatsby Resolvers

Last Updated:
Moving documents from one place to another

Back in September 2019 I wrote an article that covered the downloading and caching of images from the WordPress Media Library into Gatsby. I was working with relatively normal Gatsby builds, that only needed access to images from WordPress.

Since then I was began work on a project that needed to pull in both images (JPGs/PNGs) and various other file types from the media library (SVGs/XSLs/PDFs).

I had initially thought that the resolver that pulled in the images from WordPress would also be able to access the files. This was not the case. This is due to several reasons.

sourceUrl and not mediaItemUrl

In the original resolver I had been using the sourceUrl value from WPGraphQL. This works perfectly for images. However, if this plugin sees that the file is not an image – this value does not get populated. Or it would be populated with a strange value. I.E. if calling a PDF, the URL would be of the PDFs cover image – not of the PDF file (helpful for the future, but not for now).

That is where mediaItemUrl comes in. After a little bit of digging I found this value that contained the file URL without mattering what the filetype was. Perfect.

Node Filetype

When pulling in only image files, the resolver uses the “file” node type, created by gatsby-source-filesystem. This is very useful! However when pulling in actual files, I did not need all the added extras. I just wanted a direct URL to where it was stored in my Gatsby Static folder.

I added a separate section of the resolver to introduce the “string” node type, and then used Node to copy the file from WordPress into the static folder and update the URL.

Here is the final code:

exports.createResolvers = ({
    actions,
    cache,
    createNodeId,
    createResolvers,
    getNode,
    store,
    reporter,
    getNodeAndSavePathDependency,
}) => {
    const { createNode, touchNode, createNodeField } = actions

    const supportedExtensions = ['jpeg', 'jpg', 'png', 'webp', 'tif', 'tiff']

    const checkIfCorrectFileType = ({ source, processType }) => {
        // If the file is not a supported image, and we are meant
        // to be checking images, return nothing to the resolver

        const fileExt = source.mediaItemUrl.split('.').pop()

        if (processType === 'image' && supportedExtensions.includes(fileExt)) {
            return true
        }

        if (
            processType === 'document' &&
            !supportedExtensions.includes(fileExt)
        ) {
            return true
        }

        return false
    }

    const fileProcessor = async ({
        processType,
        source,
        getNode,
        store,
        cache,
        reporter,
        touchNode,
        createNodeId,
        getNodeAndSavePathDependency,
        createRemoteFileNode,
        context,
        createNodeField,
        pathPrefix = '',
    }) => {
        if (source.mediaItemUrl) {
            // Check the filetype here, rather than using the nodeFile extention
            // function as it means we do not need to download the file twice
            if (
                checkIfCorrectFileType({
                    source,
                    processType,
                })
            ) {
                let fileNodeID
                let fileNode
                let sourceModified

                // Set the file cacheID, get it (if it has already been set)
                const mediaDataCacheKey = `wordpress-media-${source.mediaItemId}`
                const cacheMediaData = await cache.get(mediaDataCacheKey)

                if (source.modified) {
                    sourceModified = source.modified
                }

                // If we have cached media data and it wasn't modified, reuse
                // previously created file node to not try to redownload
                if (
                    cacheMediaData &&
                    sourceModified === cacheMediaData.modified
                ) {
                    fileNode = getNode(cacheMediaData.fileNodeID)

                    // check if node still exists in cache
                    // it could be removed if image was made private
                    if (fileNode) {
                        // https://www.gatsbyjs.org/docs/node-creation/#freshstale-nodes
                        touchNode(fileNode)
                    }
                }

                // If we don't have cached data, download the file
                if (!fileNodeID) {
                    try {
                        // Get the filenode
                        fileNode = await createRemoteFileNode({
                            url: source.mediaItemUrl,
                            store,
                            cache,
                            createNode,
                            createNodeId,
                            auth: {
                                htaccess_user: process.env.BASIC_AUTH_USER,
                                htaccess_pass: process.env.BASIC_AUTH_PASS,
                            },
                            reporter,
                        })
                        if (fileNode) {
                            fileNodeID = fileNode.id

                            await cache.set(mediaDataCacheKey, {
                                fileNodeID,
                                modified: sourceModified,
                            })
                        }
                    } catch (e) {
                        // Ignore
                        console.log(e)
                        return null
                    }
                }

                // If this is a document, it needs to moved to the
                // static folder if it has not been moved yet
                if (processType === 'document') {
                    const fileName = `${fileNode.name}-${fileNode.internal.contentDigest}${fileNode.ext}`

                    const publicPath = path.join(
                        process.cwd(),
                        `public`,
                        `static`,
                        fileName
                    )

                    // if the file does not exist
                    if (!fs.existsSync(publicPath)) {
                        await fs.copy(
                            fileNode.absolutePath,
                            publicPath,
                            err => {
                                if (err) {
                                    console.error(
                                        `error copying file from ${fileNode.absolutePath} to ${publicPath}`,
                                        err
                                    )
                                }
                            }
                        )
                    }

                    return `${pathPrefix}/static/${fileName}`
                }

                if (fileNode) {
                    return fileNode
                }
            }
        }
        return null
    }

    createResolvers({
        WPGraphQL_MediaItem: {
            imageFile: {
                type: `File`,
                async resolve(source, args, context, info) {
                    return await fileProcessor({
                        processType: 'image',
                        source,
                        getNode,
                        touchNode,
                        store,
                        cache,
                        createNodeField,
                        reporter,
                        createNodeId,
                        getNodeAndSavePathDependency,
                        createRemoteFileNode,
                        context,
                    })
                },
            },
            staticUrl: {
                type: `String`,
                async resolve(source, args, context, info) {
                    const outcome = await fileProcessor({
                        processType: 'document',
                        source,
                        getNode,
                        touchNode,
                        store,
                        cache,
                        createNodeField,
                        reporter,
                        createNodeId,
                        getNodeAndSavePathDependency,
                        createRemoteFileNode,
                        context,
                    })

                    return outcome
                },
            },
        },
    })
}

How To Use It

A few things are required when using this resolver. When querying an image in the GraphQL query the following options need to be included:

  • sourceUrl
  • mediaItemId
  • modified

This means that a query to get a posts featured image would be written like this:

The ‘templateFile’ is a custom ACF field that we are grabbing the static file from.

 

query GET_POSTS {
  posts {
    edges {
      node {
        featuredImage {
          sourceUrl
          mediaItemId
          modified
          imageFile {
            childImageSharp {
              fluid(maxWidth: 650) {
                base64
                aspectRatio
                src
                srcSet
                sizes
              }
            }
          }
        }
        templateFiles {
          fileURL
        }
      }
    }
  }
}

Related Posts

Helpful Bits Straight Into Your Inbox

Subscribe to the newsletter for insights and helpful pieces on React, Gatsby, Next JS, Headless WordPress, and Jest testing.