npm package: @scayle/storefront-nuxt2
Description: Nuxt2 integration for the SCAYLE Commerce Engine and Storefront API
Author: SCAYLE Commerce Engine
Latest version: 7.39.28

# Storefront Core Nuxt2

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]

This is the basis for setting up a new Storefront based on Nuxt 2.
It provides a set of useful configurations and reusable composables, that can be used in the actual shop project.

## Getting started

To start working with storefront-core, make sure to register it as a module in your nuxt.config.ts file:

```typescript
import { NuxtConfig } from '@nuxt/types'

const config: NuxtConfig & { storefront: ModuleOptions } = {
  // ...

  modules: ['@scayle/storefront-nuxt2/dist/module/register'],

  storefront: {
    stores: [
      {
        domain: 'www.aboutyou.de',
        basePath: '/',
        bapi: {
          host: 'https://api-cloud.aboutyou.de/v1/',
          shopId: 139,
        },
        redis: {
          host: process.env.REDIS_HOST || 'localhost',
          port: process.env.REDIS_PORT
            ? parseInt(process.env.REDIS_PORT, 10)
            : 6379,
        },
      },
    ],
    imageBaseUrl: 'https://cdn.aboutstatic.com/',
  },
}

export default config
```

You can see all the possible configuration options [here](./src/types/moduleOptions.ts). Basically you
are able to configure the BAPI backend, from which URL images should be served, caching providers and
so forth.

## Architecture

The storefront-core comprises a multitude of functionality. Generally speaking, the most important are:

- API middleware + RPC methods
- Composable functions like `useWishlist`
- Helper functions for managing shop data like attributes
- Adaption of nuxt/image with custom provider to load images from About You CDNs

### How do the RPC methods work?

The RPC method calls use a hybrid approach. When you are on the server, they directly call the corresponding function. When you are calling the RPC method on the client, the call is serialized and sent to the API endpoint, which calls the function and then returns the return values.

Example:

```typescript
import { rpcCall } from './rpcCall'
import { rpcMethods } from './index'

const returnedValue = await rpcCall(rpcMethods.getProductById, { id: 12 })
```

The benefit of this approach is, that we can use the same interface, regardless if the function is currently called in a SSR context on the server or client-side in the Vue application. If you are interested in how this is handled, take a look at the [implementation](./src/rpc/rpcCall.ts)

## List of composables

### Core: [useRpc](./src/composables/core/useRpc.ts)

One of the most used composable, although you will probably not use this composable directly. It provides a useful wrapper around the `rpcCall` method mentioned above, adding loading states that can easily be used from within your components.

Example:

```typescript
import { useRpc } from '@scayle/storefront-nuxt2'
import { rpcMethods } from './index'

const { fetching, data, fetch } = useRpc(rpcMethods.getProductById)

await fetch({ id: 12 })
```

### [useWishlist](./src/composables/wishlist/useWishlist.ts)

Methods for managing the wishlist - adding and removing items, etc.

```typescript
import { useWishlist } from '@scayle/storefront-nuxt2'

const { addItem, removeItem, clearWishlist, isInWishlist, fetch } =
  useWishlist()

await fetch()

await addItem({ variantId: 123 })
```

## Logging

### How to set up

There are two places where we can set up a log.

1. Injected into the nuxt context
2. Provided in the `req`(request)-object for middleware support

IMPORTANT: You can very simply adapt the implementation from demo-shop. To do so, please have a look at the /modules/log folder.

#### Log for nuxt context

Create a new nuxt plugin and run `Log.attachToNuxtContext`.

```typescript
declare module '@nuxt/types' {
  interface Context {
    $log: Log
  }
}

export default defineNuxtPlugin((context, inject) => {
  Log.attachToNuxtContext(inject, new Log({
      space: 'exampleProject',
      handler: (entry: LogEntry) => { ... },
  }))
}
```

IMPORTANT: Make sure this plugin is initialized on client-side AND on server-side.

#### Log for server middleware (no nuxt context)

Create a new server-middleware that runs before core.

```typescript
export default (req: any, res: any, next: any) => {
  Log.attachToRequest(
    req,
    new Log({
      space: 'exampleProject',
      handler: (entry: LogEntry) => { ... },
    })
  )
  next()
}
```

### How to make sure it works

If the setup did not work, your log messages will have the default `space: "global"`.
So your terminal will look like this:

```
DEBUG [global.checkout.logout] Destroy session and redirect user
```

If the setup was correct, you should see something like this:

```
DEBUG [exampleProject.checkout.logout] Destroy session and redirect user
```

### Features

#### Spaces

To better determine, where the log messages originated from, you can pass a space to the log, as well as create sub-spaces very easily.

```typescript
const log = new Log({ space: 'myApp' })
const subLog = log.space('login')

subLog.error('User not found.') // ERROR [myApp.login] User not found.

// or simply do:
log.space('login').error('User not found.') // ERROR [myApp.login] User not found.
```

#### Time (Track execution time)

You can track execution time by using the `.time(...)` method from `Log`.
Here is an example how to use it:

```typescript
return await log.time(`RPC [${methodName}]`, async () => {
  return (await axios.post(url, params, { headers })).data as TResult
})
```

Console output will look like this:

```
[demo.sfc.rpc] RPC [getFilters] took 6ms
[demo.sfc.rpc] RPC [getProductsByCategory] took 10ms
```

## Caching

Storefront Core supports two modes of Caching. In general all cached entries are namespaced by shopId.

### SSR Cache

The whole rendered HTML of the server is stored inside redis and on each request we check if we have a cache HIT. If an entry is found the HTML is returned and no page has to be rendered.

### Wrapped Cache

Another possibility is to cache downstream API calls, for example for requesting product information. This is heavily used in all RPC methods. They way this works is that the expensive function is simply wrapped by the `cached()` method, like this:

```typescript
return await cached(bapiClient.products.getById)(options.id, {
  with: options.with,
  campaignKey,
  pricePromotionKey: options.pricePromotionKey,
})
```

The wrapping method takes care of calculating a cache key (based on the name of the function wrapped and the passed arguments). If no cache is found the underlying expensive function gets called and the returned value will be cached. If an entry is found, this will be returned instead omitting the expensive function call.

### Disable caching of specific pages

There are multiple ways to control the caching feature. First of all, the cache is only used when it is enabled via config:

```typescript
// inside nuxt.config.ts
  storefront: {
    // [...]
    cache: {
      enabled: true,
      ttl: 60 * 60,
      paths: [],
      pathsDisabled: [],
    },
    // [...]
```

The properties `paths` and `pathsDisabled` allow you to select in a white- and blacklist matter which pages to allow or disallow. If no paths are specified, all SSR requests will be cached.

_Warning_: Make sure that no user-related data is rendered ON the server! Make sure that either a) this is initialized on the client (like the wishlist/basket/user information in the navigation) or b) these pages are forbidden to get cached (like the account area)! Otherwise you _will_ leak user data to other people.

### Tagging of Cache

The cache allows you to associate tags to rendered pages. This way it is very easy to invalidate all rendered pages that contain an updated product, for example.

The best place for adding tags to the cache is inside the `onFetchAsync` method. Here is an example:

```typescript
const { data, fetch } = useProduct()
const { $cache } = useContext()

onFetchAsync(async () => {
  await fetchProduct(id)
  $cache.addTags([`product:${id}`])
})
```

Internally this works by creating a SET entry per tag, including all the cached keys that contain this tag. Example: Keys 'abc', 'def' and 'xyz' are stored but only 'abc' and 'def' are tagged with 'product:1001'. Executing SMEMBERS 'tag:product:1001' will produce `['abc', 'def']`.

### Invalidation of Cache

The cache module automatically registers a middleware (`<host>/_cache/`) with endpoints that you can trigger externally. This allows you to register webhooks or other apps to issue POST requests.

#### Purge entire cache

POST `<host>/_cache/purge/all`

Purge the entire cache, including all cached downstream API requests (BAPI, etc.)

#### Purge a specific tag

POST `<host>/_cache/purge/tags`
Body: ["product:1001"]

Purge all cache entries that are tagged with `product:1001`.

### Cache-Control headers

The cache module allows to set Cache-Control headers, so that CDN's like Cloudflare can cache the page on the edge. The only supported mode is with smax-age and stale-while-revalidate. If no maxAge and staleWhileRevalidate is configured, no cache-control header will be sent:

```typescript
// nuxt.config.ts
storefront: {
  // [...]
  cache: {
    enabled: true,
    ttl: 60 * 60,
    sendCacheControlHeaders: true,
    maxAge: 60 * 60,
    staleWhileRevalidate: 60 * 60 * 24,
  }
  // [...]
}
```

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/@scayle/storefront-nuxt2/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@scayle/storefront-nuxt2
[npm-downloads-src]: https://img.shields.io/npm/dm/@scayle/storefront-nuxt2.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/@scayle/storefront-nuxt2
[license-src]: https://img.shields.io/npm/l/@scayle/storefront-nuxt2.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/@scayle/storefront-nuxt2
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com