How to interpolate variables from content using richTextComponents?

Hi, I have an issue and somehow struggling with implementation of this.

I'm using Prismic with Next.js and my content will contain prices that should change when the currency is changed on the site.

For example I have a some rich text in my model with the following content (a paragraph):
"The new reader is just a one-off fee of £250 and is automatically available to your subscription once connected. A compatible Bouncepad is required to use App Tap, however you can upgrade your existing Bouncepad with the new back plate for just £30."

Now when I change the currency on site to $ it should read:

"The new reader is just a one-off fee of $300 and is automatically available to your subscription once connected. A compatible Bouncepad is required to use App Tap, however you can upgrade your existing Bouncepad with the new back plate for just $40."

So basically these prices should be as variables:

"The new reader is just a one-off fee of {{ subscriptionPrice }} and is automatically available to your subscription once connected. A compatible Bouncepad is required to use App Tap, however you can upgrade your existing Bouncepad with the new back plate for just {{ upgradePrice }}."

I'm using context to set the currency and provide these prices globally in my app.
I'm also using Rich Text with PrismicProvider component as follows:

const App = ({Component, pageProps, children}) => {

    const richTextComponents = {
        heading1: ({children}) => <h1 className="text-5xl font-bold mb-6">{children}</h1> ,
        heading2: ({children}) => <h2 className="text-4xl font-bold mb-6">{children}</h2>,
        heading3: ({children}) => <h3 className="text-3xl font-bold mb-6">{children}</h3>,
        heading4: ({children}) => <h4 className="text-2xl font-bold mb-6">{children}</h4>,
        heading5: ({children}) => <h5 className="text-xl font-bold mb-6">{children}</h5>,
        heading6: ({children}) => <h6 className="text-lg font-bold mb-6">{children}</h6>,
        paragraph: ({children}) => <p className="text-lg mb-6">{children}</p>,
    };

    return (
        <CurrencyProvider>
            <PrismicProvider internalLinkComponent={(props) => <Link {...props} />}
                             richTextComponents={richTextComponents}>
                {children}
                <PrismicPreview repositoryName={repositoryName}>
                    <Layout>
                        <Component {...pageProps} />
                    </Layout>
                </PrismicPreview>
            </PrismicProvider>
        </CurrencyProvider>
    );
};

export default appWithTranslation(App);

To add, the content would be contained in a slice, and multiple slices are present on the page.

How can I catch these variables from rich text and replace them with any value I want? Is this even possible?

When I console.log mapped children it's a bit tough as it's quite nested and would be really ugly to find and replace these like that. I would have to chain multiple maps to access the text itself.

        paragraph: ({children}) => {
            const text = children.map((child) => console.log(child));
            return <p className="text-lg mb-6">{children}</p>;

There must a simpler way...

So I actually tried to see if this is even going to work but unfortunately it's not doing anything and it's not updating the content when I switch the currency.

The variables need to react to currency change, they need to be reactive. I'm using SSG and if I have a hard-coded content it works perfectly fine.

What are my options in this case? I'm really running out of ideas here :frowning:

Another update from me - I managed to make it work.

As I cannot access context in _app.js I overwritten it like this in my slice:

    const {currency, signInTapPrice, signInTapUpgradePrice} = useContext(CurrencyContext);
    const [updatedComponent, setUpdatedComponent] = useState({});

    useEffect(() => {
        setUpdatedComponent({
            paragraph: ({children}) => {
                const childrenMapping = children.map(({props}) => props.children.map(({props}) => props.children));
                const result = childrenMapping.map(item => item[0]
                    .replace('{{ signInTapPrice }}', signInTapPrice())
                    .replace('{{ signInTapUpgradePrice }}', signInTapUpgradePrice()));
                return <p className="text-lg mb-6">{result}</p>;
            }
        });
    }, [currency]);

Then in my slice template:

<PrismicRichText field={slice.primary.body} components={updatedComponent}/>

It's a bit ugly if I have to be honest, mostly due to how nested it is. Is there a cleaner way to do this? It still doesn't solve the problem that I'd like these variables to be used globally across whole content - otherwise I'll have to do it in every component I'm using it :frowning:

Hi, @dmakos. Thanks for reaching out. I can think of a few workarounds you could use to do this. Let me look into this to create a few examples, and I'll get back to you tomorrow.

1 Like

Hi @louise.findlay thank you so much, looking forward to get some examples :slight_smile:

I also need to make sure there are no concerns for SEO

Hi, @dmarkos. Here’s a couple of ways you could extract the prices from Prismic Rich Text and have it update when switching currency.

Our official recommendation for this use case is to use locales. Create a locale for each currency you use and copy all content to every locale. Then, you can change the currency values in the Editor. Reading the Next.js internationalization how-to will help you create the routing and link the currency switcher to the locales.

Alternatively, if you don't want to create duplicate content with locales, you could use custom labels. There are two ways you could do this.

The first option is to create a custom label for the currency. Use the HTML serializer to get the label content from the Rich Text field. Then pass it to your currency function and return the result. This will work if the currency conversion rate is consistent.

If the currency conversion rate differs between products, you need to include all the currencies in the Rich Text field. Separate the currency values with a special character such as|. Then apply the custom label. For example, £30|$40|€20. Create a custom HTML Serializer and pass the text from the custom label to your currency function. In your formatCurrency() function, you can split() the currencies by the special character and then manipulate them.


function formatCurrency(value){
  // Manipulate the currency string here
}

const htmlSerializer = {
  label: ({ node }) => {
    if (node.data.label === "currency") {
      return {formatCurrency(node.text)}
    }
  },
};
1 Like

I can't use currency for different locales unfortunately - I need to have currencies locale agnostic and not connected to them. Basically our users need to be able to see the price in pounds even if I they are looking at this in French.

The second option looks much better for me - I will play around once I'm back to this project, sorry I've been thrown into another one that I need to complete first and then I'll come back to you if I have any issues with that approach :slight_smile: thank you!

2 Likes

Hi @louise.findlay ,
I'm finally back to this project! :slight_smile: But I'm struggling a bit on how to create a custom label? I'm not sure if the link you provided under 'custom labels' is correct - can't find any info about it there or anywhere in docs really. Do I need to edit model.json directly to do this?

I'm using slice machine with Next.js and the currency containing content will be in one of the slices, how can I go about this?

Hi, @dmakos.

Yes, you need to edit the slice's model.json to create a custom label. I've added an example below. Here's the link to our custom label doc for more information.

"primary": {
   "content": {
      "type": "StructuredText",
      "config": {
        "label": "Content",
        "placeholder": "Lorem ipsum",
        "allowTargetBlank": false,
+       "labels": ["currency"],
        "multi": "paragraph"
      }
    }
  },
1 Like

@louise.findlay I managed to make it work within the slice! Thank you so much :slight_smile:

2023-04-17 12.40.56

My final question would be is there a way to make it global across the whole app rather than per slice? If that's not possible to make it global, would it work on Custom Types (page or article) the same way as with slice? Where would I pass serializer in this case?

I'm using React useContext to set the currency globally and return the correct values if that makes a difference

1 Like

Hi, @dmakos. That's great news. Let me take a look at making the currency converter code global and then I'll let you know.

Thanks,
Louise

1 Like

@louise.findlay thanks so much, really looking forward for having this option - the reason why I'd like it global is that I cannot know where our marketing team will be using these prices in the content - it could be any slice or any page. I mean, I could declare custom labels in every RichText type I have on page and in slices but then if I need to add new pricing I'll have to through all these and add it there as well, which is not ideal, especially when the site will keep growing and I'll be adding new slices.

Edit: Another point why I need this globally: slice variations, I have slice with two variations meaning I have to add labels twice in the model.json, I plan to have more variations in a few slices - it can become a duplication and maintenance problem fairly quickly :frowning:

Are there any plans for the future to be able to add "custom labels" via Slice Machine? Or it will strictly remain as a direct model editing?

Hey @dmakos,

I'm stepping in because Louise is out this afternoon. To make this global, you will need to do two things:

  • Manually add the label to the JSON for any Rich Text field where you want it available. These fields could be in the Static Zone of a Custom Type or inside a Slice.
  • If you're using @prismicio/react, you can pass your htmlSerializer to the richTextComponents prop of the PrismicProvider component. This will apply the htmlSerializer everywhere in your app so you don't have to worry about providing it manually.

For now, a UI for custom labels in Slice Machine isn't on the roadmap. If you'd like to see it, feel free to create a thread in #product-feature-idea to share the idea and invite others to add :heart:s.

I hope that helps! Let me know if you have any questions.

Sam

1 Like

Hi @samlittlefair

Thanks for coming back to me, really appreciate it.

I see, so there is no way to set the custom labels globally and it has to be added on each slice/custom type in each Rich Text Field. The issue will be with maintainability of this, if I have to add additional pricing to the pool to be used within content, I'll have to add it in each and every slice/type I have, including variations :frowning: wish there was some better way to do this. Still, I'm happy that this is even possible and grateful for your help.

I shall try with passing serializer in the richTextComponent, at least one part can be sorted easily!

I also started a thread in the product feature idea.

Thank you!