menu

Gatsby.js

Fast in every way that matters. Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps.

Channels
# All channels
view-forward
# General
view-forward
# I Made This
view-forward
# Meta
view-forward
# Themes
view-forward
Team

Unchanging names for static files

February 18, 2020 at 2:58pm

Unchanging names for static files

February 18, 2020 at 2:58pm
I think I asked this a while back and didn't get a response. I need to pull in PDF files from Wordpress with names like 'annual-report-2020.pdf' and have those exact names persist on the Gatsby site, rather than getting hashes appended to them. Is there a way to do this?

February 19, 2020 at 2:44am
Need a bit more information here. How are you getting your data? Also, how are these nodes being created? createRemoteFileNode?
Edited
  • reply
  • like

February 21, 2020 at 8:17pm
I am pulling this in from my WordPress REST API:
reports": [ { "report_year": "2015-16", "report_file": { "ID": 1368, "id": 1368, "title": "WFAA_Annual_Report_2015-16", "filename": "WFAA_Annual_Report_2015-16.pdf", "filesize": 2358176, "url": "https://wp.advanceuw.org/wp-content/uploads/2019/12/WFAA_Annual_Report_2015-16.pdf", ... } }, { "report_year": "2016-17", "report_file": { "ID": 1367, "id": 1367, "title": "wfaa_annual_report_201617", "filename": "wfaa_annual_report_201617.pdf", "filesize": 2817769, "url": "https://wp.advanceuw.org/wp-content/uploads/2019/12/wfaa_annual_report_201617.pdf", ... } } ],
and my graphQL looks like this: reports { report_year report_file { url { localFile { publicURL } } } }
This was the only way I could figure out how to upload the PDFs in the CMS and have them available on the Gatsby site. But the value of "publicURL" has a hash in it, like: wfaa_annual_report_201617-d7f2db98dc39ca64fd2dc4b58d8fb7cf.pdf
So that's what I'm trying to eliminate.
Edited
  • reply
  • like
Can you show your gatsby-node? Just redact any sensitive information (like domain names, secrets, anything like that). I want to replicate what you’re doing with my own servers, and see if I can get it to cooperate.
Edited
  • reply
  • like
const _ = require('lodash')
const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const getOnlyPublished = edges =>
_.filter(edges, ({ node }) => node.status === 'publish')
exports.sourceNodes = ({ getNodes, actions }) => {
const { createNodeField } = actions;
const pageNodes = getNodes().filter(
node => node.internal.type === "wordpress__PAGE"
);
pageNodes.forEach(pageNode => {
let pathFragments = [];
let tmpNode = pageNode;
do {
pathFragments.push(tmpNode.slug);
tmpNode = pageNodes.find(
node => node.wordpress_id === tmpNode.wordpress_parent
);
} while (tmpNode);
const fullpath = pathFragments.reverse().join("/");
createNodeField({
node: pageNode,
name: `fullpath`,
value: fullpath
});
});
};
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions
return graphql(`
{
allWordpressPage {
edges {
node {
id
slug
status
page_info {
web_page
}
fields {
fullpath
}
}
}
}
}
`)
.then(result => {
if (result.errors) {
result.errors.forEach(e => console.error(e.toString()))
return Promise.reject(result.errors)
}
const getTemplateFor = (fullpath) => {
const fixed = (fullpath !== undefined) ? fullpath.replace(/-/g, '_') : '';
console.log(fixed);
const mapping = {
about: 'top',
annual_report: 'annual_report',
board: 'board',
bts: 'wide-promos',
careers: 'careers',
contact: 'info',
event_waiver: 'info',
history: 'wide-promos',
home: 'home',
leadership: 'leadership',
locations: 'locations',
jobs: 'jobs',
policies: 'wide-promos',
privacy_policy: 'info',
refund_policy: 'info',
}
let tpl = '';
switch (mapping[fixed]) {
case 'annual_report': {
tpl = `./src/templates/page-annual-report.js`
break
}
case 'home': {
tpl = `./src/templates/page-home.js`
break
}
case 'careers': {
tpl = `./src/templates/page-careers.js`
break
}
case 'jobs': {
tpl = `./src/templates/page-jobs.js`
break
}
case 'top': {
tpl = `./src/templates/page-top.js`
break
}
case 'wide': {
tpl = `./src/templates/page-wide.js`
break
}
case 'info': {
tpl = `./src/templates/page-info.js`
break
}
case 'locations': {
tpl = `./src/templates/page-locations.js`
break
}
case 'wide-promos': {
tpl = `./src/templates/page-wide-use-promos.js`
break;
}
case 'leadership': {
tpl = `./src/templates/page-use-promos-alt.js`
break
}
case 'board': {
tpl = `./src/templates/page-board.js`
break
}
default: {
tpl = `./src/templates/page.js`
break
}
}
console.log('fixed:', fixed)
if (fixed.startsWith('bts/') || fixed.endsWith('messages_from_mike')) {
tpl = `./src/templates/page-spotlight-article.js`
}
console.log('fullpath: ', fullpath, ' tpl: ', tpl)
return path.resolve(tpl);
}
// Only publish pages with a `status === 'publish'` in production. This
// excludes drafts, future posts, etc. They will appear in development,
// but not in a production build.
const allPages = result.data.allWordpressPage.edges
const pages =
process.env.NODE_ENV === 'production'
? getOnlyPublished(allPages)
: allPages
// Call `createPage()` once per WordPress page
_.each(pages, ({ node: page }) => {
const pagePath = ( page.slug === 'home') ? `/` : `/${page.fields.fullpath}/`
createPage({
path: `${pagePath}`,
component: getTemplateFor(page.fields.fullpath),
context: {
id: page.id,
},
})
})
})
.then(() => {
return graphql(`
{
allWordpressWpJobs {
edges {
node {
id
slug
}
}
}
}
`)
})
.then(result => {
if (result.errors) {
result.errors.forEach(e => console.error(e.toString()))
return Promise.reject(result.errors)
}
const jobTemplate = path.resolve(`./src/templates/page-job.js`)
_.each(result.data.allWordpressWpJobs.edges, ({ node: job }) => {
console.log('job: ', `/jobs/${job.slug}`)
createPage({
path: `/jobs/${job.slug}`,
component: jobTemplate,
context: {
id: job.id,
},
})
})
})
}
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
exports.onCreateWebpackConfig = ({ getConfig, stage, loaders, actions }) => {
const config = getConfig()
if (stage.startsWith('develop') && config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
'react-dom': '@hot-loader/react-dom',
react: path.join(process.cwd(), "node_modules/react")
}
}
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /react-flickity-component/,
use: loaders.null(),
},
],
},
})
}
}
  • reply
  • like
Okay, so no custom node creation, it looks like. And since you said REST API, I'm assuming it's from gatsby-source-wordpress, and not a custom API call or gatsby-source-graphql. Which is all fine.
Pretty sure gatsby-source-wordpress is using createRemoteFileNode; I don't know for sure, because I haven't looked, but the behavior makes sense. Here's what happens in createRemoteFileNode:
So far it's just dealing with the cache, and not with the static files.
Now, gatsby-node of gatsby-source-filesystem adds a GraphQL field extension (with a bit of an older API).
Look at what the publicURL resolver does:
So you can see why it's returning files named like that.
You might be able to overload that publicURL resolver, but I don't know if that's the best route. Probably could add your own custom resolver that returns the real name of the file? Would that work for you? It wouldn't point to the real file on the disk, but you could add a proxy of sorts, so you can know the real name of the file, but have a unique name on the disk
Edited
  • reply
  • like

February 23, 2020 at 12:26am
Okay, I finally had some time to create a replication. I tried to get it to work how I wanted to with createSchemaCustomization, but it just didn't work out, and I didn't bother investigating deeper to figure out why that api doesn't override setFieldsOnGraphQLNodeType (setFieldsOnGraphQLNodeType probably runs later...).
If you read through some of the source from the links I posted last, you would have seen that publicURL is what actually copies the files from your cache to your public directory. The main issue with the way that resolver does it, is it puts them all in the root of the public/static directory. In order to ensure there is not any file name collisions, it appends the contentDigest to the filename. The contentDigest is a hash of the File node content, so it will always be unique, so long as all Nodes have unique IDs (which is a guarantee, because otherwise there's an error). That means the final static file name will always be unique.
Because I wanted to keep the actual file name, but not have to worry about unique filenames, I decided it would make sense to duplicate the folder structure from the server. However, I only wanted to do this for content coming from Wordpress, because I wanted to also avoid causing issues with local files, or files that came from somewhere else, winding up without unique names, since they might not have the desired folder structure I needed to keep everything neatly isolated. See the below code that works for me:
// gatsby-node.js
const path = require('path');
const fs = require('fs-extra');
const { GraphQLString } = require('gatsby/graphql');
exports.setFieldsOnGraphQLNodeType = ({ type, getNodeAndSavePathDependency, pathPrefix = `` }) => {
if (type.name !== `File`) {
return {}
}
return {
publicURL: {
type: GraphQLString,
args: {},
description: `Copy file to static directory and return public url to it`,
resolve: async (file, fieldArgs, context) => {
const details = getNodeAndSavePathDependency(file.id, context.path)
const isWordpressContent = file.url && file.url.indexOf(`wp-content`) > -1
const relativePath = isWordpressContent
? file.url.replace(/^.*wp-content\/uploads\//, '').split('/')
: [`${file.name}-${file.internal.contentDigest}${details.ext}`];
const publicPath = path.join(
process.cwd(),
`public`,
`static`,
...relativePath,
)
if (!fs.existsSync(publicPath)) {
await fs.ensureDir(path.dirname(publicPath));
fs.copy(details.absolutePath, publicPath, err => {
if (err) {
console.error(
`error copying file from ${details.absolutePath} to ${publicPath}`,
err
)
}
})
}
return `${pathPrefix}/static/${relativePath.join('/')}`
},
},
}
}
Before, I was seeing the same behavior as you. After I implemented this code, problem solved. Assuming I have a PDF called "Test_PDF.pdf" in my media library, this query:
query PDFQuery {
allWordpressWpMedia {
nodes {
localFile {
publicURL
name
}
}
}
}
returns this data:
{
"data": {
"allWordpressWpMedia": {
"nodes": [
{
"localFile": {
"publicURL": "/static/2020/02/Test_PDF.pdf",
"name": "Test_PDF"
}
}
]
}
}
}
And I can link to and view/download that file from my running site.
Edited
  • reply
  • like