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).
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
:
index.html
through Firebase Hosting, the page will become interactive but no additional queries are performed on the client side./posts/1
is generated, but all the others are not and exists only in the databaseindex.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).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鈥攁side from the generated pages鈥攁 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!
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
.
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?
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
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.
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')
}
Right! I guess that would work really well, solving two issues instead of one:
200.html
is being generated always, even when not on surgeWhat's your idea @Atinux @alexchopin @clarkdo?
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?
@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.
@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 :)
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 :)
@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.
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?
@jeroenvisser101 exactly, the point here is to not show a 404 page to the user while your are generating the website behind.
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.
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
@DreaMinder Cache invalidation would be as easy as doing rm dist/blog/post/1995.html
.
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.
@pi0
const { fallback } = this.options.generate // Defaults to 404.html
This would break BC. We could adopt the following:
200.html
(default)common/options.json
, this matches current functionality and would require no change when updatingtrue
-> 404.html
404.html
(this might be too confusing, but would work with Github pages)@pi0 @Atinux I've drafted a PR (#2698) for this and included some tests as well. Let me know what you think!
Fixed in #2698
It's now live with v1.3.0!
Woooh! 馃帀
@jeroenvisser101 Would love to see a simple demo of this setup in examples if possible!
@stursby I like that, let me see if I can make time for that this week
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.
Most helpful comment
@stursby I like that, let me see if I can make time for that this week