docs
  1. SCAYLE Resource Center
  2. Developer Guide
  3. Features
  4. Pages

Pages

Page Rendering

Here we'll consider how pages in the Storefront Boilerplate are built and how the behavior differs when the page is rendered on the server vs. client.

The following is a basic example of a product detail page:

<template>
  <span v-if="fetching">Loading ...</span>
  <pre v-else>{{ data }}</pre>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { onFetchAsync, useProduct } from '@scayle/storefront-nuxt2'

export default defineComponent({
  setup() {
    const { id } = useCurrentRoute();
    const { data, fetching, fetch } = useProduct();

    onFetchAsync(async () => {
      await fetch({
        id: parseInt(id, 10)
      });
    });

    return {
      data,
      fetching
    };
  }
})
</script>

Nuxt automatically registers all the *.vue files in the pages folder in the router. Parts that start with an underscore are parameters that will be parsed and can be used inside our page component. That means opening all these paths will be resolved to the page we just created (e.g., products/very-beautiful/123 and products/123/456).

Templating

Considering the first part of the example above:

<template>
  <span v-if="fetching">Loading ...</span>
  <pre v-else>{{ data }}</pre>
</template>

This checks if we are currently loading data (v-if="fetching") and, if that is the case, shows a loading indicator. Otherwise, the fetched product information is output as JSON.

The loading state and product data are provided by the useProduct composable:

export default defineComponent({
  setup() {
    const { id } = useCurrentRoute()
    const { data, fetching, fetch } = useProduct()

    // [...]

    return {
      data,
      fetching
    }
  }
})

The setup method to define the logic that gets executed once the component is mounted in the application.

const { id } = useCurrentRoute() lets us use the named path parameter id in our page (since our path is products/_slug/_id.vue we could also access the slug, but we don’t need it for this example.)

Next, we are initializing the useProduct composable with const { data, fetching, fetch } = useProduct(). This provides us with variables:

  • data will contain the data returned from fetching the product.
  • fetching is a boolean showing us the loading state (i.e., is product information currently being fetched?)
  • fetch is used for actually fetching the information.

Most of the provided composables are used for fetching data (products, categories, baskets, wishlists, etc.) and return similar information.

Finally, we forward the data and fetching state to the Vue template by returning these. This allows us to use them inside our template.

The final step is fetching the data:

onFetchAsync(async () => {
  await fetch({
    id: parseInt(id, 10)
  })
})

Here, we are using the onFetchAsync hook. If the current page is rendered on the server it will halt the rendering of the page until the method passed to onFetchAsync is finished running and then continue rendering. Therefore, the server-rendered pages never show a loading screen, but only display completely loaded information. (This is important for SEO and also provides a better user experience.)

If the page is rendered on the client (this happens when the user follows a link in our application to this page) it will trigger the provided method when the component is mounted. Because of the implementation of the useProduct composable, this will show the loading state and change once the data is fetched.

Data Loading

If you need to fetch a lot of data to render a page (i.e., category information, product information and then also want to enrich this data with information fetched from other sources) you can use the onFetchAsync hook:

onFetchAsync(async () => {
  await fetchA()
  await fetchB()
  await fetchC()
})

This will serially load the data (when fetchDataA has finished the call fetchDataB and once this has finished the call fetchDataC). This can be a desired use-case, for example, if you need some information from a previous fetch call and have to provide this to another call. The downside is that it can take a long time until all data is loaded and the user sees the page.

If the calls can be made independently, it is better to do them in parallel:

onFetchAsync(async () => {
  await Promise.all([
    async () => fetchA(),
    async () => fetchB(),
    async () => fetchC()
  ])
})

This executes all calls in parallel but still waits for all calls to finish.

Of course, you can also mix and match these approaches:

onFetchAsync(async () => {
  await Promise.all([
    async () => {
      await fetchA()
      await fetchB()
    },
    async => fetchC()
  ])
})

Shared state between composables

Notice that we pass key as a second argument to useRpc. Internally useRpc uses sharedRef to store state inside our application. You can think of it as a global registry inside your application and we have to provide the key under which to store the data. This has some side effects that you have to keep in mind. Here we use our example useNews composable defined above:

const newsA = useNews()
const newsB = useNews()

await newsA.fetch()

console.log("NewsA data", newsA.value) // will output data returned by the getNews RPC method
console.log("NewsB data", newsB.value) // will output the same data as newsA.value
console.log(newsA.value == newsB.value) // => true

This is because both composable calls use the same key when initializing. This allows us to use the useNews composable in several areas of our application and we only have to fetch the data once (usually this can be handled in the default layout) and the returned data can be used by all the other components without having to pass the data to every component as props.

However, if you don’t want to share the data, you can initialize useNews with different keys:

const newsA = useNews('A')
const newsB = useNews('B')

await newsA.fetch()

console.log("NewsA data", newsA.value) // will output data returned by the getNews RPC method
console.log("NewsB data", newsB.value) // will be null
console.log(newsA.value == newsB.value) // => false

Routing

The navigation behavior of the user is managed via useRouterHistory:

const { from, to, rouerHistory } = useRouterHistory()

console.log("Current location", to.value)
console.log("Previous location (if exists)", from.value)

This can be used for generating a backlink from the product detail page back to the listing page. A product can be listed in several categories (i.e. /women, /women/dresses) and depending on from which page the user came, the link for navigating back should link to the correct page.

Redirects

The SCAYLE Panel allows you to create, update, and delete redirects for storefront applications. This can be done in bulk or as a single-entity operation. Functionality allows for two inputs: source and target URLs. Users can specify a redirect from a source to a target URL. For example, a redirect could be added from /de/old-shop-poster-1 to /de/new-better-poster-1.

Configuration

To activate the redirects feature, set the REDIRECTS_ENABLED flag to 'true' in your local .env file.

REDIRECTS_ENABLED='true'

Redirects Specification

  • The redirect feature works only for external source navigation or direct URL inputs to the browser navigation tab. It won't trigger internal application navigation click events (menus, buttons, links, etc.).
  • When external navigation occurs, Storefront Core middleware will intercept the call and send another call to the Storefront API /redirects route.
  • The API call checks if there is a redirect URL (targetURL) for the URL (sourceUrl) used for navigation. If the URL matching is exact, the Storefront API stores the complete URL as a targetURL.
  • If there is a match, the redirectEntity object with targetURL is returned and the navigation is server-side redirected to the targetURL, otherwise, navigation is proceeded to the original sourceUrl.
  • Every Storefront API call to the /redirects route is stored in Storefront Core Redis implementation for 1 minute (TTL is 1 minute). This storage is done to reduce the possible load on the API route and to enhance redirect performance. This storage means that with every redirect update in the SCAYLE Panel, there is a small delay (1-2 minutes) when the new redirect is updated and used for navigation.

For information about configuring redirects in the SCAYLE Panel, see Redirects.

State sharing

sharedRef is used by every composable inside the Headless Storefront. It shares the state based on a key globally serializes the data stored in it and sends it to the client (you can look at this by entering __NUXT__.ssrRefs in the browser console). This is what Nuxt rehydrates from when initializing the client page.

Example usage (actual implementation of the useRpc composable:

export const useRpc = <
  T extends (...args: any[]) => any,
  P = Parameters<T>[0],
  TResult = PromiseReturnType<T>
>(
  method: T,
  key: string
) => {
  const fetching = sharedRef(false, `${key}-fetching`)
  const data = sharedRef<TResult | null>(null, `${key}-data`)
  const wrappedCall = rpcCall(method)

  const fetch = async (params: Partial<P> = {}) => {
    fetching.value = true
    try {
      data.value = (await wrappedCall(params)) as any
    } finally {
      fetching.value = false
    }
  }

  return {
    fetching,
    data,
    fetch,
  }
}