This is a preview of the Storyblok Website with Draft Content

Building Email Templates with Storyblok

Try Storyblok

Storyblok is the first headless CMS that works for developers & marketers alike.

This tutorial explores how to build email templates using Storyblok and Next.js. We will use React Email, which allows the creation of email templates in react using a component-based approach. Furthermore, live preview for email templates will be enabled using Storyblok’s Visual Editor. At the end of the tutorial, different approaches to sending emails from Storyblok are considered.

Setup

To get started, we will need a Next.js 14 project integrated with Storyblok. Feel free to follow this tutorial to get started.

First of all, we need to set up a dynamic route to create multiple email templates. Let’s create a [[..slug]] folder and create a page.js file with the following content:

[[slug]]/page.js
        
      import {
  getStoryblokApi,
} from "@storyblok/react/rsc";
import StoryblokStory from "@storyblok/react/story";
export const dynamicParams = true;
export default async function Page({params}) {
  let slug = params.slug ? params.slug.join("/") : "home";
  const storyblokApi = getStoryblokApi();
    let { data } = await storyblokApi.get(`cdn/stories/${slug}`, {version: 'draft'}, {cache: "no-store"});

  return (
    <div>
      <StoryblokStory story={data.story} bridgeOptions={{}} />
    </div>
  );
}
    

Next, let’s install React Email:

npm install @react-email/components  

Further information is provided in the React Email documentation.

Creating Template Components

Now, we need to create components in our frontend and inside Storyblok. To get started, let’s create an email component (content-type) first: 

  • full_screen_content: Blocks
  • body: Blocks
  • theme: Single-Option with two options:
    • Light: bg-white
    • Dark: bg-black

The first field will be used to have full screen content, the second one with a container, and the third one to define the background of the template.

Let’s add a file named Email.js  in the components with the following code:

Email.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import { Body, Container, Tailwind, Section, Head } from "@react-email/components";
const Email = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <Tailwind>
    <Head>
      <title>My email title</title>
    </Head>
      <Body className={blok.theme}>
        <div className={`w-full my-auto mx-auto`}>
          {blok?.full_screen_content?.map((nestedBlok) => (
            <StoryblokComponent className="w-full" blok={nestedBlok} key={nestedBlok._uid} />
          ))}
        </div>
        <Section className="w-2/3">
          {blok?.body?.map((nestedBlok) => (
            <StoryblokComponent className="w-full" blok={nestedBlok} key={nestedBlok._uid} />
          ))}
        </Section>
      </Body>
    </Tailwind>
  </main>
);
export default Email;
    

This will be our main content type which will render all the other components dynamically using StoryblokComponent in two separate sections. As we are using TailwindCSS, we should also use the Tailwind wrapper component from React Email. Furthermore, its Body and Container are used.

Hint:

Read about all the components from the library here.

Next, let’s create additional required components. The library contains a few components that can be read easily and used in a table format. Take a look at some examples here. For this tutorial, we will be creating only basic components -

header: Nestable block

  • title: Text
  • background:  Single-Option (feel free to add color options of your choice)
  • title_color: Single-Option with the following options:
    • White: white
    • Black: black
  • title_size: Single-Option with the following options:
    • Small: base
    • Medium: 2xl
    • Large: 4xl
    • Extra large: 7xl
  • header_height: Single-Option with the following options:
    • Small: 20
    • Medium: 32
    • Large: 40
    • Extra large: 56
StoryblokHeader.js
        
      import { storyblokEditable } from "@storyblok/react/rsc";
import { Section, Text} from "@react-email/components";
const StoryblokHeader = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <Section className={`h-${blok.header_height} bg-${blok.background}`}>
        <Text className={`text-${blok.title_size} text-center text-${blok.title_color}`}>
            {blok.title}
        </Text>
    </Section>
  </main>
);
export default StoryblokHeader;
    

section: Nestable block

  • columns: Blocks (allowing only column components to be inserted) 
  • background_color: Single-Option (feel free to add color options of your choice)
StoryblokSection.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import {
  Section
} from "@react-email/components";
const StoryblokSection = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <Section className={`bg-${blok.background_color} `}>
        {blok?.columns?.map((nestedBlok) => (
          <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
        ))}
    </Section>
  </main>
);
export default StoryblokSection;
    

row: Nestable block

  • columns: Blocks (allowing only column components to be inserted with a minimum and maximum of 2) 
  • background_color: Single-Option (feel free to add color options of your choice)
StoryblokRow.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import {
  Row,
  Section
} from "@react-email/components";
const StoryblokRow = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <Section className={`bg-${blok.background_color}`}>
      <Row>
        {blok?.columns?.map((nestedBlok) => (
          <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
        ))}
      </Row>
    </Section>
  </main>
);
export default StoryblokRow;
    

column: Nestable block

  • body: Blocks
StoryblokColumn.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import { Column } from "@react-email/components";
const StoryblokColumn = ({ blok }) => (
  <Column className="px-4 w-1/2" {...storyblokEditable(blok)}>
    {blok?.body?.map((nestedBlok) => (
      <StoryblokComponent className="" blok={nestedBlok} key={nestedBlok._uid} />
    ))}
  </Column>
);
export default StoryblokColumn;
    

text: Nestable block

  • content: text
  • color: Single-Option (feel free to add color options of your choice)
  • style: Single-Option with the following options: 
    • italic: text-base italic tracking-tight 
    • paragraph: text-base 
    • heading: text-lg font-bold     
StoryblokText.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import {
  Text
} from "@react-email/components";
const StoryblokText = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <Text className={`text-${blok.color} ${blok.style}`}>{blok.content}</Text>
  </main>
);
export default StoryblokText;

    

image

  • content: Asset
  • width: Single-Option with the following options: 
    • small: w-20
    • medium: w-40
    • large: w-60
StoryblokImage.js
        
      import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
import {
  Img
} from "@react-email/components";
const StoryblokImage = ({ blok }) => (
    <Img src={blok.content.filename} className={`${blok.width} m-4`} />
);
export default StoryblokImage;

    

line_break: Nestable block

We don’t need any fields in this, it is just a component to add a line break.

button: Nestable block

  • text : Text
  • color: Single-Option (feel free to add color options of your choice)
  • bg_color: Single-Option (feel free to add color options of your choice)
  • link: Link
StoryblokButton.js
        
      import { storyblokEditable } from "@storyblok/react/rsc";
import { Button } from "@react-email/components";
const StoryblokButton = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
  <Button
      href={blok.link.url}
      style={{ backgroundColor: blok.bg_color, padding: "10px 20px", color: blok.color }}
    >
      {blok.text}
    </Button>
  </main>
);
export default StoryblokButton;
    

In the end, to render these components dynamically, they need to be registered correctly. Replace the code from StoryblokProvider.js to the following:

StoryblokProvider.js
        
      /** 1. Tag it as client component */
"use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";
import Email from "./Email";
import StoryblokHeader from "./StoryblokHeader";
import StoryblokRow from "./StoryblokRow";
import StoryblokColumn from "./StoryblokColumn";
import StoryblokText from "./StoryblokText";
import StoryblokButton from "./StoryblokButton";
import StoryblokSection from "./StoryblokSection";

import StoryblokImage from "./StoryblokImage";
import {
  Hr
 } from "@react-email/components";
/** 2. Import your components */

/** 3. Initialize it as usual */
storyblokInit({
  accessToken: "storyblok-access-token",
  use: [apiPlugin],
  components: {
    email: Email,
    header: StoryblokHeader,
    row: StoryblokRow,
    column: StoryblokColumn,
    text: StoryblokText,
    line_break: Hr,
    image: StoryblokImage,
    button: StoryblokButton,
    section: StoryblokSection
  },
});
export default function StoryblokProvider({ children }) {
  return children;
}
    

And that’s it, most of the work is done! Now we just need to add, drag and drop these components to create email templates using Storyblok.

hint:

You might have noticed that we have added a lot of Single Option fields with tailwdind styles. The reason to add these is to have felxibility on the edtitor side enabling changing in styles directly from the Visual Editor. You are free to use your own styling frameworks or just CSS according to what is required

Creating a Template

Now that we have created most of the components we needed, let’s create a new story named “News Letter Week 1” with the content type Email and start adding the components there. 

Let’s add a Header component in both of the Blocks fields of the new Story (Full Screen Content and Body). Feel free to add the content and choose to style as preferred.

Next, let’s add a Section component and a couple of Text components along with Line Break inside it.

Similarly, we can play around and add more components, such as Rows and Columns inside those.

And that’s it! We are done with creating Email Templates inside Storyblok along with having a live preview. Let’s now take a look at approaches to send this email. 

Sending Emails

There are multiple ways in which the complete flow can work, from creating the templates to sending the emails. You are free to choose on how you would like the create the templates as we did with one of the libraries. 

You can have your own engine to read the data stored inside Storyblok for creating templates out of it and sending emails accordingly. This data can be used along with any email provider to generate templates. 

There are a few options to send the emails to the customers, these emails can be dynamic as well (can have dynamic content like users name and so on). The approach to send the emails would be to convert the code into a HTML format, and send it to the required users. In this case, we need to transform our React code to the required format and send it then so it is compatible with the browsers. And for this transformation to HTML email, we can use the render utility from React Email.

There can be multiple providers that can be use for sending the emails, here are a few that can be used with React Email. For our case we can use a library called Resend that works very well with React Email. It also takes care of the transformation, so we don’t have render the template into HTML by ourselves.

Let’s install it and follow the steps mentioned in the official docs for implementing it with Next.js. You also need to have an account to grab the resend token.

We need to a new file api/send/route.js in our Next.js project with the following content:

api/send/route.js
        
      
import { Resend } from 'resend';
import {
  getStoryblokApi, StoryblokComponent, storyblokInit, apiPlugin
} from "@storyblok/react/rsc";
import Email from '@/components/Email';
import StoryblokHeader from "@/components/StoryblokHeader";
import StoryblokRow from "@/components/StoryblokRow";
import StoryblokColumn from "@/components/StoryblokColumn";
import StoryblokText from "@/components/StoryblokText";
import StoryblokImage from "@/components/StoryblokImage";
import StoryblokSection from "@/components/StoryblokSection";
import StoryblokButton from "@/components/StoryblokButton";
import { Hr } from '@react-email/components';

const resend = new Resend("resend-token");

storyblokInit({
  accessToken: "storyblok-access-token",
  use: [apiPlugin],
  components: {
    email: Email,
    header: StoryblokHeader,
    row: StoryblokRow,
    column: StoryblokColumn,
    text: StoryblokText,
    line_break: Hr,
    image: StoryblokImage,
    button: StoryblokButton,
    section: StoryblokSection
  },
})

export async function POST() {
  const storyblokApi = await getStoryblokApi();
  let { data } = await storyblokApi.get(`cdn/stories/news-letter-week-1`, {version: 'draft'}, {cache: "no-store"});
  const emailTemplate =  Email({blok: data.story.content}) 
  try {
    const { d, e } = await resend.emails.send({
      from: 'Chakit <onboarding@resend.dev>',
      to: ['chakit.arora@storyblok.com'],
      subject: 'Hello world',
      react: emailTemplate,
    });
    if (e) {
      return Response.json({ e }, { status: 500 });
    }
    return Response.json({'message': 'ok'});
  } catch (error) {
    console.log(error)
    return Response.json({ error }, { status: 500 });
  }
}
    

This function grabs the data, creates an email template out of it and then uses Resend to send the email to the specified email(s). To keep it simple, we are just fetching the data of only story we created but in a real case it would be dynamic, dependent on the use case. To fetch the data we are using the Storyblok API, and then passing it inside the Email component to generate the React code which is directly used by Resend.

In the end, we just need to send a post request to the following route - https://www.yourdomain/api/send. This can also be triggered in multiple ways depending on the use case. We could even use Storyblok Webhooks to trigger the sending based on the publishing event of Email stories.

That’s all; we now have a fully functional Email template setup using Storyblok! 

Now, let’s also understand some more interesting concepts. 

As we already saw the possibility of transforming emails in HTML format using the render functionality, it is good to know that there can be more possibilities for our custom methods to render these templates. Once rendered, these templates can also be stored on other email communication platforms. Webhooks can also automate this process, so any change in the template can be synced with the templates stored on the email platform. 

Another essential thing to talk about is the personalization of the emails, like having dynamic receiver names or showing something specific to a condition. For such cases, the variables can be injected while sending the emails or when using an email-sending platform to store the templates; the templates can be generated with variables that can be added by the email platform on the go. Multiple possibilities and routes here can be taken according to the use cases. Such conditions and variable placeholders can also be stored in Storyblok, which can be used while generating the template as required. 

Conclusion

In this tutorial, we saw a way of creating Email Templates using Next.js and Storyblok that are editable in real time. We also discussed rendering in required format and different ways of sending email templates maintained in Storyblok. Along with this we touched upon the part of personalization and using different platforms. 

Author

Chakit Arora

Chakit Arora

Chakit is a Full Stack Developer based in India, he is passionate about the web and likes to be involved in the community. He is a Twitter space host, who likes to talk and write about technology. He is always excited to try out new technologies and frameworks. He works as a Developer Relations Engineer at Storyblok.