Recently I have started working with Gatsby and right now I'm trying things with MDX, In my MDX file i can work with Gatsby Image throught GraphQL, but I want to use Static Image from gatsby-plugin-image and I'm getting errors like this:
react_devtools_backend.js:2557 Image not loaded
https://images.unsplash.com/photo-1597305877032-0668b3c6413a?w=1300
When i try to implement this image inside .tsx it works so I'm wondering if it is possible or not.
gatsby-config
"gatsby-remark-images",
{
resolve: "gatsby-plugin-mdx",
options: {
defaultLayouts: {
default: require.resolve("./src/components/common/Layout.tsx")
},
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {},
},
],
}
},
{
resolve: "gatsby-source-filesystem",
options: {
name: "images",
path: `${__dirname}/src/images/`,
},
__key: "images",
},
Then in test.mdx I'm trying to use Static Image like this:
<StaticImage
src={'https://images.unsplash.com/photo-1597305877032-0668b3c6413a?w=1300'}
alt={''}
width={3840}
height={1000}
layout={'constrained'}
/>
You can't use gatsby-plugin-image directly in an MDX document. This post on the Gatsby blog explains how to use it indirectly by passing in image reference props via your Frontmatter.
Personally I've been able to do it like so:
This example only loads local images, refer to the blog post for how to reference remote ones as it's more complex.
Template Component
import React from "react";
import { graphql } from "gatsby";
import { MDXRenderer } from "gatsby-plugin-mdx";
import Layout from "../components/layout";
const Game = ({ data }) => {
const { mdx } = data;
const { frontmatter, body } = mdx;
return (
<Layout title={frontmatter.title}>
<span className="date">{frontmatter.date}</span>
<MDXRenderer localImages={frontmatter.embeddedImagesLocal}>
{body}
</MDXRenderer>
</Layout>
);
};
export const pageQuery = graphql`
query($slug: String!) {
mdx(slug: { eq: $slug }) {
slug
body
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
embeddedImagesLocal {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`;
export default Game;
MDX Document
---
title: Death Stranding
author: Hideo Kojima
date: 2021-05-06
template: game
embeddedImagesLocal:
- '../images/20210513035720_1.jpg'
---
import { getImage, GatsbyImage } from 'gatsby-plugin-image';
A great game from Hideo Kojima.
<GatsbyImage alt='Sam in a destroyed mall' image={getImage(props.localImages[0])} />
You can use the gatsby-remark-images plugin with gatsby-plugin-mdx and it handles the images for you.
Install the plugin, then in gatsby-config.js, update the gatsby-plugin-mdx to something like this:
{
resolve: 'gatsby-plugin-mdx',
options: {
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 900,
},
},
],
plugins: [`gatsby-remark-images`]
},
},
Then images work as expected using the ![alt](url) markdown format.
Unfortunately the maxWidth is fixed across the site which wasn't ideal for me. I'm using the #bonobolabs/gatsby-remark-images-custom-widths fork which lets you specify the image width in the MDX file with an HTML style img tag:
<img src="./image.jpg" alt="My image" width="500px"/>
I believe it's only 'width' that is the extra property this gives you.
Related
I'm attempting to pull an image: /src/images/es.png
And to display it on a gatsby page. Here's the page code:
import React from "react"
import {
Header,
Image,
} from 'semantic-ui-react';
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '#/components/layout'
import SEO from '#/components/seo'
export const query = graphql`
query {
file(
relativePath: { eq: "images/es.png" },
sourceInstanceName: {
eq: "images"
}
) {
childImageSharp {
# Specify the image processing specifications right in the query.
# Makes it trivial to update as your page's design changes.
fixed(width: 125, height: 125) {
...GatsbyImageSharpFixed
}
}
}
}
`
export default ({ data }) => (
<Layout>
<SEO title="Earthshaker" />
<div style={{ height: '100%' }}>
<Header
as="h1"
style={{
color: 'white',
}}
>
Earthshaker
</Header>
{ JSON.stringify(data) }
{/* <Img fixed={data.file.childImageSharp.fixed} /> */}
</div>
</Layout>
)
Here's the config code:
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: path.join(__dirname, `src`, `images`),
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
Here's my file structure:
- src
- images
- es.png
- components
- heroes
- earthshaker <---- (this is the page code HERE)
I'm expecting the image to get pulled out but I always get file: null. What am I doing wrong?
Assuming that your query works, I'd change your filesystem paths to:
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`,
},
},
In addition, I would check the query to see if it gathers the expected result in the GraphQL playground (localhost:8000/___graphql). The rest of the code looks fine.
I recently learnt about lazy loading components and started using it. Now I am trying to prefetch the lazy loaded components as well as vue-router routes. But using the chrome devtools I found that lazy loaded chunks are only loaded when we actually navigate to the lazy loaded route (in case of a vue-router route) or when the v-if evaluates to true and the component is rendered (in case of a lazy loaded component).
I have also tried using the webpackPrefetch: true magic string in the router as well as component import statement but doing that does not seem to make any difference.
Project structure:
Master-Detail layout
router config:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
var routes = [
{
path: "/DetailPage",
component: () => import(/* webpackChunkName: "Detail-chunk" */ "AppModules/views/MyModuleName/DetailPage.vue")
},
{
path: "/MasterPage",
component: () => import("AppModules/views/MyModuleName/MasterPage.vue")
}
]
export const router = new Router({
routes: routes,
stringifyQuery(query) {
// encrypt query string here
}
});
export default router;
Master view:
<template>
<div #click="navigate">
Some text
</div>
</template>
<script>
export default {
name: "MasterPage",
methods: {
navigate() {
this.$router.push({
path: "/DetailPage",
query: {},
});
},
},
};
</script>
Details page:
<template>
<div>
<my-component v-if="showComponent" />
<div #click="showComponent = true">Show Component</div>
</div>
</template>
<script>
const MyComponent = () => import(/* webpackChunkName: "MyComponent-chunk" */ "AppCore/components/AppElements/Helpers/MyComponent");
export default {
name: "DetailPage",
components: {
MyComponent,
},
data() {
return {
showComponent: false
}
}
};
</script>
vue.js.config file:
const path = require("path");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
module.exports = {
publicPath: "some-url",
outputDir: "./some/path",
chainWebpack: webapckConfig => {
webapckConfig.plugin("html").tap(() => {
return [
{
inject: true,
filename: "index.html",
template: "./public/index.html"
}
];
});
},
productionSourceMap: true,
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "server",
generateStatsFile: false,
statsOptions: {
excludeModules: "node_modules"
}
})
],
output: {
filename: "some file name",
libraryTarget: "window"
},
module: {
rules: [
{
test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: "url-loader",
options: {
limit: 50000,
fallback: "file-loader",
outputPath: "/assets/fonts",
name: "[name].[ext]?hash=[hash]"
}
}
]
}
]
},
resolve: {
alias: {
vue$: process.env.NODE_ENV == 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
AppCore: path.resolve(__dirname, "..", "..", "AppCoreLite"),
AppModules: path.resolve(__dirname, "..", "..", "AppModulesLite")
}
}
}
};
Both the async route and component do get split into separate chunks but these chunks are not prefetched.
When I navigate to the master view, I dont see Detail-chunk.[hash].js in the network tab. It gets requested only when the navigate method in the master page is executed (this the correct lazy load behaviour without prefetch).
Now when I am on the details page, MyComponent-chunk.[hash].js is only requested when the showComponent becomes true (on click of a button)
I've also read at a few places that vue-cli v3 does has prefetch functionality enabled by default and webpack magic string is not needed. I also tried that by removing the webpackPrefetch comment but it made no difference.
I did vue-cli-service inspect and found that prefetch plugin is indeed present in the webpack config:
/* config.plugin('preload') */
new PreloadPlugin(
{
rel: 'preload',
include: 'initial',
fileBlacklist: [
/\.map$/,
/hot-update\.js$/
]
}
),
/* config.plugin('prefetch') */
new PreloadPlugin(
{
rel: 'prefetch',
include: 'asyncChunks'
}
),
UPDATE: I tried removing the prefetch webpack plugin using config.plugins.delete('prefetch'); and then using the webpack magic comment: /* webpackPrefetch: true */ but it made no difference.
How do I implement prefetch functionality?
I solved this by creating a simple prefetch component that loads after a custom amount of time.
Prefetch.vue
<script>
import LazyComp1 from "./LazyComp1.vue";
import LazyComp2 from "./LazyComp2.vue";
export default {
components:{
LazyComp1,
LazyComp2,
}
}
</script>
App.vue
<template>
<Prefech v-if="loadPrefetch"></Prefech>
</template>
<script>
export default {
components: {
Prefech: () => import("./Prefetch");
},
data() {
return {
loadPrefetch: false
}
},
mounted() {
setTimeout(() => {
this.loadPrefetch = true;
}, 1000);
}
}
</script>
Lazy loaded components are meant to be loaded only when user clicks the route. If you want to load component before it, just don't use lazy loading.
vue-router will load components to memory and swap the content of the tag dynamically even if you will use normally loaded component.
You need to implement vue-router-prefetch package for your need. Here is a working demo.
Note: From the working demo, you can notice from console.log that only page 2 is prefetched by the QuickLink component imported from vue-router-prefetch
Code :
import Vue from "vue";
import Router from "vue-router";
import RoutePrefetch from "vue-router-prefetch";
Vue.use(Router);
Vue.use(RoutePrefetch, {
componentName: "QuickLink"
});
const SiteNav = {
template: `<div>
<ul>
<li>
<router-link to="/page/1">page 1</router-link>
</li>
<li>
<quick-link to="/page/2">page 2</quick-link>
</li>
<li>
<router-link to="/page/3">page 3</router-link>
</li>
</ul>
</div>`
};
const createPage = (id) => async() => {
console.log(`fetching page ${id}`);
return {
template: `<div>
<h1>page {id}</h1>
<SiteNav />
</div>`,
components: {
SiteNav
}
};
};
const routers = new Router({
mode: "history",
routes: [{
path: "/",
component: {
template: `<div>
<h1>hi</h1>
<SiteNav />
</div>`,
components: {
SiteNav
}
}
}]
});
for (let i = 1; i <= 3; i++) {
routers.addRoutes([{
path: `/page/${i + 1}`,
component: createPage(i + 1)
}]);
}
export default routers;
I'm working on a mobile app. and wanted to load some components dynamically while showing the splash screen.
#Thomas's answer is a good solution (a Prefetch component), but it doesn't load the component in the shadow dom, and Doesn't pass Vetur validation (each component must have its template)
Here's my code:
main.vue
<template>
<loader />
</template>
<script>
import Loader from './Loader'
const Prefetch = () => import('./Prefetch')
export default {
name: 'Main',
components: {
Loader,
Prefetch
}
}
</script>
Prevetch.vue
<template>
<div id="prefetch">
<lazy-comp-a />
<lazy-comp-b />
</div>
</template>
<script>
import Vue from 'vue'
import LazyCompA from './LazyCompA'
import LazyCompB from './LazyCompB'
Vue.use(LazyCompA)
Vue.use(LazyCompB)
export default {
components: {
LazyCompA,
LazyCompB
}
}
</script>
<style lang="scss" scoped>
#prefetch {
display: none !important;
}
</style>
The loader component is loaded & rendered, then the Prefetch component can load anything dynamically.
since vue-router-prefetch didn't work for me i ended up doing it manually.
Vue 3 Example - all routes are iterated on page load and async components are loaded
const router = createRouter({
history: createWebHistory(),
routes: [{
path: '/',
component: HomeView
}, {
path: '/about',
component: () => import('./views/AboutView.vue')
}]
});
async function preloadAsyncRoutes() {
// iterate all routes and if the component is async - prefetch it!
for (const route of router.getRoutes()) {
if (!route.components) continue;
// most routes have just a "default" component unless named views are used - iterate all entries just in case
for (const componentOrImporter of Object.values(route.components)) {
if (typeof componentOrImporter === 'function') {
try {
// prefetch the component and wait until it finishes before moving to the next one
await componentOrImporter();
} catch (err) {
// ignore failing requests
}
}
}
}
}
window.addEventListener('load', preloadAsyncRoutes);
This seems to be a relatively common problem. I am trying to generate blog post pages but am experiencing this error and the pages show a 404 on load. Which means that they are not being generated.
Here is my code for the gatsby.node.js file:
exports.createPages = async ({ graphql, useStaticQuery, actions: { createPage } }) => {
const postQuery = graphql(`
{
gcms {
posts(where: { stage: PUBLISHED }) {
id
slug
}
}
}
`);
const {
gcms: { posts },
} = useStaticQuery(postQuery);
posts.forEach(({ id, slug }) =>
createPage({
path: `/blog/${slug}`,
component: require.resolve(`./src/templates/PostPage.js`),
context: {
id: id,
slug: slug,
},
})
);
};
And my code for the blog post PostPage.js file:
/* eslint-disable react/prop-types */
import React from 'react';
import { graphql } from 'gatsby';
import Layout from "../components/layout";
//import galaxy from "../images/galaxy.jpg";
import SEO from "../components/seo";
export const postPageQuery = graphql`
query PostPageQuery($id: ID!) {
gcms {
post(where: { id: $id }) {
title
slug
excerpt
postContentMarkdown
tags
author {
name
biography
}
seo {
title
description
keywords
}
}
}
}
`;
const PostPage = ({data: {post}}) => {
return (
<Layout>
<SEO
keywords={[
`ui`,
`ux`,
]}
title="Blog" />
{post.slug}
</Layout>
);
};
export default PostPage;
There are a few things that caught my attention and may fix your issue.
The usage of useStaticQuery in your gatsby-node.js. You don't need to fetch postQuery data with the static query hook since you are using the hook outside a component.
The usage of where filter. According to GraphQL documentation, the way to filter data is by using filter filter. In addition, when filtering the filtered criteria are strings, so must be quoted.
When you pass a field via context API to your PostPage, you should avoid filter to all your gcms since your template has the information of that post, is not needed to redo the same previous query again (same than gatsby-node.js), it's not optimal. I will let it there since I don't know how is your data structured but should be refactored.
Applying it to your code should look like this.
gatsby-node.js:
exports.createPages = async ({ graphql, useStaticQuery, actions: { createPage } }) => {
const postQuery = graphql(`
{
gcms {
posts(filter: { stage: "PUBLISHED" }) {
id
slug
}
}
}
`);
let {posts}= postQuery.gcms;
posts.forEach(({ id, slug }) =>
createPage({
path: `/blog/${slug}`,
component: require.resolve(`./src/templates/PostPage.js`),
context: {
id: id,
slug: slug,
},
})
);
};
PostPage.js:
/* eslint-disable react/prop-types */
import React from 'react';
import { graphql } from 'gatsby';
import Layout from "../components/layout";
//import galaxy from "../images/galaxy.jpg";
import SEO from "../components/seo";
export const postPageQuery = graphql`
query PostPageQuery($id: ID!) {
gcms {
post(filter: { id: $id }) {
title
slug
excerpt
postContentMarkdown
tags
author {
name
biography
}
seo {
title
description
keywords
}
}
}
}
`;
const PostPage = ({data: {post}}) => {
return (
<Layout>
<SEO
keywords={[
`ui`,
`ux`,
]}
title="Blog" />
{post.slug}
</Layout>
);
};
export default PostPage;
I ended up fixing this by doing a complete recomposition of my project, with an update to the latest version of Gatsby, this with a bare bones gatsby starter, plugin by plugin. It ended up being plugin conflict issue. I'm not sure which plugin exactly it was, but most likely it was one of these:
gatsby-plugin-eslint, gatsby-plugin-offline, gatsby-plugin-root-import or possibly the prop-types NPM package.
I experienced this same issue after upgrading to the latest version of Gatsby.
Similarly to Tapha's answer, it was a plugin conflict for me. I had yet to upgrade the gatsby-source-strapi plugin. Upgrading that package to its latest available version solved the issue. Whatever your data source happens to be, I would check that it's still playing nice with Gatsby.
So, this warning/error message is very misleading, you can still use an exported query from templates, as detailed in the Gatsby documentation here.
According to Gatsby docs, components that use static queries need to be split into variants: one being a pure component and the other that actually uses the static query, passing results to the pure component.
Given an example component:
import React from "react";
import { Helmet } from "react-helmet";
import { useStaticQuery, graphql } from "gatsby";
export const PureSEO = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
image = ``,
...metaData
}): JSX.Element => {
const metaDescription = description;
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: metaData.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
{
name: `og:image`,
content: image,
},
{
name: `twitter:image`,
content: image,
},
{
name: `image`,
content: image,
},
].concat(meta)}
/>
);
};
const SEO: React.FC<Props> = ({
description = ``,
lang = `en`,
meta = [],
title = ``,
}): JSX.Element => {
const { site, image } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
rootDir
}
}
image: file(relativePath: { eq: "favicon.png" }) {
childImageSharp {
fixed(width: 400) {
...GatsbyImageSharpFixed
}
}
}
}
`
);
return (
<PureSEO
{...site.siteMetadata}
description={description}
image={image}
lang={lang}
meta={meta}
title={title}
/>
);
};
interface Props {
description?: string;
lang?: string;
meta?: HTMLMetaElement[];
title: string;
}
export default SEO;
I don't get full test coverage because SEO is not tested; but I don't think I should be testing that component, since the only logical difference is that it is using static queries.
How can I achieve better or 100% test coverage for my components, given this pattern?
Problem
I understand the intention of the Gatsby documentation avoid executing useStaticQuery, but the approach implicitly recommends leaving code uncovered.
Recommendation
To achieve code coverage, the most logical thing to do would be to either mock useStaticQuery or better yet use MockProvider.
GraphQL Testing: MockProvider - https://www.apollographql.com/docs/react/development-testing/testing/#mockedprovider
This problem is quite similar to how you would do unit testing of a backend component that communicates with some kind of external service, such as an SQL database.
You could of course mock useStaticQuery which will give you higher coverage, but it will not actually make you any surer that your code will work. What you want to test revolves around the correctness of your GraphQL query and whether the response format is what you expect, which is not something you can unit test.
So if you want to be certain that your code works you'd need to perform integration testing, e.g. by running Cypress tests against the built website.
Theoretically, you could also use contract testing to check that your query itself performs as expected, but it seems like an unnecessarily complicated solution for this usecase.
👋
My current GatsbyJS project is a one pager with a carousel and some other content.
Background
The carousel should be filled with information about some products. My goal is to let the carousel build itself by iterating through all markdown files, picking the ones with these three lines at the top of the file:
---
type: product
---
So I've created a CarouselContainer (class component) and a Carousel component (functional component). The Container should load the markdown via GraphQL query and pass the resulting products object to it's nested component. Then the component should map over the object and create the carousel.
But there are also other markdown files for menu lists, text modals and so on. They have the type: page. I thought preparing a few GraphQL queries would be the solution. But it turned out to be more difficult than expected...
The container component is a class component, so I am not able to call the query directly in it (https://github.com/gatsbyjs/gatsby/issues/3991#issuecomment-364939030).
Then I thought putting multiple queries into the pages/index.js could be the solution.
export const indexQuery = graphql`
query IndexQuery {
allMarkdownRemark(filter: {frontmatter: {type: {eq: "page"}}}) {
edges {
node {
frontmatter {
title
text
}
}
}
}
}
`
export const productsQuery = graphql`
query ProductsQuery {
allMarkdownRemark(filter: {frontmatter: {type: {eq: "product"}}}) {
edges {
node {
id
frontmatter {
title
content
}
}
}
}
}
`
Nope again. Using GraphQL fragments should be a solution...
Q Can someone tell me how to prepare fragments for that purpose and/or have another idea how to get the markdown content right into my container?
Thanks for reading.
You're not too far off. GraphQL supports multiple discrete nodes being queried in the same query:
export const query = graphql`
{
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
pages: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "pages" } } }
) {
edges {
# ...
}
}
}
`
Note that I've used aliases to fetch the same initial node (allMarkdownRemark) with separate filters in the same GraphQL query. This will result in data.products and data.pages being passed into your default exported React component.
To clean this up, you can use fragments, allowing you to colocate your products query in your Carousel file:
In carousel.js (or whatever file houses your Carousel component):
export const query = graphql`
fragment Products on Query {
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
}
`
Then in your page file:
export const query = graphql`
{
pages: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "pages" } } }
) {
edges {
# ...
}
}
...Products
}
`
Note: if you're using Gatsby 1.x you'll need to change the on Query portion of the fragment to on RootQueryType.
Assuming you're using Gatsby v2, you can also use StaticQuery instead of combining the query into one. This is particularly useful if your pages have no bearing on the Carousel of products.
import React from "react";
import { graphql, StaticQuery } from "gatsby";
class Carousel extends React.Component {
// ...
}
export default props => (
<StaticQuery
query={graphql`
products: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "product" } } }
) {
edges {
# ...
}
}
`}
render={({ products }) => <Carousel products={products} {...props} />}
/>
);