vikejs / vike

🔨 Flexible, lean, community-driven, dependable, fast Vite-based frontend framework.
https://vike.dev
MIT License
4.3k stars 348 forks source link

Generate and route pages based on API data #123

Closed renestalder closed 3 years ago

renestalder commented 3 years ago

I have a headless CMS that basically allows the user to include components and content with a visual editor. The CMS is Storyblok.

In production, the website should be statically generated, yet all information about routes and pages comes from the CMS which means, the information about which pages to actually generate to HTML is available at build time and dynamic.

In Nuxt, the generate property in the config can be used to return an object containing the string of the route and the corresponding payload. This allows to prerender all pages with the given payload, but also load the content of a single page client-side to a later point, e.g. in the mounted hook, to enable live editing. And thanks to the _.vue in the root of the pages folder, it allows me to use one page template without the need to create a vue file for every page in the vue folder, allowing the user to dynamically add new pages.

In my understanding, in vue-plugin-ssr, I might achieve the same with prerender() hook. But what is the equivalent of the _.vue file of Nuxt? Probably one of the _default files, but I can't make the connection yet in my head.

brillout commented 3 years ago

Yes, _default.page.client.js defines the browser-side code of your pages, while _default.page.server.js defines the render() hook which renders your pages to HTML.

If I'm understanding your use case correctly, it should be possible by setting pageContext.Page in your prerender() hook.

export async function prerender() {
  const content = await getContentFromYourCms();

  return content.map(({url, component}) => ({
    url,
    pageContext: {
      // We set the root component `Page` here
      Page: component,
    },
  }))
}
renestalder commented 3 years ago

@brillout Thanks for taking the time for answering my questions.

So, to get that right before I dig to deep, when I only have the _default.page.*.js files, no other files in the pages/ folder, every page that is generated will then always fall back to the default page, with the specific pageContext given in the prerender() hook, right?

And if everything comes together, I can still hydrate data given during pre-rendering on the client-side, the same way it is already demonstrated in the boilerplate examples? This situation only applies when the page is loaded inside the CMS editor iFrame, which I'm going to check based on the URL query or cookies.

brillout commented 3 years ago

Yes, although the plugin will complain that you didn't define any page.

What you can do:

// pages/index.page.js

export const Page = "This is a dummy file that will actually never be used";
// pages/index.page.server.js

export async function prerender() {
  // ...
}

I believe it's optional for prod, but for dev and/or for clarity you can also define:

// pages/index.page.route.js

export '/:params*'

Yes the browser-side is the same. Although, if you want, you can also have some pages have a different browser-side code.

renestalder commented 3 years ago

Awesome. Thank you again for taking the time to help me here. I really appreciate it. I think I got everything to tinker around with what I try to achieve.

brillout commented 3 years ago

Sure and let me know if you get stuck with something.

renestalder commented 3 years ago

I could make everything work after moving the prerender hook to the index.page.server.js, as suggested by you.

One thing I noticed in the manual is that it demonstrates the assignment of pageContext without using pageProps.

https://vite-plugin-ssr.com/prerender

How it is:

// /pages/movie.page.server.js
// Environment: Node.js

export { prerender }

async function prerender() {
  const movies = await Movie.findAll()

  const moviePages = (
    movies
    .map(movie => {
      const url = `/movie/${movie.id}`
      const pageContext = { movie }
      return {
        url,
        // Beacuse we already provide the `pageContext`, vite-plugin-ssr will *not* call
        // the `addPageContext()` hook for `url`.
        pageContext
      }
      /* We could also only return `url` and not provide `pageContext`. In that case
         vite-plugin-ssr would call the `addPageContext()` hook. But that would be wasteful
         since we already have all the data of all movies from our `await Movie.findAll()` call.
         return { url } */
    })
  )

  // We can also return URLs that don't match the page's route.
  // That way we can provide the `pageContext` of other pages.
  // Here we provide the `pageContext` of the `/movies` page since
  // we already have the data.
  const movieListPage = {
    url: '/movies', // Note how the URL `/movies` isn't part of the page's route `/movie/:movieId`
    pageContext: {
      movieList: movies.map(({id, title}) => ({id, title})
    }
  }

  return [movieListPage, ...moviePages]
}

How I applied it, by wrapping my data into pageProps to retrieve the specific dataset with setup(props):

// /pages/movie.page.server.js
// Environment: Node.js

export { prerender }

async function prerender() {
  const movies = await Movie.findAll()

  const moviePages = (
    movies
    .map(movie => {
      const url = `/movie/${movie.id}`
      const pageContext = { pageProps: { movie } }
      return {
        url,
        // Beacuse we already provide the `pageContext`, vite-plugin-ssr will *not* call
        // the `addPageContext()` hook for `url`.
        pageContext
      }
      /* We could also only return `url` and not provide `pageContext`. In that case
         vite-plugin-ssr would call the `addPageContext()` hook. But that would be wasteful
         since we already have all the data of all movies from our `await Movie.findAll()` call.
         return { url } */
    })
  )

  // We can also return URLs that don't match the page's route.
  // That way we can provide the `pageContext` of other pages.
  // Here we provide the `pageContext` of the `/movies` page since
  // we already have the data.
  const movieListPage = {
    url: '/movies', // Note how the URL `/movies` isn't part of the page's route `/movie/:movieId`
    pageContext: {
      pageProps: { movieList: movies.map(({id, title}) => ({id, title}) }
    }
  }

  return [movieListPage, ...moviePages]
}

I'm not sure what the direct assignment of the data to the pageContext does and how this data then can be used in the index.page.vue. I explicitly had to assign it to pageProps to make it work for me.

I thought I report that back. Maybe I don't understand the mechanics behind it or it might be a mistake in the documentation example.

brillout commented 3 years ago

You're right it's a mistake in the docs. It's now fixed.

I'm glad it worked out. Let me know if there is anything else.