Designing Emails to Drive Clicks and Conversions

Email design has evolved to reflect changes in technology and consumer behavior. The goal is to elicit the desired action from the recipient, such as a click or conversion. In this post, I’ll address tips on designing high-performing emails for ecommerce.

Every email marketing message should contain specific elements. For U.S. senders, the message must be compliant with the CAN-SPAM Act of 2003, which requires a clear unsubscribe link and a valid “from” address that is representative of the sender.

Beyond those requirements, the message and images are up to the marketer, so long as both are not misleading to the recipients. Common design elements include:

  • Logo or brand name, linked to the home page;
  • A primary image;
  • Text-based message (not embedded in an image);
  • A thoughtful balance of text and images;
  • Clear call-to-action;
  • Contact information and social media accounts;
  • Compelling “From” line, subject line, and preheader.

An email template can help streamline the production process. A consistent template also helps recipients navigate and respond to the message.

Email Templates

In my experience, a visual hierarchy produces the best layouts. Images are powerful. They impact recipients’ actions and attitudes. Templates should reflect the natural way people comprehend and interpret information, such as an inverted V pattern with a large image on top, then text, and then a call-to-action.

A marketing email from Costco with an inverted "V" design.

This email from Costco uses an inverted “V” design.

Another popular layout is a “Z” pattern, where the recipients read left to right, mimicking standard reading patterns.

This email from Rite Aid uses a "Z" design pattern.

This email from Rite Aid uses a “Z” design pattern.

Hero section. The so-called “hero” section of an email typically appears just under the logo and top navigation. It conveys the main objective of the email. Hero text should be large enough to read easily. Buttons should facilitate a finger on mobile.

This "hero" example from Famous Footwear is easy to read and conveys the overall theme. 

This “hero” example from Famous Footwear is easy to read and conveys the overall theme.

White space. Adequate white space around an image, text, and call-to-action is critical. It makes the entire message less intimidating and easier to digest, especially on mobile.

Buttons. Again, all buttons should be large enough for a finger on a smartphone and not too close. Many designers utilize a “bulletproof button.” It is not an image. It’s text on top of a background, such as a solid color. This enables recipients to read the call-to-action if images are turned off or take too long to download.  Campaign Monitor, for example, offers a widget for building bulletproof buttons.

Experiment and Test

Maintaining email design consistency can help recipients know what to expect. But change is good, too, to avoid design fatigue. Test new design layouts against performance — opens, clicks, conversions.

One idea is testing “dark mode” in an email.  That’s a setting on smartphones wherein users swap light colors for dark and vice versa. The purpose is to preserve battery life and ease viewing in low-light situations. Certainly all emails should by default render well in dark mode. But marketers can also test a native dark version with white text on a dark or black background.

Another test is inserting interactive or dynamic content, such as animated buttons, product carousels, countdown timers, surveys, or polls. Accelerated Mobile Pages (“AMP”) for email was introduced in 2018 to integrate live or custom content into the body of an email. AMP has not taken off due to limited support from email service providers. But Gmail does support it. I’ve addressed the possibilities at “Does ‘AMP for Email’ Impact Ecommerce?

Getting Started With Next.js

Lately, Next.js has termed itself The React Framework for Production, and with such bold claim comes a bevy of features that it offers to help you take your React websites from zero to production. These features would matter less if Next.js isn’t relatively easy to learn, and while the numerous features might mean more things and nuances to learn, its attempt at simplicity, power, and perhaps success at it is definitely something to have in your arsenal.

As you settle in to learn about Next.js, there are some things you might already be familiar with and you might even be surprised at how it gives you a lot to work with that it might seem almost overwhelming at face value. Next.js is lit for static sites and it has been well-engineered for that purpose. But it also takes it further with its Incremental Static Regeneration that combines well with existing features to make development a soothing experience. But wait, you might ask. Why Next.js?

This tutorial will be beneficial to developers who are looking to get started with Next.js or have already begun but need to fill some knowledge gaps. You do not need to be a pro in React, however, having a working experience with React will come in handy.

But Why Next.js?

  1. Relatively easy to learn.
    That’s it. If you’ve written any React at all, you’d find yourself at home with Next.js. It offers you advanced tools and a robust API support, but it doesn’t force you to use them.
  2. Built-in CSS support.
    Writing CSS in component-driven frameworks comes with a sacrosanct need for the “cascade”. It’s why you have CSS-in-JS tools, but Next.js comes out of the box with its own offering — styled-jsx, and also supports a bevy of styling methodologies.
  3. Automatic TypeScript support.
    If you like to code in TypeScript, with Next.js, you literally have automatic support for TypeScript configuration and compilation.
  4. Multiple data fetching technique.
    It supports SSG and/or SSR. You can choose to use one or the other, or both.
  5. File-system routing.
    To navigate between one page to another is supported through the file-system of your app. You do not need any special library to handle routing.

There are many more other features, e.g. using experimental ES features like optional-chaining, not importing react everywhere you use JSX, support for APIs like next/head that helps manage the head of your HTML document, and so on. Suffice to say the deeper you go, the more you enjoy, appreciate, and discover many other features.

Requirements For Creating A Next.js App

Creating a Next.js app requires Node.js, and npm (or npx) installed.

To check if you have Node.js installed, run the command in your terminal:

# It should respond with a version number
node -v

Ideally, npm (and npx) comes with your Node.js installation. To confirm that you have them installed, run the commands in your terminal:

# Run this. It should respond with a version number
npm -v # Then run this. It should also respond with a version number
npx -v

In case any of the commands above fails to respond with a version number, you might want to look into installing Node.js and npm.

If you prefer the yarn package manager instead, you can run install it with the command:

# Installs yarn globally
npm i -g yarn

Then confirm the installation with:

# It should also respond with a version number
yarn -v

Creating A Next.js App

Getting the requirements above out of the way, creating a Next.js can be done in two ways, the first being the simplest:

  1. With create-next-app, or
  2. Manually

Creating A Next.js App With create-next-app

Using create-next-app is simple and straightforward, plus you can also get going with a starter like Next.js with Redux, Next.js with Tailwind CSS, or Next.js with Sanity CMS etc. You can view the full list of starters in the Next.js examples repo.

# Create a new Next.js app with npx
npx create-next-app <app-name> # Create a new Next.js app with npm
npm create-next-app <app-name> # With yarn
yarn create next-app <app-name>

If you’re wondering what the difference between npm and npx is, there’s an in-depth article on the npm blog, Introducing npx: an npm package runner.

Creating A Next.js Project Manually

This requires three packages: next, react, and react-dom.

# With npm
npm install next react react-dom # With yarn
yarn add next react react-dom

Then add the following scripts to package.json.

"scripts": { "dev": "next dev", "start": "next start", "build": "next build"

Folder Structure

One salient thing you might notice after creating a Next.js app is the lean folder structure. You get the bare minimum to run a Next.js app. No more, no less. What you end up with as your app grows is up to you more than it is to the framework.

The only Next.js specific folders are the pages, public, and styles folder.

# other files and folders, .gitignore, package.json...
- pages - api - hello.js - _app.js - index.js
- public - favicon.ico - vercel.svg
- styles - globals.css - Home.module.css


In a Next.js app, pages is one of the Next-specific folders you get. Here are some things you need to know about pages:

  • Pages are React components
    Each file in it is a page and each page is a React component.

// Location: /pages/homepage.js
// <HomePage/> is just a basic React component
export default HomePage() { return <h1>Welcome to Next.js</h1>
  • Custom pages
    These are special pages prefixed with the underscore, like _app.js.

    • _app.js: This is a custom component that resides in the pages folder. Next.js uses this component to initialize pages.
    • _document.js: Like _app.js, _document.js is a custom component that Next.js uses to augment your applications <html> and <body> tags. This is necessary because Next.js pages skip the definition of the surrounding document’s markup.
  • File-based routing system based on pages
    Next.js has a file-based routing system where each page automatically becomes a route based on its file name. For example, a page at pages/profile will be located at /profile, and pages/index.js at /.

# Other folders
- pages - index.js # located at / - profile.js # located at /profile - dashboard - index.js # located at /dashboard - payments.js # located at /dashboard/payments


Next.js has a file-based routing system based on pages. Every page created automatically becomes a route. For example, pages/books.js will become route /book.js.

- pages - index.js # url: / - books.js # url: /books - profile.js # url: /profile

Routing has led to libraries like React Router and can be daunting and quite complex because of the sheer number of ways you might see fit to route section of your pages in your Next.js app. Speaking about routing in Next.js is fairly straightforward, for the most part of it, the file-based routing system can be used to define the most common routing patterns.

Index Routes

The pages folder automatically has a page index.js which is automatically routed to the starting point of your application as /. But you can have different index.jss across your pages, but one in each folder. You don’t have to do this but it helps to define the starting point of your routes, and avoid some redundancy in naming. Take this folder structure for example:

- pages - index.js - users - index.js - [user].js

There are two index routes at / and /users. It is possible to name the index route in the users folder users.js and have it routed to /users/users if that’s readable and convenient for you. Otherwise, you can use the index route to mitigate the redundancy.

Nested Routes

How do you structure your folder to have a route like /dashboard/user/:id.

You need nested folders:

- pages - index.js - dashboard - index.js - user - [id].js # dynamic id for each user

You can nest and go deeper as much as you like.

Dynamic Route Segments

The segments of a URL are not always indeterminate. Sometimes you just can’t tell what will be there at development. This is where dynamic route segments come in. In the last example, :id is the dynamic segment in the URL /dashboard/user/:id. The id determines the user that will be on the page currently. If you can think about it, most likely you can create it with the file-system.

The dynamic part can appear anywhere in the nested routes:

- pages - dashboard - user - [id].js - profile

will give the route /dashboard/user/:id/profile which leads to a profile page of a user with a particular id.

Imagine trying to access a route /news/:category/:category-type/:league/:team where category, category-type, league, and team are dynamic segments. Each segment will be a file, and files can’t be nested. This is where you’d need a catch-all routes where you spread the dynamic parts like:

- pages - news - [].js

Then you can access the route like /news/sport/football/epl/liverpool.

You might be wondering how to get the dynamic segments in your components. The useRouter hook, exported from next/router is reserved for that purpose and others. It exposes the router object.

import { useRouter } from 'next/router'; export default function Post() { // useRouter returns the router object const router = useRouter(); console.log({ router }); return <div> News </div>;

The dynamic segments are in the query property of the router object, accessed with router.query. If there are no queries, the query property returns an empty object.

Linking Between Pages

Navigating between pages in your apps can be done with the Link component exported by next/link. Say you have the pages:

- pages - index.js - profile.js - settings.js - users - index.js - [user].js

You can Link them like:

import Link from "next/link"; export default function Users({users) { return ( <div> <Link href="/">Home</Link> <Link href="/profile">Profile</Link> <Link href="/settings"> <a> Settings </a> </Link> <Link href="/users"> <a> Settings </a> </Link> <Link href="/users/bob"> <a> Settings </a> </Link> </div> )

The Link component has a number of acceptable props, href — the URL of the hyperlink — been the only required one. It’s equivalent to the href attribute of the HTML anchor (<a>) element.

Other props include:

PropDefault valueDescription
asSame as hrefIndicates what to show in the browser URL bar.
passHreffalseForces the Link component to pass the href prop to its child./td>
prefetchtrueAllows Next.js to proactively fetch pages currently in the viewport even before they’re visited for faster page transitions.
replacefalseReplaces the current navigation history instead of pushing a new URL onto the history stack.
scrolltrueAfter navigation, the new page should be scrolled to the top.
shallowfalseUpdate the path of the current page without re-running getStaticProps, getServerSideProps, or getInitialProps, allows the page to have stale data if turned on.


Next.js comes with three styling methods out of the box, global CSS, CSS Modules, and styled-jsx.

There’s an extensive article about Styling in Next.js that has been covered in Comparing Styling Methods in Next.js

Linting And Formatting

Linting and formatting I suspect is a highly opinionated topic, but empirical metrics show that most people who need it in their JavaScript codebase seem to enjoy the company of ESLint and Prettier. Where the latter ideally formats, the former lints your codebase. I’ve become quite accustomed to Wes Bos’s ESLint and Prettier Setup because it extends eslint-config-airbnb, interpolate prettier formatting through ESLint, includes sensible-defaults that mostly works (for me), and can be overridden if the need arises.

Including it in your Next.js project is fairly straightforward. You can install it globally if you want but we’d be doing so locally.

  • Run the command below in your terminal.
# This will install all peer dependencies required for the package to work
npx install-peerdeps --dev eslint-config-wesbos
  • Create a .eslintrc file at the root of your Next.js app, alongside the pages, styles and public folder, with the content:
{ "extends": [ "wesbos" ]

At this point, you can either lint and format your code manually or you can let your editor take control.

  • To lint and format manually requires adding two npm scripts lint, and lint:fix.
"scripts": { "dev": "next dev", "build": "next build", "start": "next start" "lint": "eslint .", # Lints and show you errors and warnings alone "lint:fix": "eslint . --fix" # Lints and fixes
  • If you’re using VSCode and you’d prefer your editor to automatically lint and format you need to first install the ESLint VSCode plugin then add the following commands to your VSCode settings:
# Other setting "editor.formatOnSave": true, "[javascript]": { "editor.formatOnSave": false
}, "[javascriptreact]": { "editor.formatOnSave": false
}, "eslint.alwaysShowStatus": true, "editor.codeActionsOnSave": { "source.fixAll": true
}, "prettier.disableLanguages": ["javascript", "javascriptreact"],

Note: You can learn more on how to make it work with VSCode over here.

As you work along you most likely will need to override some config, for example, I had to turn off the react/jsx-props-no-spreading rule which errors out when JSX props are been spread as in the case of pageProps in the Next.js custom page component, _app.js.

function MyApp({ Component, pageProps }) { return <Component {...pageProps} />;

Turning the rule off goes thus:

{ "extends": [ "wesbos" ], "rules": { "react/jsx-props-no-spreading": 0 }

Static Assets

At some or several points in your Next.js app lifespan, you’re going to need an asset or another. It could be icons, self-hosted fonts, or images, and so on. To Next.js this is otherwise known as Static File Serving and there is a single source of truth, the public folder. The Next.js docs warns: Don’t name the public directory anything else. The name cannot be changed and is the only directory used to serve static assets.

Accessing static files is straightforward. Take the folder structure below for example,

- pages profile.js
- public - favicon.ico #url /favicon.ico - assets - fonts - font-x.woff2 - font-x.woff # url: /assets/fonts/font-x.woff2 - images - profile-img.png # url: /assets/images/profile-img.png
- styles - globals.css

You can access the the profile-img.png image from the <Profile/> component:

// <Profile/> is a React component
export default function Profile() { return { <div className="profile-img__wrap"> <img src="/assets/images/profile-img.png" alt="a big goofy grin" /> </div> }

or the fonts in the fonts folder in CSS:

/* styles/globals.css */
@font-face { font-family: 'font-x'; src: url(/assets/fonts/font-x.woff2) format('woff2'), url(/assets/fonts/font-x.woff) format('woff');

Data Fetching

Data fetching in Next.js is a huge topic that requires some level of undertaken. Here, we’ll discuss the crux. Before we dive in, there’s a precursory need to have an idea of how Next.js renders its pages.

Pre-rendering is a huge part of how Next.js works as well as what makes it fast. By default, Next.js pre-renders every page by generating each page HTML in advance alongside the minimal JavaScript they need to run, through a process known as Hydration.

It is possible albeit impractical for you to turn off JavaScript and still have some parts of your Next.js app render. If you ever do this, consider doing it for mechanical purposes alone to show that Next.js truly Hydrates rendered pages.

That being said, there are two forms of pre-rendering:

  1. Static Generation (SG),
  2. Server-side Rendering (SSR).

The difference between the two lies in when data is been fetched. For SG, data is fetched at build time and reused on every request (which makes it faster because it can be cached), while in SSR, data is fetched on every request.

What they both have in common is that they can be mixed with Client-side Rendering
wit fetch, Axios, SWR, React Query etc.

The two forms of pre-rendering isn’t an absolute one-or-the-other case; you can choose to use Static Generation or Server-side Rendering, or you can use a hybrid of both. That is, while some parts of your Next.js app uses Static Generation, another can use SSR.

In both cases, Next.js offers special functions to fetch your data. You can use one of the Traditional Approach to Data Fetching in React or you can use the special functions. It’s advisable to use the special functions, not because they’re supposedly special, nor because they’re aptly named (as you’ll see) but because they give you a centralized and familiar data fetching technique that you can’t go wrong with.

The three special functions are:

  1. getStaticProps — used in SG when your page content depends on external data.
  2. getStaticPaths — used in SG when your page paths depends on external data.
  3. getServerSideProps — used in Server-side Rendering.


getStaticProps is a sibling to getStaticPaths and is used in Static Generation. It’s an async function where you can fetch external data, and return it as a prop to the default component in a page. The data is returned as a props object and implicitly maps to the prop of the default export component on the page.

In the example below, we need to map over the accounts and display them, our page content is dependent on external data as we fetched and resolved in getStaticProps.

// accounts get passed as a prop to <AccountsPage/> from getStaticProps()
// Much more like <AccountsPage {...{accounts}} />
export default function AccountsPage({accounts}) { return ( <div> <h1>Bank Accounts</h1> { => ( <div key={}> <p>{account.Description}</p> </div> ))} </div> )
} export async function getStaticProps() { // This is a real endpoint const res = await fetch(''); const accounts = await res.json(); return { props: { accounts: accounts.slice(0, 10), }, };

As you can see, getStaticProps works with Static Generation, and returns a props object, hence the name.


Similar to getStaticProps, getStaticPaths is used in Static Generation but is different in that it is your page paths that is dynamic not your page content. This is often used with getStaticProps because it doesn’t return any data to your component itself, instead it returns the paths that should be pre-rendered at build time. With the knowledge of the paths, you can then go ahead to fetch their corresponding page content.

Think about Next.js pre-rendering your page in the aspect of a dynamic page with regards to Static Generation. For it to do this successfully at build time, it has to know what the page paths are. But it can’t because they’re dynamic and indeterminate, this is where getStaticPaths comes in.

Imagine you have a Next.js app with pages States and state that shows a list of countries in the United States and a single state respectively. You might have a folder structure that looks like:

- pages - index.js - states - index.js # url: /states - [id].js # url /states/[id].js 

You create the [id].js to show a single state based on their id. So, it the page content (data returned from getStaticProps) will be dependent on the page paths (data returned from getStaticPaths).

Let’s create the <States/> components first.

// The states will be passed as a prop from getStaticProps
export default function States({states}) { // We'll render the states here
} export async function getStaticProps() { // This is a real endpoint. const res = await fetch(; const states = await res.json(); // We return states as a prop to <States/> return { props: { states } };

Now let’s create the dynamic page for a single state. It’s the reason we have that [id].js so that we can match the path /states/1, or /states/2 where 1 and 2 are the id in [id].js.

// We start by expecting a state prop from getStaticProps
export default function State({ state }) { // We'll render the states here
} // getStaticProps has a params prop that will expose the name given to the
// dynamic path, in this case, id that can be used to fetch each state by id.
export async function getStaticProps({ params }) { const res = await fetch(${} ); const state = await res.json(); return { props: { state: state[0] } };

If you try to run the code as it is, you’d get the message: Error: getStaticPaths is required for dynamic SSG pages and is missing for /states/[id].

// The state component
// getStaticProps function
// getStaticPaths
export async function getStaticPaths() { // Fetch the list of states const res = await fetch(""); const states = await res.json(); // Create a path from their ids: /states/1, /states/2 ... const paths = => /states/${}); // Return paths, fallback is necessary, false means unrecognize paths will // render a 404 page return { paths, fallback: false };

With the paths returned from getStaticPaths, getStaticProps will be made aware and its params props will be populated with necessary values, like the id in this case.


Absolute Imports

There’s support for absolute import starting from Next.js 9.4 which means you no longer have to import components relatively like:

import FormField from "../../../../../../components/general/forms/formfield"

instead you can do so absolutely like:

import FormField from "components/general/forms/formfield";

To get this to work, you will need a jsconfig.json or tsconfig.json file for JavaScript and TypeScript respectively, with the following content:

{ "compilerOptions": { "baseUrl": "." }

This assumes that the components folder exists at the root of your app, alongside pages, styles, and public.

Experimental ES Features

It is possible to use some experimental features like Nullish coalescing operator (??) and Optional chaining (?.) in your Next.js app.

export default function User({user) { return <h1>{person?.name?.first ?? 'No name'}</h1>


According to the Next.js team, many of the goals they set out to accomplish were the ones listed in The 7 principles of Rich Web Applications, and as you work your way in and deep into the ecosystem, you’d realize you’re in safe hands like many other users who have chosen to use Next.js to power their websites/web applications. Give it a try, if you haven’t, and if you have, keep going.


Introduction to Google Analytics 4

Google Analytics has come a long way since it launched in 2005. Version 4 is the latest iteration. It enables the blending of online and offline user activity into one reporting stream.

Google purchased Urchin, an early day analytics platform, in April 2005. Later that year, Google repurposed Urchin, calling it Google Analytics, to report traffic and conversion details on public websites.

As the internet has evolved, the demand for metrics that track user activity — online and offline — has increased. Google Analytics 4 (previously “App + Web”) supports that demand, reporting the customer journey wherever it occurs.

Google Analytics 4

Google Analytics 4 will not immediately enhance the reporting of most online merchants. Version 4 does help companies that:

  • Have an app;
  • Have a software-as-a-service business model;
  • Are interested in advanced remarketing.

Nonetheless, all merchants should create a Google Analytics 4 property to benefit from the updated reporting and enhanced features that will come in time. The current version 4 is just the beginning.

To create a version 4 property, log in to Google Analytics > Admin [icon] > Create Property.

Screenshot of Google Analytics 4 setup.

To create a version 4 property, log in to Google Analytics > Admin [icon] > Create Property. Click image to enlarge.

Select “Apps and web” as the property type.

Screenshot of a setup screen for Google Analytics 4

Select “Apps and web” as the property type. Click image to enlarge.

Name the property and complete the settings on the page. Then click “Create.”

Screenshot of Google Analytics 4 set up page: name the property

Name the property and complete the settings on the page. Then click “Create.” Click image to enlarge.

On the next page, select your data stream. For most online merchants, it will be “Web.”

Screenshot of choosing the version 4 data stream

The data stream for most online merchants will be “Web.” Click image to enlarge.

Next, provide the “Website URL,” name the stream, and choose the interactions to track automatically. Select all options, even if you presently do not have some of those activities, such as Videos or File Downloads. Then click “Create Stream.”

Screenshot of version 4 setup

Provide the “Website URL,” name the stream, and choose the interactions to track automatically. Click image to enlarge.

The next page includes the option to add a new tag or use an existing one. If you have already tagged your site, click “Use existing on-page tag.” Otherwise, follow the instructions to add a new tag.

Screenshot of Google Analytics 4: assigning a tag

If you have already tagged your site, click “Use existing on-page tag.” Otherwise, follow the instructions to add a new tag. Click image to enlarge.

When selecting “Use existing on-page tag,” follow the instructions for your setup: a standalone Google Analytics tag on your site or via Google Tag Manager.

Screenshot of setting up tags for version 4

Follow the instructions for your setup: a standalone Google Analytics tag on your site or via Google Tag Manager. Click image to enlarge.

The steps above will enable general tracking. The more challenging part in version 4 is setting up Ecommerce. Google has addressed it — with more documentation to come.


To share some advanced capabilities of Google Analytics 4, I am using data from Power My Analytics, which is my SaaS business. Visitors can browse our website and then start a free trial that is activated in our app. This trial signup is “online,” but when a customer converts from a trial to a paid subscription, that activity is offline in our app.

Note that there are no “Views” when setting up Google Analytics 4. The structure is Admin > Account > Property.

Screenshot of account setup for version 4

There are no longer “Views” when setting up Google Analytics 4. The structure is Admin > Account > Property. Click image to enlarge.

Google Analytics 4 contains new links on the left-side navigation. Realtime reporting is still available, along with Acquisition and Conversions. The other navigation links are new but link to similar reporting as before.

Screenshot of the main reporting screen showing the left-side navigation

Version 4 contains new links on the left-side navigation. Realtime reporting is still available, along with Acquisition and Conversions. Click image to enlarge.

Get Acquainted

Click around and get acquainted with the new reporting. Discover new metrics and dimensions. Some of them offer insights that you may have set up with custom tagging, such as “Engaged Sessions.” It is now automated. Expect more user-friendly features in time.