Bloomreach Content is a powerful Software-as-a-Service (SaaS) solution that allows businesses to create, manage, and deliver digital content across multiple channels. Resource bundles are an important aspect of Bloomreach Content that enable users to easily manage localised content and translations for different regions and languages.
The use case of resource bundles is the localisation of labels that are reused frequently across your site. For example, unique texts in the header and footer, button labels, common component texts etc. The question at hand is how to make the resource bundles available to the frontend application in a headless implementation.
In this blog, we will showcase two solutions with their respective configurations, coding examples, and pros and cons.
Solution 1: Developer configurable Channel property in combination with an SPA provider component
In this approach, we define a new channel property (eg. rbPath) that will contain the absolute content path to the resource bundle (it can also be a comma-separated list if multiple is necessary). The frontend will utilise the document delivery API to retrieve and store the resource bundles. Below we provide implementation examples in React and Vue of such services.
Note: the implementation examples assume a single resource bundle is configured.
Adding the channel property can be achieved either via the site development tool (see screenshots below) or via the site management API as described here.
This is an example response of the content delivery API for a resource bundle
eg.https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents/{path-to-rb-document}
Important: Remember to change the {client}
, {channel}
and {path-to-rb-document}
with the right values for you. The same applies to the provided URLs in the code snippets below.
{
"meta": {
"product": "brx",
"version": "1.0",
"branch": "master"
},
"document": {
"$ref": "/content/u3e27f53db63b4a76a7912a1896acd157"
},
"content": {
"u3e27f53db63b4a76a7912a1896acd157": {
"type": "document",
"links": {
"site": {
"type": "unknown"
}
},
"meta": {},
"data": {
"name": "site-labels",
"displayName": "Site labels",
"stateSummary": "live",
"messages": [
"Test"
],
"keys": [
"test"
],
"id": "3e27f53d-b63b-4a76-a791-2a1896acd157",
"state": "published",
"valueSets": [
{
"name": "[default]",
"messages": [
"Test"
]
},
{
"name": "en",
"messages": [
"Test EN"
]
},
{
"name": "nl",
"messages": [
"Test NL"
]
}
],
"localeString": null,
"contentType": "resourcebundle:resourcebundle"
}
}
}
}
React example code - Provider/Consumer
In React, we can have an implementation leveraging the Context provider/consumer functionality.
RBContext.tsx
is our frontend component responsible for fetching and providing the resource bundle document
import React, {useEffect, useState} from "react";
import axios from "axios";
export const RBContext = React.createContext(null);
async function rbMap(page: any) {
const locale = 'en'; //'this should be dynamic based on how your SPA handles the locale'
const rbpath = page.model.channel.info.props.rbPath;
const data = await axios
.get(`https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents${rbpath}`)
.then(response => response.data)
.catch(error => console.log(error))
const content = data?.document.$ref.split('/').reduce((value, key) => (key ? value?.[key] : data), data).data
const keys = content.keys;
const valueSets = content.valueSets;
let valueSet;
if(valueSets.some((set:any) => set.name === locale)) {
valueSet = valueSets.find((set:any) => set.name === locale);
} else {
valueSet = valueSets.find((set:any) => set.name === '[default]');
}
const rbMap = {};
keys.forEach((element, index) => {
rbMap[element] = valueSet.messages[index];
});
return rbMap;
}
export const RBProvider = (props:any) =>{
const [data, setData] = useState(new Map)
useEffect(()=> {
(async () => {
const data = await rbMap(props.page);
setData(data);
})()
},[])
return(
<RBContext.Provider value={data}>
{props.children}
</RBContext.Provider>
)
}
In App.tsx
we should wrap our application with the RBProvider
component and pass to it the page context.
<BrPage configuration={{ ...configuration, httpClient: axios as any }} mapping={mapping} page={page}>
<BrPageContext.Consumer>
{(contextPage) => (<>
<RBProvider page={contextPage}>
//Rest of App
</RBProvider>
</>)}
</BrPageContext.Consumer>
</BrPage>
In any of your components where you want to retrieve and use a label
const valueSet = React.useContext(RBContext);
{valueSet?.['test']}//where test is the key
Vue example code - Provide/Inject
RBContext.vue
is our frontend component responsible for fetching and providing the resource bundle document
<template>
<div>
<slot />
</div>
</template>
<script lang="ts">
import { Page } from '@bloomreach/spa-sdk';
import { Component, Prop, Vue } from 'nuxt-property-decorator';
import axios from 'axios';
@Component({
name: 'RBContext',
provide() {
return {
rbMap: this.rbMap
}
},
computed: {
async rbMap() {
const locale = 'en'; //'this should be dynamic based on how your SPA handles the locale'
const rbpath = this.page.model.channel.info.props.rbPath;
const data = await axios
.get(`https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents${rbpath}`)
.then(response => response.data)
.catch(error => console.log(error))
const content = data?.document.$ref.split('/').reduce((value, key) => (key ? value?.[key] : data), data).data
const keys = content.keys;
const valueSets = content.valueSets;
let valueSet;
if(valueSets.some(set => set.name === locale)) {
valueSet = valueSets.find(set => set.name === locale);
} else {
valueSet = valueSets.find(set => set.name === '[default]');
}
const rbMap = {};
keys.forEach((element, index) => {
rbMap[element] = valueSet.messages[index];
});
return rbMap;
}
},
})
export default class RBContext extends Vue {
@Prop() page!: Page;
data: any;
}
</script>
In your _.vue
file the template should look something like the following example.
Important: the component r-b-context
should be within the br-page
component and include the rest application structure so that we can use the provide/inject functionality of Vue.
<template>
<br-page v-if="configuration && page" :configuration="configuration" :mapping="mapping" :page="page">
<template #default="props">
<template v-if="props.page">
<r-b-context :page="page">
<br-component component="header" />
<br-component component="main">
<template v-slot:default="{ component, page }" />
</br-component>
<br-component component="footer" />
</r-b-context>
</template>
</template>
</br-page>
</template>
In your template use the valueSet map like below
{{valueSet?.['test']}} //where 'test' is the key
In your component configuration have the inject, data and mounted defined as follows
@Component({
name: 'HomepageBanner',
inject: ['rbMap'],
data() {
return {
valueSet: null,
};
},
computed: {...},
async mounted(): Promise<void> {
this.valueSet = await this.rbMap;
},
})
Solution 2: Page layout root component with a SPA provider component
An alternative solution to the first approach is to make the resource bundle document(s) part of the page model API response directly.
For the above, all the page layouts will require a root component (via inheritance and explicitly) that contributes the configured resource bundle to the API response. As a static component, the document(s) can only be configured by site developers. The resource bundle component will have to have the rest of the page structure below it. This hierarchy is required so that the frontend functionality can work.
The frontend implementation though similar would have to change slightly. Now the RBContext component wonβt read the data from the Content delivery API but instead from the component itself that is part of the page structure. The rest functionality of parsing the retrieved resource bundle document and providing it for consumption (React) or injection (Vue) will remain the same.
Implementation remarks
- Any type of async functionality has to be done outside the rendering of components to HTML if you want to support SSR. Getting the valueSet on the server side and setting it as a value in the provider/context directly.
- We advise building in some caching mechanism that allows you to only request the translation once per session or something like that. Usually, translations can be requested per page, but once requested they could simply be saved in memory instead of re-requesting it on every page load as a user is navigating through the app.
Solution 1
+ smaller payload of initial page load
+ easier maintainable configuration
- requires extra call(s) to retrieve the configured resource bundlesSolution 2
+ no extra calls necessary
- due to how translations are usually quite static the request can not be cached this way as it is always part of the page
- the response payload will be bigger as the resource bundle component and the document(s) will always be part of the API response
- it is a more complicated setup
- configuring more than one resource bundles will be an extra configuration overhead
Top comments (0)