Description
I am new and learning GatsbyJs/NetlifyCM/GraphQL so I am sorry if I come across as stupid. I, however, have been trying to fix this issue for 5 days now and am at my wits end. I am trying to implement NetlifyCMS into a GatsbyJS blog starter. I seem to have everything working except the images. Graphql is querying the image URL as "img/image.jpg" which is what is set in the MD file, and that works fine. However, when I make a new post with NetlifyCMS it sets the URL to "/img/image.jpg" and graphql can't seem to find it. The extra slash in front causes the error. If I remove the slash OR put a dot in front of the slash, "./img/image.jpg" it builds fine which leads me to believe relative paths may be working? I have read issue 4123, issue 13938, issue 5990, As well as multiple other similar issues and blogs and still, am unable to get it to work. I have tried implementing gatsby-remark-relative-images and gatsby-plugin-netlify-cms but neither seems to fix it.
Any help would be appreciated. I am newer at web development and very new to gatsby/graphql. Thank you.
Steps to reproduce
remove or add a / in front of the image URL in the MD file. Here is a link to the repo to clone/download https://github.com/AaronCuddeback/blog.aaroncuddeback.com
Environment
System:
OS: Windows 10
CPU: (16) x64 Intel(R) Core(TM) i9-9900KF CPU @ 3.60GHz
Binaries:
Node: 12.13.0 - C:\Program Files\nodejs\node.EXE
npm: 6.13.0 - ~\AppData\Roaming\npm\npm.CMD
Languages:
Python: 2.7.17 - /usr/bin/python
Browsers:
Edge: 44.18362.387.0
npmPackages:
gatsby: 2.18.2 => 2.18.2
gatsby-image: 2.2.33 => 2.2.33
gatsby-plugin-canonical-urls: 2.1.15 => 2.1.15
gatsby-plugin-emotion: 4.1.15 => 4.1.15
gatsby-plugin-feed: 2.3.21 => 2.3.21
gatsby-plugin-google-analytics: 2.1.28 => 2.1.28
gatsby-plugin-netlify: ^2.1.25 => 2.1.25
gatsby-plugin-netlify-cms: ^4.1.28 => 4.1.28
gatsby-plugin-netlify-cms-paths: ^1.3.0 => 1.3.0
gatsby-plugin-postcss: 2.1.15 => 2.1.15
gatsby-plugin-react-helmet: 3.1.15 => 3.1.15
gatsby-plugin-sharp: 2.3.2 => 2.3.2
gatsby-plugin-sitemap: 2.2.21 => 2.2.21
gatsby-plugin-typescript: 2.1.19 => 2.1.19
gatsby-remark-abbr: 2.0.0 => 2.0.0
gatsby-remark-copy-linked-files: 2.1.30 => 2.1.30
gatsby-remark-images: 3.1.33 => 3.1.33
gatsby-remark-prismjs: 3.3.24 => 3.3.24
gatsby-remark-relative-images: ^0.2.3 => 0.2.3
gatsby-remark-responsive-iframe: 2.2.27 => 2.2.27
gatsby-remark-smartypants: 2.1.16 => 2.1.16
gatsby-source-filesystem: 2.1.38 => 2.1.38
gatsby-transformer-json: 2.2.19 => 2.2.19
gatsby-transformer-remark: 2.6.37 => 2.6.37
gatsby-transformer-sharp: 2.3.5 => 2.3.5
gatsby-transformer-yaml: 2.2.17 => 2.2.17
File Contents:
gatsby-config.js:
const path = require('path');
module.exports = {
siteMetadata: {
title: 'Ghost',
description: 'The professional publishing platform',
siteUrl: 'https://gatsby-casper.netlify.com', // full path to blog - no ending slash
},
mapping: {
'MarkdownRemark.frontmatter.author': 'AuthorYaml',
},
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/static/img`,
name: 'uploads',
},
},
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/src/content`,
name: 'content',
},
},
{
resolve: `gatsby-plugin-netlify-cms-paths`,
options: {
cmsConfig: `/static/admin/config.yml`,
},
},
'gatsby-plugin-sitemap',
'gatsby-plugin-sharp',
{
resolve: 'gatsby-transformer-remark',
options: {
plugins: [
`gatsby-plugin-netlify-cms-paths`,
{
resolve: `gatsby-remark-relative-images`,
options: {
name: 'uploads', // Must match the source name ^
},
},
{
resolve: 'gatsby-remark-images',
options: {
maxWidth: 1170,
quality: 90,
},
},
{
resolve: 'gatsby-remark-responsive-iframe',
options: {
wrapperStyle: 'margin-bottom: 1rem',
},
},
'gatsby-remark-prismjs',
'gatsby-remark-copy-linked-files',
'gatsby-remark-smartypants',
'gatsby-remark-abbr',
],
},
},
'gatsby-transformer-json',
{
resolve: 'gatsby-plugin-canonical-urls',
options: {
siteUrl: 'https://gatsby-casper.netlify.com',
},
},
'gatsby-plugin-emotion',
'gatsby-plugin-typescript',
'gatsby-transformer-sharp',
'gatsby-plugin-react-helmet',
'gatsby-transformer-yaml',
'gatsby-plugin-feed',
{
resolve: 'gatsby-plugin-postcss',
options: {
postCssPlugins: [require('postcss-color-function'), require('cssnano')()],
},
},
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: 'UA-XXXX-Y',
// Puts tracking script in the head instead of the body
head: true,
// IP anonymization for GDPR compliance
anonymize: true,
// Disable analytics for users with `Do Not Track` enabled
respectDNT: true,
// Avoids sending pageview hits from custom paths
exclude: ['/preview/**'],
// Specifies what percentage of users should be tracked
sampleRate: 100,
// Determines how often site speed tracking beacons will be sent
siteSpeedSampleRate: 10,
},
},
`gatsby-plugin-netlify-cms`,
`gatsby-plugin-netlify`,
],
};
gatsby-node.js
const path = require('path');
const _ = require('lodash');
const { fmImagesToRelative } = require('gatsby-remark-relative-images');
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
fmImagesToRelative(node);
// Sometimes, optional fields tend to get not picked up by the GraphQL
// interpreter if not a single content uses it. Therefore, we're putting them
// through `createNodeField` so that the fields still exist and GraphQL won't
// trip up. An empty string is still required in replacement to `null`.
switch (node.internal.type) {
case 'MarkdownRemark': {
const { permalink, layout, primaryTag } = node.frontmatter;
const { relativePath } = getNode(node.parent);
let slug = permalink;
if (!slug) {
slug = `/${relativePath.replace('.md', '')}/`;
}
// Used to generate URL to view this content.
createNodeField({
node,
name: 'slug',
value: slug || '',
});
// Used to determine a page layout.
createNodeField({
node,
name: 'layout',
value: layout || '',
});
createNodeField({
node,
name: 'primaryTag',
value: primaryTag || '',
});
}
}
};
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
{
allMarkdownRemark(
limit: 2000
sort: { fields: [frontmatter___date], order: ASC }
filter: { frontmatter: { draft: { ne: true } } }
) {
edges {
node {
excerpt
timeToRead
frontmatter {
title
tags
date
draft
image {
childImageSharp {
fluid(maxWidth: 3720) {
aspectRatio
base64
sizes
src
srcSet
}
}
publicURL
}
author {
id
bio
avatar {
children {
... on ImageSharp {
fixed(quality: 90) {
src
}
}
}
}
}
}
fields {
layout
slug
}
}
}
}
allAuthorYaml {
edges {
node {
id
}
}
}
}
`);
if (result.errors) {
console.error(result.errors);
throw new Error(result.errors);
}
// Create post pages
const posts = result.data.allMarkdownRemark.edges;
// Create paginated index
const postsPerPage = 6;
const numPages = Math.ceil(posts.length / postsPerPage);
Array.from({ length: numPages }).forEach((_, i) => {
createPage({
path: i === 0 ? '/' : `/${i + 1}`,
component: path.resolve('./src/templates/index.tsx'),
context: {
limit: postsPerPage,
skip: i * postsPerPage,
numPages,
currentPage: i + 1,
},
});
});
posts.forEach(({ node }, index) => {
const { slug, layout } = node.fields;
const prev = index === 0 ? null : posts[index - 1].node;
const next = index === posts.length - 1 ? null : posts[index + 1].node;
createPage({
path: slug,
// This will automatically resolve the template to a corresponding
// `layout` frontmatter in the Markdown.
//
// Feel free to set any `layout` as you'd like in the frontmatter, as
// long as the corresponding template file exists in src/templates.
// If no template is set, it will fall back to the default `post`
// template.
//
// Note that the template has to exist first, or else the build will fail.
component: path.resolve(`./src/templates/${layout || 'post'}.tsx`),
context: {
// Data passed to context is available in page queries as GraphQL variables.
slug,
prev,
next,
primaryTag: node.frontmatter.tags ? node.frontmatter.tags[0] : '',
},
});
});
// Create tag pages
const tagTemplate = path.resolve('./src/templates/tags.tsx');
const tags = _.uniq(
_.flatten(
result.data.allMarkdownRemark.edges.map(edge => {
return _.castArray(_.get(edge, 'node.frontmatter.tags', []));
}),
),
);
tags.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag)}/`,
component: tagTemplate,
context: {
tag,
},
});
});
// Create author pages
const authorTemplate = path.resolve('./src/templates/author.tsx');
result.data.allAuthorYaml.edges.forEach(edge => {
createPage({
path: `/author/${_.kebabCase(edge.node.id)}/`,
component: authorTemplate,
context: {
author: edge.node.id,
},
});
});
};
exports.onCreateWebpackConfig = ({ stage, actions }) => {
// adds sourcemaps for tsx in dev mode
if (stage === `develop` || stage === `develop-html`) {
actions.setWebpackConfig({
devtool: 'eval-source-map',
});
}
};