Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Allow fallback to SPA in universal mode's generate #2690

Closed
jeroenvisser101 opened this issue Jan 25, 2018 · 26 comments
Closed

Proposal: Allow fallback to SPA in universal mode's generate #2690

jeroenvisser101 opened this issue Jan 25, 2018 · 26 comments
Labels

Comments

@jeroenvisser101
Copy link

jeroenvisser101 commented Jan 25, 2018

Intro

We're using Nuxt.js to build a site. We have an GraphQL API that we query to get all the content. Nuxt.js is then used to generate a 'prerendered' version of the site, that will be interactive once fully loaded. Consecutive page loads (user navigating around the site) only load GraphQL data (and if needed, additional JS).

Problem

Now, we were anticipating that the 'universal' mode would allow us to generate routes for all dynamic pages, but when someone adds new data, some problems arise.

Imagine the following on the async-data example when being served on a static hosting (e.g. Netlify, Firebase Hosting) being built by running nuxt generate:

  1. User navigates to home page
    We serve the generated index.html through Firebase Hosting, the page will become interactive but no additional queries are performed on the client side.
  2. User clicks on Blog
    We query the posts and show all post titles with a link to their respective routes.
  3. User clicks on a post that hasn't been generated (/posts/1 is generated, but all the others are not and exists only in the database
    We query the post by id, and we render the component.
  4. For some reason (refresh, user copies URL and shares it, etc.) the page is loaded from scratch
    Now, there are two ways this could go:
    1. Firebase responds with a 404 status (because the prerendered file could not be found)
    2. We could serve the index.html, but since it assumes that it was correctly generated, upon becoming interactive, it won't notice the route doesn't match the rendered content, and shows the wrong content (the homepage).

Proposal

So to solve this issue and to make Nuxt.js more flexible when used in 'universal' mode but deployed statically, I'd want to suggest a configuration option where—aside from the generated pages—a HTML file with the SPA template (with the loader and without any data) would be returned which would allow us to route all 'Not found' traffic to the SPA template.

This would allow us to build a platform where data changes trigger rebuilds of the site (e.g. run nuxt generate asynchronously) but where the route would function instantly.

I'd love to get your thoughts on this, to see if you have a better suggestion on handling cases like these. Thanks!

This question is available on Nuxt.js community (#c2338)
@jeroenvisser101
Copy link
Author

I implemented a first draft of this functionality in a module:

// modules/spa-fallback.js
const { writeFile } = require('fs-extra')
const { join } = require('path')

module.exports = function installSPAFallback () {
  this.nuxt.hook('generate:done', async generator => {
    let { html } = await this.nuxt.renderRoute('/', {spa: true})
    await writeFile(join(generator.distPath, '__spa-fallback.html'), html, 'utf8')
  })
}
// nuxt.config.js
{
  // ...
  modules: ['~/modules/spa-fallback'],
  generate: {
    subFolders: false,
    // ...
  }
}

Now when you use the following config on Firebase hosting:

// firebase.json
{
  "hosting": {
    "public": "./dist",
    "cleanUrls": true,
    "trailingSlash": false,
    "rewrites": [
      {"source": "**", "destination": "/__spa-fallback.html"}
    ]
  }
}

You can test this locally by running firebase serve.

@pi0
Copy link
Member

pi0 commented Jan 25, 2018

Hi. I love the idea behind this proposal. Currently, we generate a 200.html page on nuxt generate for SPA fallback (for surge). Can you please confirm renaming it to __spa-fallback.html will make it working with firebase?

@jeroenvisser101
Copy link
Author

jeroenvisser101 commented Jan 25, 2018

Hi @pi0! I've actually tried that before, but it didn't work when mode: 'universal' and running nuxt generate. Looking at the source of Nuxt.js, it's just a copy of the generated index.html

@jeroenvisser101
Copy link
Author

jeroenvisser101 commented Jan 25, 2018

It does work on mode: 'spa', which I think is what it's meant for, but for generated 'universal'-mode apps that are statically deployed, it wouldn't work.

@pi0
Copy link
Member

pi0 commented Jan 25, 2018

What if we change it to something like this and always call it (Both SSR and SPA generates)

async afterGenerate() {
    const { fallback } = this.options.generate // Defaults to 404.html

    if (!fallback) {
        return // Disabled
    }

    const fallbackPath = join(this.distPath, fallback)

    if (existsSync(fallbackPath)) {
        return // Prevent conflicts
    }

    const { html } = await this.nuxt.renderRoute('/', {spa: true})
    await writeFile(fallbackPath, html, 'utf8')
}

@jeroenvisser101
Copy link
Author

Right! I guess that would work really well, solving two issues instead of one:

  1. 200.html is being generated always, even when not on surge
  2. Unable to deploy universal apps to static hosting with dynamic routes for resources created after building

@pi0
Copy link
Member

pi0 commented Jan 25, 2018

What's your idea @Atinux @alexchopin @clarkdo?

@Atinux
Copy link
Member

Atinux commented Jan 26, 2018

I love this idea! I think we could call it 404.html when using nuxt generate with mode: 'universal'. This way it will work for Surge.sh and for Firebase it has only to point to 404.html, what do you think?

@jeroenvisser101
Copy link
Author

@Atinux Great! I would like to be able to configure it, since we might choose to only route posts/** to the fallback, but misspellings like auth/logn should show the actual 404.

Maybe have the SPAFallback key in the generate config. If true, defaults to 404.html, if set to a string, it will override the path?

Let me know what you think, will draft a PR for this tonight.

@Atinux
Copy link
Member

Atinux commented Jan 26, 2018

@jeroenvisser101 Actually since it's on SPA mode, it will try to match the url, and if it does not match anything, it will show the 404 page too :)

@pi0
Copy link
Member

pi0 commented Jan 26, 2018

Actually defaulting SPAFallback (or maybe simpler generate.fallback) option to 404.html may be a little misunderstanding. But I like this since it will also make SPA working with GitHub pages(spa-github-pages).

But misspellings like auth/logn should show the actual 404.

I think they will, since 404.html uses vue-router and if route was not found, it will show a 404 :)

@jeroenvisser101
Copy link
Author

@Atinux @pi0 ah, right! Only thing might be responding with a 404 status and then still loading the content, but that might not be half bad since the SEO meta tags haven't been generated, but the user still gets to see the content.

@Atinux
Copy link
Member

Atinux commented Jan 26, 2018

I think making by default a 404.html (when generate with universal only?) is the best option since it will work automatically with GitHub pages and Surge. We can add the option generate.fallback to give a custom name/path instead?

@Atinux
Copy link
Member

Atinux commented Jan 26, 2018

@jeroenvisser101 exactly, the point here is to not show a 404 page to the user while your are generating the website behind.

@jeroenvisser101
Copy link
Author

I think making by default a 404.html (when generate with universal only?) is the best option since it will work automatically with GitHub pages and Surge.

Totally, let's do this.

We can add the option generate.fallback to give a custom name/path instead?

This could be nice just in case someone has a use case where this is needed, maybe different hosting that requires special paths.

@DreaMinder
Copy link

DreaMinder commented Jan 26, 2018

This would allow us to build a platform where data changes trigger rebuilds of the site

I have been digging the same idea for a while and I am glad that someone not only thinks the same way, but also came up with implementation.
Let me share some ideas of building a "platform". Lets call prerendered pages a "static cache" in this case. So when a new page added, it appears simultaneously on API and SPA mode, but not for search engines. This is just one of cases of the "platform" thing. But what if page changes or removes from API? Not only this page, but also pages that depend on it, should be rerendered. Or at least switched to SPA fallback until all pages are rerendered.
Can we implement "cache invalidation" in this "platform solution"? Or at least make some decisions to be able to connect own "cache invalidation logic"?
I think this problem related to this issue.
sorry for my English btw

@pi0
Copy link
Member

pi0 commented Jan 26, 2018

@DreaMinder Cache invalidation would be as easy as doing rm dist/blog/post/1995.html.

@jeroenvisser101
Copy link
Author

Can we implement "cache invalidation" in this "platform solution"? Or at least make some decisions to be able to connect own "cache invalidation logic"?

@DreaMinder I think this is a very specific use case (but still one that we share, obviously). We've been thinking about this and something that we think we could do is building better build-caching so rebuilds are faster (e.g. only changed data has to be loaded).

We've also been experimenting with plugins that add additional metadata to the dist folder, GlobalID-like identifiers that specify what resources have been used on which routes, but this hasn't been stable yet. This metadata would then be used to determine which files to delete/update, and we'd only re-render those files.

But what if page changes

We have determined some resources change quite often (e.g. e-commerce store, changing stock wouldn't trigger a rebuild), but we still do queries once the component has been mounted, so we do still load the most up-to-date data for the user. Once the result comes back we merge the data with the preloaded data.

@jeroenvisser101
Copy link
Author

@pi0

const { fallback } = this.options.generate // Defaults to 404.html

This would break BC. We could adopt the following:

  • 200.html (default)
    set in common/options.json, this matches current functionality and would require no change when updating
  • true -> 404.html
    setting it to true would 'default' it to 404.html (this might be too confusing, but would work with Github pages)
  • any string value would override the path to the value set

@jeroenvisser101
Copy link
Author

@pi0 @Atinux I've drafted a PR (#2698) for this and included some tests as well. Let me know what you think!

@jeroenvisser101
Copy link
Author

Fixed in #2698

@Atinux
Copy link
Member

Atinux commented Jan 31, 2018

It's now live with v1.3.0!

@jeroenvisser101
Copy link
Author

Woooh! 🎉

@stursby
Copy link

stursby commented Jan 31, 2018

@jeroenvisser101 Would love to see a simple demo of this setup in examples if possible!

@jeroenvisser101
Copy link
Author

@stursby I like that, let me see if I can make time for that this week

@lock
Copy link

lock bot commented Nov 3, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Nov 3, 2018
@danielroe danielroe added the 2.x label Jan 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

6 participants