GatsbyJs client only paths goes to 404 page when the url is directly accessed in browser in "production"

Viewed 13571

I have created a Gatsby app and configured gatsby-node.js to a create client only paths, which are all working fine in development while directly accessing the url of the path but not in production.

example :

if(page.path.match(/^\/sample/)){
     page.matchPath = "/sample/:value1/:value2/:value3";
     createPage(page)
  }

I am using heroku to deploy the app

10 Answers

The Why

While the client-side router knows about this path, there is no corresponding HTML file. When the browser looks at the site it first loads the 404.html file generated by gatsby, which includes the client-side router. Once the router completes its initialization it reads the path and loads the correct page. Meaning you end up at the right place but there's half a second of landing on the wrong page.

How to fix It

The general solution is to tell your server to redirect the /sample/ path to your /sample/index.htmlfile. The way to do this depends on your host, but I'll provide the name of the technique for various hosts in case you want to look it up. It's usually called URL Rewriting and should be supported by every major hosting platform.

Heroku

The Heroku section of the gatsby deploy documentation suggests using the heroku-buildpack-static module which has built-in support for "custom routes" which will solve this for your case using syntax like this:

{
  "routes": {
    "/sample/**": "sample/index.html",
  }
}

AWS Amplify

You need to add the redirect in the AWS Amplify console. For this example, the params are:

  • Source URI: /sample/<*>
  • Target URI: /sample/index.html
  • Type: 200

This solves my problem

pages/404.jsx

import React, { useEffect, useState } from 'react';

export default ()=>{
  const [isMount, setMount] = useState(false);
  
  useEffect(() => {
        setMount(true);
  },[])

  if(!isMount) {
     return(
        <div>loading</div>
     )
  }

  return (
     <div>Page Not Found</div>
  )


}

This will not show a 404 error on the client-side render until the response is 404.

For anyone who uses S3 (gatsby-plugin-s3) and CloudFront, I've resolve 302/404 by adding generateMatchPathRewrites: false in the gatsby-plugin-s3 config and creating a Lambd@Edge function for origin request with code below:

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  if (/^\/app\//i.test(request.uri)) {
      request.uri = '/app/index.html';
  }
  return request;
};

Though 302/404 went away, I still have the issue when doing a hard refresh. For example when I am at /app/page2 and hit refresh, the default component in /app will load then half a second later the component in /app/page2 will show up but in a weird way. Some css classes of /app is mixed in /app/page2. If anyone has any idea about this, please let me know.

I looked in the public folder to find the index file for the client route I created

for netlify I added the below to the netlify.toml file at the root.


[[redirects]]
  from = "/user/dashboard/"
  to = "/user/index.html"
  status = 200

For Firebase hosting, I added the following to my firebase.json:

"rewrites": [
  {
      "source": "**",
      "destination": "/loading/index.html"
  }

]

Then I created a new loading.js file under /page. With this configuration, Firebase first responds with loading.html then Gatsby handles the client routing.

Vercel

For anyone looking trying to solve this on Vercel platform, the answer is rewrites: https://vercel.com/docs/configuration#project/rewrites

If you have a gatsby app with client-only paths, as per something like https://www.gatsbyjs.com/plugins/gatsby-plugin-create-client-paths/, it's likely you will encounter this issue on deployment.

Example: your application has a path like mything.com/app/user-profile.

In this case, app/* are client-only routes that don't get generated on the server, so you may get a flash of a 404 when you reload this route or land on it directly from an external link, for example.

To solve, add a rewrite on the /app/* server side route in Vercel to point back to /app where, it is assumed, your routing is controlled by Reach router or similar in your pages/app.js file.

To set this up on Vercel, create a vercel.json in the root of your gatsby/react project and add the following configuration:

{
  "rewrites": [{ "source": "/app/:match*", "destination": "/app" }]
}

This issue will generally only manifest itself when deployed to production/staging. It won't occur in development (via gatsby develop anyway).

NGINX

If your environment includes Nginx you should add something like this into your virtual host config:

rewrite ^/products/([0-9]+)$ /products/[id]/index.html;

or

rewrite ^/users/confirm/(.+)$ /users/confirm/[code]/index.html;

etc.

Just go to your production root (by default this is a /public directory) and look for the correct path to the index.html file for your case. For example: /public/products/[id]/index.html

Usually your virtual host config is located here: /etc/nginx/sites-enabled/mysite.com.conf

The heroku solution was not working for me, but I don't know exactly why. My frontend is fetching data from a express.js back end. I solved my problem by adding the following lines in my server.js to redirect my clients only routes.

app.get('/user/**', (req, res) => {
  res.sendFile(path.join(`${__dirname}/public/user/index.html`));
});

For anyone using Gatsby Cloud, you add this to your gatsby-node.js

Working with Redirects and Rewrites

// gatsby-node.js

exports.createPages = async ({ actions: { createRedirect } }) => {
 
  ...

  createRedirect({
    fromPath: '/sample/*',
    toPath: '/sample/index.html
  });
};

Related