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.
To start working with storefront-core, make sure to register it as a module in your nuxt.config.ts file:
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. Basically you are able to configure the BAPI backend, from which URL images should be served, caching providers and so forth.
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
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:
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
Core: useRpc
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:
import { useRpc } from '@scayle/storefront-nuxt2'
import { rpcMethods } from './index'
const { fetching, data, fetch } = useRpc(rpcMethods.getProductById)
await fetch({ id: 12 })
Methods for managing the wishlist - adding and removing items, etc.
import { useWishlist } from '@scayle/storefront-nuxt2'
const { addItem, removeItem, clearWishlist, isInWishlist, fetch } =
useWishlist()
await fetch()
await addItem({ variantId: 123 })
There are two places where we can set up a log.
- Injected into the nuxt context
- 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.
Create a new nuxt plugin and run Log.attachToNuxtContext
.
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.
Create a new server-middleware that runs before core.
export default (req: any, res: any, next: any) => {
Log.attachToRequest(
req,
new Log({
space: 'exampleProject',
handler: (entry: LogEntry) => { ... },
})
)
next()
}
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
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.
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.
You can track execution time by using the .time(...)
method from Log
.
Here is an example how to use it:
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
Storefront Core supports two modes of Caching. In general all cached entries are namespaced by shopId.
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.
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:
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.
There are multiple ways to control the caching feature. First of all, the cache is only used when it is enabled via config:
// 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.
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:
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']
.
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.
POST <host>/_cache/purge/all
Purge the entire cache, including all cached downstream API requests (BAPI, etc.)
POST <host>/_cache/purge/tags
Body: ["product:1001"]
Purge all cache entries that are tagged with product:1001
.
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:
// nuxt.config.ts
storefront: {
// [...]
cache: {
enabled: true,
ttl: 60 * 60,
sendCacheControlHeaders: true,
maxAge: 60 * 60,
staleWhileRevalidate: 60 * 60 * 24,
}
// [...]
}