I ran into an interesting problem while trying to host a small graphics project on my Gatsby website.
For context, I’m using Three.js to render a snow globe on a web canvas. Writing the snow globe itself was relatively easy. Getting Gatsby to handle the assets the way I wanted was the harder part.
Here’s why:
I export the model in GLTF format, which consists of a primary .gltf file and
some associated files—in my case, a .bin file. The Three.js loader takes the
URL of the .gltf file and resolves those other paths relative to it.
The problem is that Gatsby’s static asset pipeline adds a hash to each file’s
name. For example, model.bin might become model-<hash>.bin. This is useful
for cache invalidation, but it breaks the relative path stored in the GLTF file.
So, what can we do?
Here’s the solution I came up with:
- Use a folder named
assetsto tell Gatsby to treat these files differently. - Generate one hash for the entire folder.
- Copy the assets without changing their individual names.
- Store the folder under a path that includes the hash.
Here’s how that looks in my gatsby-node.ts config:
async function scanForAssetFolders(
baseDir: string
): Promise<{[path: string]: AssetFolderInfo}> {
const foldersToScan = [baseDir];
const assetFolders: {[path: string]: AssetFolderInfo} = {};
while (foldersToScan.length > 0) {
const folder = foldersToScan.pop()!;
for (const filename of await fs.readdir(folder)) {
const filepath = path.resolve(folder, filename);
const filestat = await fs.stat(filepath);
if (!filestat.isDirectory()) {
continue;
}
if (filename === 'assets') {
const mtimeStr = filestat.mtime.getSeconds().toString();
const hash = crypto.createHash('md5')
.update(filepath)
.update(mtimeStr)
.digest('hex');
assetFolders[filepath] = {hash};
continue;
}
foldersToScan.push(filepath);
}
}
return assetFolders;
}
export const onCreateWebpackConfig: GatsbyNode["onCreateWebpackConfig"] = async ({
actions,
getConfig
}) => {
const config = getConfig();
const assetFolders = await scanForAssetFolders(path.resolve('src'));
for (const rule of config.module.rules) {
rule.exclude = /\/assets\//;
}
config.module.rules.push({
test: /\/assets\//,
use: {
loader: 'file-loader',
options: {
outputPath: (url: string, resourcePath: string) => {
const splitIdx = resourcePath.lastIndexOf('/assets/') + 7;
const assetFolderPath = resourcePath.slice(0, splitIdx);
const assetFilePath = resourcePath.slice(splitIdx + 1);
const hash = assetFolders[assetFolderPath].hash;
return path.join('assets', hash, assetFilePath);
}
}
}
});
actions.replaceWebpackConfig(config);
};The GLTF and .bin file now stay together, while the parent folder still gets a
new path when the assets change. So far, this works well!