Gatsby: Disable Client-Side Routing?

Created on 2 Mar 2018  ·  69Comments  ·  Source: gatsbyjs/gatsby

Description

I have a use case where the server is defining some custom routes. When the browser loads these routes, the expected content is shown for a brief moment until client side routing takes over and replaces the page with the 404 because the url in the browser is not recognized.

My first thought was that maybe the matchPath could be used here, but I won't necessarily know the url patterns that would render these pages, and there may be some overlap in what the url is, and what page is returned.

I'm guessing it may be possible with some hook into find-page but I'm not sure what that would look like.

Environment

Gatsby version: 1.9.221
Node.js version: 8.9.1
Operating System: macOS

Actual result

After the browser loads, the expected page is shown briefly until the javascript loads, determines the url is unknown, and renders the 404 page.

Expected behavior

The server rendered page should be available at the custom url, and not be replaced by the 404 page when the client loads.

Steps to reproduce

1. git clone https://github.com/TuckerWhitehouse/gatsby-client-routing-issue

2. npm install

3. npm run build

4. npm start

5. open http://localhost:3000

awaiting author response question or discussion

Most helpful comment

Based on how active this thread continues to be, there seems to be a non-trivial number of users who desire this behavior.

To quickly reiterate my use case: I want to (did!) use Gatsby as a static _page_ generator -- as opposed to a static _site_ generator -- and because the Gatsby "page" is injected into a containing page whose URL is out of my control and subject to change, I don't want the Gatsby application _ever_ mucking with the URL. Out of the box, Gatsby _mostly_ supports this use case and is a great pleasure to use, but it does makes some assumptions -- again, because of its standard static _site_ use case -- that result in the need for hacks like the ones mentioned above.

So, is there any hope for the ability to disable client side routing becoming a top-level config option? I would be happy to submit a PR, but don't want to sink time into it if there's no chance it'll be accepted.

All 69 comments

Why don't you know what paths the server is rendering?

It's less that I won't know the paths (I can write a regex that will match them) but more the overlap. The actual setup is behind an apache server that reverse proxies to a bunch of different apps, including the gatsby site. If any of those apps become unavailable, or return an internal server error, we return a custom error page that is part of the gatsby site.

So at any point in time, if app1 is unavailable or misbehaving, any requests to /app1 would return the content of /error/unavailable.html or /error/internal.html, and the same would be true for app2, and so on.

Using a matchPath like /^(app1|app2)/.*/, on both the unavailable an internal error pages doesn't work because findPage doesn't know (based on the url) which page I actually intend to show the user.

I was able to get something working using a global variable and "patching" ___history and ___loader in onClientEntry. It's very fragile because of the dependency on gatsby exposing those globals - not sure if there is some way to generalize this and add it to gatsby.

// gatsby-browser.js
exports.onClientEntry = () => {
  // Check for a custom pathname
  const pathname = global.___pathname
  if (!pathname) return

  // Override the history location
  const history = global.___history
  history.location.pathname = pathname

  // Patch the resource loader
  const loader = global.___loader
  const { getResourcesForPathname } = loader

  loader.getResourcesForPathname = (path, ...args) => {
    return getResourcesForPathname(path === location.pathname ? pathname : path, ...args)
  }
}
// src/pages/page1.js
import React from "react"
import Helmet from 'react-helmet'

export default () => (
  <div>
    <Helmet>
      <script>{`window.___pathname = '/page1'`}</script>
    </Helmet>
    <div>Page 1!</div>
  </div>
)

I also agree that there should be a build option to disable this feature. We also have an unconventional setup and would like to disable this feature temporarily while we finish our migration to a full Gatsby site.

A simple flag at build time would be perfect.

how about this one? any solution for this?

We ended up modifying the pages.json to match the path that we needed. We basicallay called addPagesArray with the corrected path name.

I still dont understand why this throws an error? The page loads fine and works. This at most should be a warning when it cant match the path.

That being said I dont know if there is a more elegant way of modifying the pages.json thru a config vs runtime code.

I want to bump this issue.

A project I am working on is experiencing a similar issue. We are build a landing page generator that will build single-page Gatsby apps. This issue comes when we try to serve a landing page outside of it's domain.

So for example we have our main Gatsby app www.example.com. We have a service that will take the Gatsby landing pages and serve them at www.example.com/trial. So a landing page URL would look like www.example.com/trail/ad-123 The page initially loads fine until all the JS loads and the router takes over. The landing page looks at the path and doesn't know where it is, so it tries to change the path to place the page at the root, looking like this www.example.com/ad-123, which results in a 404 redirect.

Are there any plans to add a configurable option to fix this? Would the Gatsby team be open to a PR?

@alex-greco-harrys It seems to me that a path prefix is what you'll want to use in that scenario.

I also needed to disable client side routing to run Google Adsense properly on my website.

Google Adsense auto ads don't detect client side routing and the ads don't refresh when routes are updated.

Is there anyway I can disable client side routing?

You can use a tags instead of gatsby-link in cases like that

I was able to get something working using a global variable and "patching" ___history and ___loader in onClientEntry. It's very fragile because of the dependency on gatsby exposing those globals - not sure if there is some way to generalize this and add it to gatsby.

// gatsby-browser.js
exports.onClientEntry = () => {
  // Check for a custom pathname
  const pathname = global.___pathname
  if (!pathname) return

  // Override the history location
  const history = global.___history
  history.location.pathname = pathname

  // Patch the resource loader
  const loader = global.___loader
  const { getResourcesForPathname } = loader

  loader.getResourcesForPathname = (path, ...args) => {
    return getResourcesForPathname(path === location.pathname ? pathname : path, ...args)
  }
}
// src/pages/page1.js
import React from "react"
import Helmet from 'react-helmet'

export default () => (
  <div>
    <Helmet>
      <script>{`window.___pathname = '/page1'`}</script>
    </Helmet>
    <div>Page 1!</div>
  </div>
)

@TuckerWhitehouse where are you getting ___history, ___loader from? When I try to replicate your example those two properties of global are undfined.

@alex-greco-harrys It seems to me that a path prefix is what you'll want to use in that scenario.

@jgierer12 That helps solve the first piece of my issue. The second piece is that the final path is unknown until the page is rendered. We have a learning service that takes a collection of static pages and serves them based off conversion rates. So at a path example.com/go/ we could be serving 1 of a collection of pages. So we wouldn't be serving the page at a path like example.com/go/first-page or example.com/go/second-page. Those both would be served at example.com/go/page path.

Essentially what I am trying to accomplish is to serve a gatsby page at whatever path I want.

@alex-greco-harrys those globals were exposed by gatsby v1. With the upgrade to v2, I know the underlying router was switched from react-router to reach-router, so my guess would be those globals were affected.

I'm also hoping to use Gatsby to build a single page application and would like to disable routing entirely. Does anyone know of a workaround (a la @TuckerWhitehouse's) that would be compatible with V2?

UPDATE:
While I wasn't able to find a solution which would _disable_ client side routing, I was able to prevent the redirect referenced by @alex-greco-harrys and others by setting:

window.page = window.page || {};
window.page.path = window.location.pathname;

in gatsby-browser.js which short circuits this conditional check in production-app.js. That conditional redirect attempts to "make the canonical path match the actual path" and results in the (IMO) unexpected behavior referenced above.

I also need this.

I am currently using code generated by Gatsby on another project and I use it on multiple pages. I am using Gatsby as it generates static code. Therefore, I used the pathPrefix so I could generate everything under a specific path and serve it. That way, everything gets requested there and then rendered as a fragment of a page. However, I get unwanted redirects all the time to the pathPrefix because it is in the scripts. I have to manually remove the condition that @ethagnawl mentioned everytime I build. I just tried his solution but it didn't work for me.

I'm also hoping to use Gatsby to build a single page application and would like to disable routing entirely. Does anyone know of a workaround (a la @TuckerWhitehouse's) that would be compatible with V2?

UPDATE:
While I wasn't able to find a solution which would _disable_ client side routing, I was able to prevent the redirect referenced by @alex-greco-harrys and others by setting:

window.page = window.page || {};
window.page.path = window.location.pathname;

in gatsby-browser.js which short circuits this conditional check in production-app.js. That conditional redirect attempts to "make the canonical path match the actual path" and results in the (IMO) unexpected behavior referenced above.

@ethagnawl I have a hacky sort of solution to produce a single page app that can be served at any URL. By single page, I actually mean one single page with no routing at all.

If you look at the following Gatsby example: https://github.com/gatsbyjs/gatsby/tree/master/examples/client-only-paths .

You can edit this file on line 15 to look like <Page path="/*" {...props} /> and delete line 16. When you build this application, any path will result in serving the Page you have defined. From there you can make that Page whatever you want. Now if you need to host this page at an arbitrary path you will see no redirection.

I was unable to figure out how this solution could work with multiple pages in an app. The goal for my project was to serve a single Gatsby page (marketing landing page) at any URL I want.

Not sure if this helps in your use case, but maybe this can fuel some future discovery!

I was able to accomplish this by following the Customizing html.js directions in the docs and removing {this.props.postBodyComponents}

https://www.gatsbyjs.org/docs/custom-html/

Based on how active this thread continues to be, there seems to be a non-trivial number of users who desire this behavior.

To quickly reiterate my use case: I want to (did!) use Gatsby as a static _page_ generator -- as opposed to a static _site_ generator -- and because the Gatsby "page" is injected into a containing page whose URL is out of my control and subject to change, I don't want the Gatsby application _ever_ mucking with the URL. Out of the box, Gatsby _mostly_ supports this use case and is a great pleasure to use, but it does makes some assumptions -- again, because of its standard static _site_ use case -- that result in the need for hacks like the ones mentioned above.

So, is there any hope for the ability to disable client side routing becoming a top-level config option? I would be happy to submit a PR, but don't want to sink time into it if there's no chance it'll be accepted.

This seems like a reasonable feature to add @ethagnawl. I think it'd need a very long and obnoxious name like dangeouslySetInnerHTML so people are fully conscious of what they're doing as this is a very special edge case.

My first pass at a PR addressing this issue can be found here. I'd greatly appreciate feedback from maintainers and/or other users who've bumped into this issue.

Thanks for creating aPR @ethagnawl

could you remind me again why won't the following work?

// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = ({ page, actions }) => {
  const { createPage } = actions;
  page.matchPath = `${page.path}*`;
  createPage(page);
};

@wardpeet I'm sure that will work and looks similar to the solution I mentioned above. However, those sorts of solutions are hard to document and potentially fragile (see the solution offered by @TuckerWhitehouse which no longer works).

IMO, codifying this concept is worthwhile as, again, it makes documentation more straightforward and this flag could also be used to make additional optimizations by bypassing/noop-ing/etc. functionality that isn't relevant when Gatsby is being used in this way.

Additionally, using the matchPath requires the url in the browser to reflect the page you want to render, but this breaks down when you are injecting a gatsby site into an unknown location. (My original issue was around having gatsby behind an apache reverse proxy and not knowing the routes that would cause a certain page to render).

@ethagnawl do you think it would be possible to disable routing at the page level (something like page.__disable_client_side_routing__ = true)? This would probably resolve the original issue I was having as well.

do you think it would be possible to disable routing at the page level

I don't see why not? Would that be in addition to or in place of my proposed solution? If it's the latter, is there any advantage to doing that at the page level?

I've setup this repo :)
https://github.com/wardpeet/gatsby-plugin-static-site

unsure if this works for your use cases.
For now you need to do

git clone https://github.com/wardpeet/gatsby-plugin-static-site
npm install
npm run build
npm link

cd "into your project"
npm link gatsby-plugin-static-site

add gatsby-plugin-static-site to your gatsby-config.js

Let me know if this is ok for your use case, I have no intention to actually support it so i'm happy to transfer it :smile:

I updated the repo as I had something wrong in my gitignore file (thanks @m-allanson). I also published it to npm under my own name.

so installation can be done

npm install --save @wardpeet/gatsby-plugin-static-site

and add @wardpeet/gatsby-plugin-static-site to gatsby-config.json

If this is looking good then I can add some test and some options to disable this behaviour for develop.

Hiya!

This issue has gone quiet. Spooky quiet. 👻

We get a lot of issues, so we currently close issues after 30 days of inactivity. It’s been at least 20 days since the last update here.

If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not stale" to keep this issue open!

Thanks for being a part of the Gatsby community! 💪💜

I would like this to stay open as it looks like it’s waiting on review

I'm not sure it's not stale, I made a fix and hoping to get feedback for it
https://github.com/gatsbyjs/gatsby/issues/4337#issuecomment-470075540

If no one responds I think it's a good idea to close this one as it is probably resolved.

@wardpeet is it possible with that plugin to conditionally disable client side routing?

@wardpeet IIRC, your proposed fix is the first step in the process of (maybe, eventually) getting this feature added as a Gatsby config option. So, that _sort of_ makes it out of band as far as this issue and Gatsby are concerned, but I might make the argument that this issue should remain open in order to continue that conversation.

@wardpeet the original issue was about disabling client side routing for specific routes, @ethagnawl brought up the use case of disabling routing for an entire site which is what I believe your plugin addresses.

I need to disable client side routing temporarily while I migrate a site on an old CMS to Gatsby. I'm doing it one page at a time before flipping the switch completely over to just gatsby.

I've tried @wardpeet plugin but it seems to not be working.

@brianbento do you have a reproduction? if you can create an issue on the repo https://github.com/wardpeet/gatsby-plugin-static-site I can have a look what's missing

@wardpeet I'll see if I can get something up. The main issue is that your previously mentioned "fix"(https://github.com/gatsbyjs/gatsby/issues/4337#issuecomment-465497418) doesn't work when you use pathPrefix. It always "corrects" the address to what's expected.

@brianbento Have you tried the solution I mentioned above? That has worked well for me on two different projects.

@ethagnawl I still get the URL correction and then it doubles up the path prefix.

So "/" becomes "//" after the canonical doesn't match.

Maybe I'm implementing it wrong. Did you use path prefix for your pages? Am I supposed to have the static site plugin active?

Did you use path prefix for your pages?

Yes. I was also using this branch to allow for remote assets. So, it's _possible_ that branch introduced behavior which made the fix work for me and not you.

However, it's more likely that recent Gatsby releases (I haven't kept up) have broken my "solution", as it was a hack to begin with.

@ethagnawl Super helpful! Thank you! I know @DSchau made some plugins to test out the assetPrefix feature. I'll give it a shot!

It seems @wardpeet solution doesn't work when you also need to use path prefix. Having a solution like the one proposed __disable_client_side_routing__ that disabled the canonical check would be super cool. I'd be happy to work on it and submit a PR if it'll be considered for merging. Any support for that idea, or do you think it doesn't fit into the roadmap?

@xavivars I would like that and find it useful, for sure.

@xavivars I took a pass at that feature a while back, which was closed in favor of what @wardpeet's solution. If you're going to consider revisiting that approach, it could be worth having a look at my attempt.

Thanks @ethagnawl ! I took a look at your approach, and that was pretty much what I was thinking.

I think @wardpeet solution covers a different use case: the app doesn't behave as an SPA because links are actually changing the browser location.href, so then it navigates "server side". But I couldn't make it work for my use case because the initial redirect is still happening: my initial hypothesis is there's some sort of interaction with prefixPath that makes this condition to evaluate to true

https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/production-app.js#L65

Did you manage to make it work properly?

I might be able to do more work on the plugin if anyone can explain me the actual issue as I kinda forgot what I need to fix. Maybe most of this can be resolved by the newly merged assetPrefix option?

Sorry for not understanding.

@xavivars

Did you manage to make it work properly?

Both my hack and the submitted PR were _working properly_ for my use case: single, static page generation with no redirects. I gave up on my PR because the Gatsby team seemed to prefer a plugin instead of an app-level config option.

I never got around to trying @wardpeet's plugin, as I'd already wrapped up the Gatsby project I was working on by the time it was published. So, I can't comment on whether or not that was ever _working properly_.

@wardpeet: the assetPrefix doesn't fix it. I managed to have it working by using both your plugin (to, basically, disable client side "navigation" when clicking around) and the workaround mentioned by @ethagnawl few months ago

https://github.com/gatsbyjs/gatsby/issues/4337#issuecomment-453244611

That workaround is needed to disable the "navigate" event that happens onClientEntry. We fixed adding a hook there and applying that workaround (forcing page.path to the actual path). But I have no idea if that has any side effects, and also how long will it take until it stops working (is not more than a hack).

Ideally, I think this should be an app-level config, given the amount of people that seems is using gatsby as a "static page generator"

@xavivars Do you have link you can share with you work around?

Not really, but this is pretty much it:

Add the static plugin mentioned before, and in gastby-browser.js

exports.onClientEntry = () => {
    window.page = window.page || {};
    window.page.path = window.location.pathname;
}

The following also works, although it relies on gatsby internals:

export function onInitialClientRender() {
  window.___navigate = (to, { replace }) => {
    if (replace) {
      window.location.replace(to);
    } else {
      window.location.assign(to);
    }
  };
}

Does using https://github.com/wardpeet/gatsby-plugin-static-site & assetPrefix work?

Linked issue which made the example work, unsure if this is what you actually need.
https://github.com/wardpeet/gatsby-plugin-static-site/issues/1#issuecomment-494802726

To me, it worked with all three things: gatsby plugin static site + assetPrefix + disabling "navigate" as shown above

Seems like I still have a hard time understanding what is needed here. I could just fix the issue with gatsby-plugin-static-site & assetPrefix.

@wardpeet Do you have a link to a demo which uses gatsby-plugin-static?

@ethagnawl sorry to keep you waiting.

I made a demo:
https://github.com/wardpeet/gatsby-demos/tree/static-asset-prefix

site is live:
https://zen-wright-33c2d8.netlify.com/

As expected (and anticipated by @TuckerWhitehouse and @ethagnawl), a fragile solution like the one provided in https://github.com/gatsbyjs/gatsby/issues/4337#issuecomment-453244611 doesn't work anymore, due to a change in Gatsby 2.9.2, for several reasons:

The first one, solvable, is that this line
window.page.path = window.location.pathname;
needs to be replaced with
window.pagePath = window.location.pathname;
in order to avoid the change in the URL client side.

But that has undesired side-effects: pagePath is set with the _wrong_ path, and page-data.json is not loaded anymore (as it relies on the original path of the page, and not the one where it's finally rendered)

https://github.com/gatsbyjs/gatsby/commit/49fd769f695ccfa6e990e3eaae7c886f073db19b#diff-2d21ea42ec874a0988977e57b17251aa

It seems the only option to make this work now would be to really introduce a variable like __disable_client_side_routing__ or, at least, __disable_client_side_canonical_redirect__ to shortcircuit this condition: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/production-app.js#L69

@wardpeet: do you see any problem on introducing such config variable?

We rather don't want to enable these escape hatches in core. What I basically understand of the issue, is this:

I have a gatsby site and a path /my-special-path and on my server I have a route called /something-else. If I rewrite /something else to /gatsby/my-special-path, it won't work because it tries to change the page into /my-special-path?

If so I'll see if I can fix it in my plugin. Do you maybe have a live demo of this?

Yes, that is exactly the problem. I'll try to put together another PR (that doesn't add something that invasive as the global config variable as https://github.com/gatsbyjs/gatsby/pull/15173).

I have something that may be acceptable that I will push as another PR in a few minutes

@wardpeet this is what I think woudl be needed to add to Gatsby so your plugin could be extended. I've added some examples and documentation on the PR

https://github.com/gatsbyjs/gatsby/pull/15180

After a conversation with @DSchau in Discord, it seems the core contributors are aligned that a solution like #15173 or #15180 shouldn't live in core, but in a plugin. So I'd like to explore other options to solve it.

Currently the only ways I found was via a global config variable (#15173 ) to shortcircuit the canonical redirect check, or by allowing to modify the perceived rendered URL for gatsby (#15180), so the canonical redirect check doesn't directly depend on window.location, but on a filterable variable.

IMHO, the challenge is to use a plugin to extend/override something that doesn't seem to be extensible/overridable right now (directly relying on window.location without the values injected from anywhere makes it really hard to me), but there may be other ways to have this behavior implemented without modifying core's code.

@xavivars I'll be merging https://github.com/wardpeet/gatsby-plugin-static-site/pull/4 and publishing a fix for this.

A demo: (page 5 has a canonical redirect)
https://static-asset-prefix--zen-wright-33c2d8.netlify.com/

I've just published @wardpeet/gatsby-plugin-static-site version 0.1.0. This should fix this issue. Feel free to reopen if it didn't fix all your issues.

Best way to get better static site support is to create an issue at the plugin itself. https://github.com/wardpeet/gatsby-plugin-static-site/issues/new

Anyone encountered this after using the above plugin?

Any workaround working for the current version of GatsbyJS?

I tried:
https://github.com/wardpeet/gatsby-plugin-static-site

but it's not working for me. I raised an issue here:
https://github.com/wardpeet/gatsby-plugin-static-site/issues/13

I also created a sample repo to reproduce the redirect issue:
https://github.com/isi-gach/gastby-static/tree/create-react-app

@isi-gach Would you mind providing your take on the root issue (what you're expecting, what you're seeing, what you'd like to see)? A few of us in this thread have tried, but it might help to get a fresh take on it.

hi @ethagnawl

I'm expecting that the browser URL doesn't change but I'm seeing the URL changing, in the following video the URL changes from /demo/index.html to /public/
https://www.youtube.com/watch?v=SxYbaDidnkY

That video was recorded using the sample repo that I have created:
https://github.com/isi-gach/gastby-static/tree/create-react-app

I'm trying to prevent the redirect using @wardpeet/gatsby-plugin-static-site but doesn't seem to work.

Hi @isi-gach @ethagnawl,

There are a couple of pull requests open into @wardpeet plug-in that should solve the problem you're mentioning.

While they get merged, you can use my fork instead

Hi @xavivars
I tried the npm from your fork and now URL doesn't change but I got a white page:
https://www.youtube.com/watch?v=uNzk9UYVCxk

That video was recorded using the following sample repo replacing the wardpeet by plugin by yours:
https://github.com/isi-gach/gastby-static/tree/create-react-app

how do i disable client side routing just for single page?

You can use this

exports.onPreBootstrap = ({ store }) => {
  const { program } = store.getState()
  const filePath = path.join(program.directory, '.cache', 'production-app.js')

  const code = fs.readFileSync(filePath, {
    encoding: `utf-8`,
  })

  const newCode = code.replace(
    `const { pagePath, location: browserLoc } = window`,
    `const { pagePath } = window
    let { location: browserLoc } = window

    if (window.parent.location !== browserLoc) {
      browserLoc = {
        pathname: pagePath
      }
    }
  `
  )

  fs.writeFileSync(filePath, newCode, `utf-8`)
}

I'm not sure if it covers all the use cases the plugin covers, but it works fine for my case.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

magicly picture magicly  ·  3Comments

timbrandin picture timbrandin  ·  3Comments

andykais picture andykais  ·  3Comments

jimfilippou picture jimfilippou  ·  3Comments

hobochild picture hobochild  ·  3Comments