gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.26k stars 10.31k forks source link

Disable Client-Side Routing? #4337

Closed TuckerWhitehouse closed 5 years ago

TuckerWhitehouse commented 6 years ago

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

KyleAMathews commented 6 years ago

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

TuckerWhitehouse commented 6 years ago

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.

TuckerWhitehouse commented 6 years ago

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>
)
master12 commented 6 years ago

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.

ichenzhifan commented 6 years ago

how about this one? any solution for this?

master12 commented 6 years ago

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.

alex-greco-harrys commented 5 years ago

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?

jgierer12 commented 5 years ago

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

callicoder commented 5 years ago

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?

pieh commented 5 years ago

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

alex-greco-harrys commented 5 years ago

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 commented 5 years ago

@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.

TuckerWhitehouse commented 5 years ago

@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.

ethagnawl commented 5 years ago

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.

Neddz commented 5 years ago

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.

alex-greco-harrys commented 5 years ago

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!

jtwillig commented 5 years ago

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/

ethagnawl commented 5 years ago

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.

KyleAMathews commented 5 years ago

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.

ethagnawl commented 5 years ago

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.

wardpeet commented 5 years ago

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);
};
ethagnawl commented 5 years ago

@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.

TuckerWhitehouse commented 5 years ago

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.

ethagnawl commented 5 years ago

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?

wardpeet commented 5 years ago

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:

wardpeet commented 5 years ago

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.

gatsbot[bot] commented 5 years ago

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! 💪💜

ivanoats commented 5 years ago

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

wardpeet commented 5 years ago

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.

bryancuster commented 5 years ago

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

ethagnawl commented 5 years ago

@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.

TuckerWhitehouse commented 5 years ago

@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.

brianbento commented 5 years ago

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.

wardpeet commented 5 years ago

@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

brianbento commented 5 years ago

@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.

ethagnawl commented 5 years ago

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

brianbento commented 5 years ago

@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?

ethagnawl commented 5 years ago

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.

brianbento commented 5 years ago

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

xavivars commented 5 years ago

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?

ivanoats commented 5 years ago

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

ethagnawl commented 5 years ago

@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.

xavivars commented 5 years ago

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?

wardpeet commented 5 years ago

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.

ethagnawl commented 5 years ago

@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.

xavivars commented 5 years ago

@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"

brianbento commented 5 years ago

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

xavivars commented 5 years ago

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;
}
cameron-martin commented 5 years ago

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);
    }
  };
}
wardpeet commented 5 years ago

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