freddy38510 / quasar-app-extension-ssg

Static Site Generator App Extension for Quasar.
MIT License
152 stars 16 forks source link

[Question] Is it possible to build SPA with only specified routes as static webpages? #86

Closed yeus closed 2 years ago

yeus commented 2 years ago

is it possible to tell the plugin to only generate static code for one specific route or component?

I have a product webpage which updates its product-data only on rare occasions and I would like to generate a static side by pulling the product information from a database and then pre-rendering it duing build stage. but I would only like to render the specific product webpages (only 3) into static code. Is this possible?

Thx for the great addon otherwise! I hope it gets included into quasar main build at some point...

freddy38510 commented 2 years ago

You can exclude some routes to be pre-prendered with the exclude option. The routes will be available from the SPA fallback on client-side.

But what you're asking doesn't seem to be about excluding routes. If I understand this correctly, you want your products data to be pre-rendered but not loaded (again) at client-side. Is it right ?

yeus commented 2 years ago

You can exclude some routes to be pre-prendered with the exclude option. The routes will be available from the SPA fallback on client-side.

But what you're asking doesn't seem to be about excluding routes. If I understand this correctly, you want your products data to be pre-rendered but not loaded (again) at client-side. Is it right ?

Yes, that is correct, I edited the original question accordingly.

I would like to pull the product information during build stage, pre-render a specific page/component (component/page would be the nicest option, but if it has to be route, thats also acceptable) and publish it as a static version online.

It seems like the exclude as a blacklist might work. Although having a "whitelist" option would be more practical in this specific scenario. Does the routes option override the crawl feature? In that case the route option could be used as a whitelist... ?

lostnet commented 2 years ago

If you set crawl to false, specifying only the 3 routes should leave all other routes as purely dynamic.

Normally a page with a static pre-render is still re-rendered dynamically to integrate correctly with SPA behavior as the user interacts, but you could of course pull out pieces from the static pre-render pages or remove normal SPA integration, i.e. by writing your own method for onRouteRendered.

freddy38510 commented 2 years ago

The routes option is independant from the crawl feature, but you can disable the crawler by setting the option crawl to false.

I think that some Regexp in the exclude option can do what you need without adding a whitelist feature. The routes option is helpful for unlinked pages.

If you want to only pre-render a specific component but not the entire page, you can use the Quasar q-no-ssr component. You would need to wrap in this Quasar component all the components/content that you don't want to be pre-render.

For the product information data you can use the Quasar prefetch feature.

You can also use the Quasar beforeBuild hook to fetch your product data then store them in json files. This way you can just import the json files in your pages. But you would need to re-build your app everytime the data changes.

freddy38510 commented 2 years ago

If you set crawl to false, specifying only the 3 routes should leave all other routes as purely dynamic.

@lostnet That is not entirely true. In fact, the extension is getting all routes from the Vue Router configuration no matter how the other options are set. Then the crawler helps to find dynamic routes. The use-case of @yeus "forces" him to use the exclude option.

The second paragraph is totally correct :)

The routes are pre-render at server-side, then the resulting html string is writed to corresponding files. Finally at client-side Vue takes over the static HTML sent by the server and turns it into dynamic DOM that can react to client-side data changes. This is what is called "client side hydration".

lostnet commented 2 years ago

Thanks @freddy38510 that clarifies quite a bit. I looked into why the exclude doesn't seem necessary for me and it is because the Router configuration fails so AppRoutes is being replaced with ['/']. I modified initRoutes to print the error:

 ================== GENERATE ==================

 App • Cleaned build artifact: "/home/me/WebstormProjects/quasargr/dist/ssg"
 Extension(ssg) • Copying assets...
[  Error: Cannot find module 'pages/en/about-us.vue'
  Require stack:
  - /home/me/WebstormProjects/quasargr/src/router/routes.js

  - loader.js:902 Function.Module._resolveFilename
    internal/modules/cjs/loader.js:902:15

  - helpers.js:99 Function.resolve
    internal/modules/cjs/helpers.js:99:19

  - jiti.js:1 p
    [quasargr]/[jiti]/dist/jiti.js:1:52925

  - jiti.js:1 g
    [quasargr]/[jiti]/dist/jiti.js:1:54200

  - routes.js:1
    /home/me/WebstormProjects/quasargr/src/router/routes.js:1:201

  - jiti.js:1 g
    [quasargr]/[jiti]/dist/jiti.js:1:55111

  - index.js:4
    /home/me/WebstormProjects/quasargr/src/router/index.js:4:38

  - jiti.js:1 g
    [quasargr]/[jiti]/dist/jiti.js:1:55111

  - generator.js:110 Generator.getAppRoutes
    [quasargr]/[quasar-app-extension-ssg]/src/generate/generator.js:110:39

  - generator.js:88 Generator.initRoutes
    [quasargr]/[quasar-app-extension-ssg]/src/generate/generator.js:88:41

] {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/home/me/WebstormProjects/quasargr/src/router/routes.js' ]
}

src/router/routes.js:

import EnAboutUs from "pages/en/about-us.vue";
...
yeus commented 2 years ago

I think that some Regexp in the exclude option can do what you need without adding a whitelist feature. The routes option is helpful for unlinked pages.

Thats right it shouldn't be to difficult to find such a regex.

If you want to only pre-render a specific component but not the entire page, you can use the Quasar q-no-ssr component. You would need to wrap in this Quasar component all the components/content that you don't want to be pre-render.

For the product information data you can use the Quasar prefetch feature.

You can also use the Quasar beforeBuild hook to fetch your product data then store them in json files. This way you can just import the json files in your pages. But you would need to re-build your app everytime the data changes.

I am just realizing that I had a slightly wrong understanding of the ssg generator. I think I understand now that, an SFC component that I populate using some json data that I fetched from a server, that fetch-operation will get called again client-side through client-side-hydration no matter what (if I download the data in preFetch for example). So, while ssg does correctly render my product page, (if I check the corresponding index.html in the folder) it doesn't prevent my app from calling the API which was the entire goal of this exercise ;). If I explicitly tell vue to not download on client side (for example by introducing an if(process.env.STATIC):

prodData = undefined

if(process.env.STATIC) {api.get(...).then((reponse) => prodData = response.data)}

the prodData variable stays empty on client side and as it is "undefined", the corresponding page, which was correctly rendered using ssg, gets dynamically replaced by a page with no data which totally defeats the purpose.

Basically thats how the client-side hydration works...

So my questions here:

--> it looks like the only way to do this would be to use the way suggested by @freddy38510: using the beforeBuild hook and then import the json. --> is there any other way? For example by turning off client-side hydration? I found this in nuxt for example: https://nuxtjs.org/docs/configuration-glossary/configuration-render/#injectscripts. IMHO it would be more idiomatic to have this kind of behavior (preFetching data in a component and then prevent the rendered component from being modified client-side) in the component itself rather than the beforeBuild hook

I wonder if I should make a feature-request for quasar to turn off client-side hydration.

freddy38510 commented 2 years ago

@yeus,

You can disable client-side hydration but your components will not be interactive anymore.

There is also this wonderful library called vue-lazy-hydration which is very useful but I have not test it on Vue3. You might read these articles writed by the same author which help a lot to have a better comprehension of hydration.

@lostnet Good catch!

I will push some commits to print warnings for this scenario. Shouldn't this error throws during webpack compilation ?

lostnet commented 2 years ago

@freddy38510 AFAICT all the other build processes are setting up quasar client or server contexts from the quasar configuration and getting access to things like process.env.VUE_ROUTER_MODE and these webpack aliases while the route analysis is happening directly in the generator's own context which only seems to have basic node variables in its process.env, etc.

As a consequence when I tried setting up a new empty project the getAppRoutes also hits the catch() but because it was trying to use hash mode which ends up needing location (It uses hashmode incorrectly since it can't find process.env.VUE_ROUTER_MODE or process.env.SERVER) in the generator environment.

freddy38510 commented 2 years ago

@lostnet,

Thank you a lot for having found this issue and made a PR for the crawler option.

Should be fixed in the latest release if you want to give it a try.

freddy38510 commented 2 years ago

Actually I missed the webpack aliases. I was dynamically importing my components so I was not getting any errors with this commit.

At least you can see the warnings now. I will work on it tomorrow.

lostnet commented 2 years ago

Thanks a lot @freddy38510, I will try out the improved error/warnings tomorrow. The ['/'] with crawler has actually been working fine for my project and I could remove the routes configuration back when I upgraded to 2.x, but how it was picking up the routes was a bit mysterious.

lostnet commented 2 years ago

@freddy38510 The process.env fixes fixed the blank (cli-created) project and the warnings look good for the webpack aliases issue.

yeus commented 2 years ago

@freddy38510. Thx for the suggestion for LazyHydration. The library itself doesn't seem to work with Vue3 yet, but I found a nice blog post which achieves the same thing: https://blog.logrocket.com/vue-3-lazy-hydration-from-scratch/

freddy38510 commented 2 years ago

@lostnet,

I didn't found a better way than compiling the router with webpack https://github.com/freddy38510/quasar-app-extension-ssg/commit/d0d94e12079cf1753ca61b2d666edbc2852563a7. The trick here is to replace any imported Vue component by a fake component to avoid having a big compiled file including all Vue components.

And of course, the webpack configuration used is derived from the server-side one. So we have all process.env and aliases needed.

lostnet commented 2 years ago

@freddy38510 I tried it out on my test project and it works well! On my real project I am actually setting the paths in the data of the vue components, i.e. [Component].data().path so it notices very quickly that it is fake. I think my method is not the normal way to do it, but I could not find any examples that were DRY where every page needed to know their own route.

freddy38510 commented 2 years ago

I'm a little bit confused. Don't you set your app routes from the src/router/routes.js file ?

Why your pages need to know their own route ?

lostnet commented 2 years ago

@freddy38510 I'd originally encapsulated everything to go along with the data for the Meta plugin within a page back on saber (which had no explicit router config?) I think I had some concerns with how large the router config itself was when I migrated to the early quasar-ssg, but it looks like I am now passing Meta's data in the confusingly similar $route.meta in to get it to the parent/Layout component. So I think it currently would just reduce the size if I moved all the data to the routes.js in my current setup.

There's not really any technical reason the path needed to go with Meta's data, but my odds are pretty high of breaking something if I'm not looking at canonical links and paths at the same time..