framework icon indicating copy to clipboard operation
framework copied to clipboard

Extending prerender routes

Open pi0 opened this issue 3 years ago • 3 comments

In Nuxt 2, using nuxt generate, we could either manually provide routes to generate or depend on the (later added) crawler to find them.

For manual mode, it was possible to use generate.routes option to provide an async function prefetching them. While this functionality is needed in general, adding async logic to nuxt.config is kinda an anti pattern that we are trying to avoid in Nuxt 3 so there might be better ideas.

Ref: #4885

pi0 avatar May 10 '22 15:05 pi0

Could this be done with by #4894?

This PR add a hook nitro:init:before that is called right before Nitro initialisation, where you can edit Nuxt config (and thus routes).

TotomInc avatar May 11 '22 07:05 TotomInc

@TotomInc As a workaround, You can also use nitro:config hook to add custom (async) routes to prerenderer:

import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  hooks: {
    async 'nitro:config' (nitroConfig) {
      if (nitroConfig.dev) { return }
      // ..Async logic..
      nitroConfig.prerender.routes.push('/custom')
    }
  }
})

pi0 avatar May 11 '22 22:05 pi0

From the Nuxt3 config schema file

You can pass a function that returns a promise or a function that takes a callback. It should return an array of strings or objects with route and (optional) payload keys.

About returning objects with a route and payload keys: is this still possible in Nuxt3?

I tried doing this

nitroConfig.prerender.routes.push({route: '/custom'})

Which resulting in the route showing as ├─ [object Object] (undefinedms) (TypeError [ERR_INVALID_URL]: Invalid URL)

kasperjha avatar Sep 12 '22 12:09 kasperjha

@pi0 This seems to work well for fetching and generating the routes in general, and functions correctly when navigating client-side from within the app. However, it seems to fail when navigating to dynamic routes directly or when reloading the page when already on a dynamic route.

Here's an example with the following route structure using RC-11:

/pages/articles/index.vue
/pages/articles/[slug].vue

I'm fetch routes using your technique (in my case, from a headless CMS) and then testing the generated content from nuxt generate .

Navigating directly to https://mysite.test/articles or clicking a <NuxtLink> to that URL works fine. A list of articles is displayed without hitting the CMS API. Great!

Clicking a <NuxtLink> to the specific article https://mysite.test/articles/my-first-article also works fine. Also great! Clicking a <NuxtLink> back to /articles also works fine. No issues for any of this, and you can navigate back and forth without any issues.

However...

Navigating directly to the dynamic route, https://mysite.test/articles/my-first-article or pressing reload when you're already on that page fails with the following error:

TypeError: Cannot read properties of null (reading 'data')
    at setup (index.a2c04747.js:1:1438)

When this happens you can see the full rendered SSG content for a fraction of a second before the error happens, wiping the un-hydrated HTML out and leaving blank content within the layout.

Hard-coding the prerender route with

// nuxt.config.ts
export default defineNuxtConfig({
    nitro: {
        prerender: {
            routes: ['/articles/my-first-article']
        }
    }
})

does not help resolve the issue, and you get the same error when navigating directly or reloading.

Smef avatar Sep 26 '22 06:09 Smef

@Smef this is the issue I had originally when working on my Nuxt e-commerce website a few months ago.

However, there may have been improvements done thanks to the v3.0.0-rc.11 for the Full Static mode.

TotomInc avatar Sep 26 '22 12:09 TotomInc

@Smef I don't think that is related to this issue, exactly. Would you create a new issue with a reproduction and I'll look at it? :pray:

danielroe avatar Sep 26 '22 12:09 danielroe

I think I've done some further diagnosis and it looks like this is a side-effect of useFetch and useAsyncData hitting the CMS from the client instead of the payload on SSG sites when doing the reload on that page.

It seems that when a key option is used for useFetch or useAsync data it causes the fetch to run from the client and not during the SSG "generate."

This works fine:

const {data: article} = await useFetch(endpoint)

But this causes the payload to not be used when reloading the page:

const {data: article} = await useFetch(endpoint, {key: route.fullPath})

Please correct me if I'm wrong, but it's necessary to use the key option for a dynamic route like this, or you end up seeing whichever data/article you view first on all of the dynamic routes until you do a reload. Removing the key seems to make things work fine for SSG, but it breaks the dev environment and you see the wrong data when navigating between pages.

I've made a simple demo project at https://github.com/Smef/nuxt-ssg-issue-demo to demonstrate this issue. You can remove the useFetch key in /pages/articles/[slug].vue to see the change in behavior.

Smef avatar Sep 26 '22 19:09 Smef

I suspect the route.fullPath might have a trailing slash in your deployed environment which is causing this mismatch. Try instead using route.params.slug.

danielroe avatar Sep 26 '22 19:09 danielroe

Yep. That fixed it. I'm surprised to see that a trailing slash would be an issue like that, but I guess that was the problem.

Using just the param could still be an issue, though, because (theoretically) a slug could conflict with another useFetch default key at a different route, right? We really do need the full path in there, so 'articles/' + route.params.slug would be necessary, correct?

Smef avatar Sep 26 '22 19:09 Smef

Well, the trailing slash is not an issue unless you are using the url as a key. You can make it any unique key; it doesn't have to be url-like - so article-${route.params.slug} would work fine.

danielroe avatar Sep 26 '22 19:09 danielroe

Ah, yes, true. I wanted to make sure I understood the usage of the key. The URL is unique through the app, so I thought that was a good place to start for a unique key.

A trailing slash being an issue might be good to include in the documentation.

Thank you very much for your assistance!

Smef avatar Sep 26 '22 19:09 Smef

Hey guys is there any way to configure prerender routes with not only an array of strings but also passing a payload? Im thinking something like this:

nitroConfig.prerender.routes.push({route: '/custom', payload: {hello: 'world'})

carlosmori avatar Sep 29 '22 02:09 carlosmori

What would your payload be where you wouldn't be getting it in your route scripting directly? In SSG you can have only one set of data like that for a route, so wouldn't that be controlled in the route scripting itself?

In your example I'd expect you'd have the "hello" data in the route, or possibly in an .env if it's environment specific data.

Smef avatar Sep 29 '22 02:09 Smef

I wanted to associate the payload with the route so the page can have information about it.

1- I have a route /:id 2- in that page I fetch an api for that particular item id to show the information in the page. 3- If I want to pre build those pages I want the page to have a prop for that item, so I dont refetch it.

So I wanted to statically generate all the possible id pages and have a prop to fill them server side.

how would you statically generate with api information?

Thanks for the fast response Smef :)

carlosmori avatar Sep 29 '22 02:09 carlosmori

I think your ID would be the key for this, not a payload. Your file/route would be something like /products/[id].vue where ID is each product ID. The prerendered routes would generate a static page for each product. You wouldn't pass the product ID as a parameter if you wanted it to be prerendered.

Here's an example: nuxt.config.ts

In this file we're getting fake blog posts, but it would be the same thing for your real products. You'd use an async function to retrieve the list of product IDs so that you have your full array of routes, pass the array to the nitro hook, and then each of your products would have a static page generated.

Smef avatar Sep 29 '22 02:09 Smef

Oh gotcha, so I pre render the routes and then in the :id page with the method useAsyncData I can do stuff server side. I didnt know I could use the router object inside useAsyncData.

Nice example!

Got it to work.

Thanks Smef!!

carlosmori avatar Sep 29 '22 03:09 carlosmori

Hi all 👋,

I've been reading at this thread with interest. We are looking to fetch from Contentful (GraphQL) and dynamically generate our static routes and their json payloads based on the response.

I'm struggling to see a clear, definitive approach for doing this with Nuxt3, can anybody point me in a useful direction?

many thanks

Rich

richhiggins avatar Oct 05 '22 10:10 richhiggins

@richhiggins For what you need, this is the solution, for now at least.

danielroe avatar Oct 05 '22 11:10 danielroe

@richhiggins For what you need, this is the solution, for now at least.

Thanks! I'd seen that, and thought it looked like the best bet 👍

richhiggins avatar Oct 05 '22 12:10 richhiggins

@richhiggins For what you need, this is the solution, for now at least.

Sorry for my ignorance, but how do you pass a payload with this solution? Currently i am saving the JSON payload to a file which works, but is not ideal.
Can you somehow give a payload like with the route object in Nuxt2?

Ref previous my previous comment

kasperjha avatar Oct 05 '22 13:10 kasperjha

I agree with @kakka0903. I would like to prerender static pages from a CMS. The idea would be to pass the CMS data as payload to the page (like in Nuxt 2).

I understand that I can do the data fetching for each page individually. Why is this not ideal in my case?

  • Fetching data from from the CMS requires an ID rather than the path. However, I cannot pass this ID down to the page.
  • Data fetching is done with GraphQL using a library. The data is fetched inside of "useAsyncData" on server side and Nuxt stores the response in payload.js. So I don't need to fetch any data from my CMS on the client side. However, all the code to fetch the data (including the GraphQL library) is included in the bundle.

Can you please let me know if I missed a feature of Nuxt which would allow me to do this?

dwin0 avatar Oct 06 '22 09:10 dwin0

I don't think you have the right approach to this problem for a static site. If your content is going to be pre-rendered it can't come from query parameters, as that won't work for SEO and also requires a server to process and serve the query data.

Your articles can be at slug URLs, like: https://mycompany.com/articles/article-1 https://mycompany.com/articles/my-second-article https://mycompany.com/articles/another-article

or IDs: https://mycompany.com/articles/1 https://mycompany.com/articles/2 https://mycompany.com/articles/3

but not query parameters: https://mycompany.com/articles?slug=article-1 https://mycompany.com/articles?slug=my-second-article https://mycompany.com/articles?slug=another-article https://mycompany.com/articles?id=1 https://mycompany.com/articles?id=2 https://mycompany.com/articles?id=3

The routes should be generated from a path, which can include the IDs or be slugs that look up the IDs to get the actual data. This strategy works fine for SSG using the techniques in this thread. If you were using a server instead of SSG these are the way you'd process your URLs, so you just do the same thing and then SSG stores the final rendered content. You don't have to do special logic for SSG vs SSR in this case.

I made an example of this at the repo https://github.com/Smef/nuxt-ssg-issue-demo if you'd like to take a look. We're doing this internally using Statamic as a headless CMS with Nuxt 3 SSG to make the blog posts and it works fine.

Smef avatar Oct 06 '22 15:10 Smef

@Smef the scenario we have is that a slug cannot be reliably used to fetch CMS data - page slugs in this instance aren't unique 🙄. We could include an ID in the path as well as slug, and use that for the query, that would work.

However it would be desirable to pass this ID to the page component without using the url, keeping the url a little cleaner.

With Gatsby for example you could pass this data in as React context and use it in the page query. It also looks like Nuxt2 supported something like this.

richhiggins avatar Oct 07 '22 08:10 richhiggins

I'm not understanding how you would use a page query parameter effectively with SSG pre-rendered content. A query needs to be processed by a server, but with SSG you don't have a server to do processing (at least not for the static content). You couldn't ever direct the user to a url like https://mypage.com/articles?id=1 and get static, prerendered content for each query parameter.

If you're passing the data directly in rendering, similar to a POST or something, how would your users access a page generated like this? SSG content is retrieved through GET and there isn't a server to receive POST data.

You can still use query parameters on an SSG page for a url like https://mypage.com/articles?id=1, but you then have to have the browser process the query paramters and fetch the data through an AJAX request. If this is your main content you've lost a lot of the benefits of SSG by doing this, as you're back to client-side rendering at that point.

Smef avatar Oct 07 '22 14:10 Smef

I think there's a misunderstanding. We are not talking about query parameters. The feature we are referring to is this one: https://nuxtjs.org/docs/configuration-glossary/configuration-generate/#speeding-up-dynamic-route-generation-with-payload

nuxt.config.js:

export default {
  generate: {
    routes() {
      // 1) fetch data from CMS
      // 2) pass page data directly to each of the pages

      return [
        { route: '/', payload: { title: 'Home', headerImageUrl: '...' } }
        { route: '/about-us', payload: { title: 'About us', headerImageUrl: '...' } }
      ]
    }
  }
}

dwin0 avatar Oct 07 '22 16:10 dwin0

So... is the ability to pass payload data alongside a route on the Nuxt3 roadmap?

We have ended up with a workaround - writing some additional data locally and then reading from that in the component during build step. Doesn't feel ideal but it's working for our needs, for now.

richhiggins avatar Oct 18 '22 15:10 richhiggins