How to build a Marketing Site with React and Strapi

Shada - Mar 8 '22 - - Dev Community

Goal

In this article, we will build a startup site with forms functionality. The startup will be an estate site that helps us find new homes. We will use Strapi for the backend and React for the frontend.

Also, we will have a newsletter form for users who want to receive monthly emails about our products. On signup, the user will also receive a confirmation email that the signup was successful.

An Introduction to Strapi

Strapi is an open-source headless CMS solution that is self-hosted to allow customization and flexibility for your backend API service. One advantage of Strapi is the ease of scaffolding an API to expose endpoints that can be used as micro-services in your frontend. Strapi supports both a RESTful and GraphQL API.

Prerequisites

Before we continue this article, it is pertinent you know;

Strapi’s CLI installation guide requires two things to be installed in your computer:

  • Nodejs: versions between v14 and higher are supported. To use v14, run the commands below in your terminal.
    nvm ls // to see all versions available
    nvm use 14 // to use version 14
    nvm alias default v14 // to make v14 your default version

  • Npm: Only v6 of npm is suported by Strapi. To downgrade to v6, run npm install npm@6.
    Checking Node and Npm versions

  • Postman: We will be using postman to test our Strapi endpoints before connecting it to our React Frontend.

    Backend Setup

We will be using Strapi for our backend setup; hence, we will install and scaffold a Strapi project.

Note: This article is written with the latest version of Strapi(v4.1.2) and tested with Strapi 4.1.2(Node 16 and yarn).

Strapi Setup

To install and scaffold a Strapi project, create a file called Marketing-Site, cd into it, and run either of the following commands below:

    cd Marketing-Site
    npx create-strapi-app@latest backend --quickstart
Enter fullscreen mode Exit fullscreen mode

or

    cd Marketing-Site
    yarn create strapi-app backend --quickstart
Enter fullscreen mode Exit fullscreen mode

From above, we are creating a project called backend inside our Marketing-Site folder and using the --quickstart flag to choose the quickstart installation type.

After installation, run yarn develop to start the new project. The command will open up a tab in your browser with a page to register your new admin. Fill out the form and submit it to log in to your Strapi dashboard.

Strapi’s dashboard

After registering, we will be directed to the admin homepage to set up our backend content and APIs.

Create a Collection

Let us create an Estate collection type. On your admin homepage, click on Content-Type Builder and then Create a new collection type.*

A modal will prompt you to create your content type and enter “Estate” as your Content-Type name because that is what we are building. Click on Continue and add the different fields you want on your website.

Strapi Dashboard

For this tutorial, we only need the Name, Description, Image and Price of our houses.

Strapi Dashboard

After you have selected the fields, click on Finish to leave the modal. There are Save and Publish buttons at the top-right side of our page. Click on Save to save the collection and Publish to publish our collection. You should have something like the image below;

Strapi Dashboard

Populate the Collection

Click on Content Manager at the top-left side of the admin page. This will navigate you to the page where we will populate our database.

Strapi Dashboard

Click on Add new entry to add different houses. We will be needing the name, description, pictures and price of the house so go ahead and populate the collection with your data. After you have done that, click on the publish button on the top-right side of your page to publish the collection.

Now, let’s make the estate route available to consume it in our React frontend. Click on Settings to go to the settings page and click on Roles under Users and Permissions Plugins.

Strapi Dashboard

Next, click on Public and then check the find and findOne options under the Estate dropdown. This will make these two estate routes publicly available.

Strapi Dashboard

Test the Routes with Postman

You can test the routes with Postman to see how and what our Strapi API returns. Below is what the http://localhost:1337/api/estates will return:

Postman Workspace

Frontend Setup

Let’s create our frontend with React, install axios and react router packages, and connect them to Strapi. To spin up our React app, we will first exit or cd out of the backend folder before creating our React app, or we can open up a new terminal and run the commands below inside the Marketing-Site folder:

    npx create-react-app frontend
    cd frontend
    npm i axios react-router-dom --save
    npm start
Enter fullscreen mode Exit fullscreen mode

Below is what our folder structure will look like. I will also explicitly state and explain the pages so you won’t get lost.

Frontend folder structure

I’ve written the code below according to the folder structure. You can copy and paste accordingly. You can also clone the Github repo here to access the CSS styling.

// frontend/src/hooks/useFetch.js
Enter fullscreen mode Exit fullscreen mode
    import { useState, useEffect } from 'react';
    import axios from 'axios';
    export default function useFetch(url) {
        const [ estate, setEstate ] = useState(null);
        const [ error, setError ] = useState(null);
        const [ loading, setLoading ] = useState(true);

        useEffect(
            () => {
                const fetchData = async () => {
                    setLoading(true);
                    try {
                        const res = await axios.get(url);
                        setEstate(res.data.data);
                        setLoading(false);
                    } catch (error) {
                        setError(error);
                        setLoading(false);
                    }
                };
                fetchData();
            },
            [ url ]
        );

        return { estate, error, loading };
    }
Enter fullscreen mode Exit fullscreen mode
// frontend/src/pages/about/About.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import { useParams, Link } from 'react-router-dom';
    import classes from './about.module.css';
    import useFetch from '../../hooks/useFetch';

    function AboutPage() {
        const { id } = useParams();
        const { loading, error, estate } = useFetch(`http://localhost:1337/api/estates/${id}?populate=*`);
        if (loading) return <p> Loading... </p>;
        if (error) return <p> Error :( </p>;

        return (
            <article className={classes.aboutPage}>
                <h2>More Description</h2>
                <hr />
                <section className={classes.aboutBoard}>
                    <h2>{estate.attributes.name}</h2>
                    <div className={classes.aboutDescription}>
                        <div className={classes.aboutImgContainer}>
                            {estate.attributes.image.data ? (
                                estate.attributes.image.data.map((pic) => (
                                    <img
                                        src={`http://localhost:1337${pic.attributes.url}`}
                                        alt="img"
                                        key={pic.attributes.id}
                                    />
                                ))
                            ) : (
                                <img
                                    src={`http://localhost:1337${estate.attributes.image.data.attributes.url}`}
                                    alt="img"
                                />
                            )}
                        </div>
                        <div>
                            <h3>{estate.attributes.price}</h3>
                            <p>{estate.attributes.description}</p>
                            <Link
                                to={'/'}
                                style={{
                                    textDecoration: 'none',
                                    background: 'black',
                                    color: 'white',
                                    border: '1px solid black',
                                    padding: '5px 10px'
                                }}
                            >
                                {'< Back to Home'}
                            </Link>
                        </div>
                    </div>
                </section>
            </article>
        );
    }
    export default AboutPage;
Enter fullscreen mode Exit fullscreen mode
// frontend/src/pages/estates/Estates.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import { Link } from 'react-router-dom';
    import useFetch from '../../hooks/useFetch';
    import classes from './estates.module.css';

    export default function Estatepage() {
        const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
        if (loading) return <p> Loading... </p>;
        if (error) return <p> Error :( </p>;

        return (
            <div className={classes['estates']}>
                <section>
                    <h2>Available Houses</h2>
                    <hr className={classes['horizontal-rule']} />
                    {estate.map((house) => (
                        <article className={classes['article']} key={house.id}>
                            <h2>{house.attributes.name}</h2>
                            <section className={classes['article-description']}>
                                <img
                                    src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
                                    alt="img"
                                />
                                <div>
                                    <p>{house.attributes.price}</p>
                                    <p>{house.attributes.description}</p>
                                    <Link to={`${house.id}`}>See More...</Link>
                                </div>
                            </section>
                        </article>
                    ))}
                </section>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode
// frontend/src/pages/home/Home.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import { Link } from 'react-router-dom';
    import useFetch from '../../hooks/useFetch';
    import classes from './home.module.css';

    export default function Homepage() {
      const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
      if (loading) return <p> Loading... </p>;
      if (error) return <p> Error :( </p>;

      return (
        <div className={classes['home']}>
          <section>
            <h2>Welcome to our Estate</h2>
            <hr className={classes['horizontal-rule']} />
            <p>We help you find your new home</p>

          <form className={classes["home-form"]}>
            <h5>Interested in joining our Newsletter</h5>
            <h6>Sign up with your email below</h6>

            <label htmlFor="email">
              Email Address:
              <input type="email" />
            </label>
            <button>Signup</button>
          </form>
            {estate.splice(0, 2).map((house) => (
              <article className={classes['home-article']} key={house.id}>
                <h2>{house.attributes.name}</h2>
                <section className={classes['home-article-description']}>
                  <img
                    src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
                    alt="img"
                  />
                  <div>
                    <p>{house.attributes.price}</p>
                    <p>{house.attributes.description}</p>
                    <Link to={`estates/${house.id}`}>See More...</Link>
                  </div>
                </section>
              </article>
            ))}
          </section>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode
// frontend/src/pages/nav/Nav.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import { Link } from 'react-router-dom';
    import classes from './nav.module.css';

    export default function NavHeader() {
      return (
        <div className={classes.navBar}>
          <h1>My Estate</h1>
          <nav className={classes.navLink}>
            <ul>
              <Link to="/" style={{ textDecoration: 'none' }}>
                <li>Home</li>
              </Link>
              <Link to="estates" style={{ textDecoration: 'none' }}>
                <li>Estates</li>
              </Link>
            </ul>
          </nav>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode
// frontend/src/App.js
Enter fullscreen mode Exit fullscreen mode
    import React, { Suspense } from 'react';
    import { Routes, Route } from 'react-router-dom';
    import Nav from './pages/nav/Nav';
    import Home from './pages/home/Home';

    const About = React.lazy(() => import('./pages/about/About'));
    const Estates = React.lazy(() => import('./pages/estates/Estates'));

    export default function App() {
        return (
            <div>
                <Nav />
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route
                        path="estates"
                        element={
                            <Suspense fallback={<p>Loading...</p>}>
                                <Estates />
                            </Suspense>
                        }
                    />
                    <Route
                        path="estates/:id"
                        element={
                            <Suspense fallback={<p>Loading...</p>}>
                                <About />
                            </Suspense>
                        }
                    />
                </Routes>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

Inside App.js file above, we implemented a React 18 feature called Suspense API.

According to React’s official page, “Suspense is a new feature that lets you also use <Suspense> to declaratively “wait” for anything else, including data. It’s a mechanism for data-fetching libraries to communicate to React that the **data a component is reading is not ready yet. React can then wait for it to be ready and update the UI”.

// frontend/src/index.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter } from 'react-router-dom';
    import App from './App';

    ReactDOM.render(
        <React.StrictMode>
            <BrowserRouter>
                <App />
            </BrowserRouter>
        </React.StrictMode>,
        document.getElementById('root')
    );
Enter fullscreen mode Exit fullscreen mode

Here’s what the files above do:

  1. We separated our fetch hook by writing it in the useFetch.js file. By doing this, we wouldn’t have to rewrite the same logic every time. Instead, we will invoke it in the components we need it.
  2. We imported it into our Home and Estates components. After our Axios fetch was successful, we mapped through the returned array to display the data. On clicking on each data, we will be redirected to the About page. This page shows the full description of the house with the different available pictures, prices, etc.
  3. Nav.js contains the static nav header on our website that houses the website’s name and the Estate and Home anchor links.
  4. Also, Home.js has a form element. We will be using this form for our newsletter signup, which is one of the main focus of this article.
  5. To run the React app and see the result, open up your terminal and run any of the commands below;
    // Yarn 
    yarn start
Enter fullscreen mode Exit fullscreen mode
    //Npm
    npm start
Enter fullscreen mode Exit fullscreen mode

Estate Homepage

Estate Description page

Now, we can easily change and add content with Strapi by simply editing any of the current Collection Types or creating new entries.

We have completed the frontend setup and integration. Next, we’ll work on our newsletter integration.

SendGrid Setup

There are so many email providers out there like Mailchimp, MailerLite, Sendinblue, and so on. But for this article, we will be using an email provider called SendGrid.

To set up a SendGrid service, we will first create a SendGrid account. This is because we need to connect SendGrid to Strapi through the SendGrid API. Head over to SendGrid to sign up and create your account.

SendGrid Welcome page

After logging into your dashboard, click on the Settings dropdown on the left side of the dashboard and click on Sender Authentication. Proceed to create a new sender and make sure to verify the email address too.

SendGrid Dashboard

Next, we will create our API key. On the left side of the dashboard, click on the Settings dropdown again and click on API keys. Click on create API key, give it a name and copy your API key.

Note: Make sure to copy your API key and store it somewhere safe because SendGrid won’t show it to you again. You can always create a new key though if you loose or forget where you stored it.

SendGrid Dashboard

Next, we will go into our backend folder and run any of the commands below to download strapi email provider plugin.

    // using yarn
    yarn add @strapi/provider-email-sendgrid --save

    // using npm
    npm install @strapi/provider-email-sendgrid --save
Enter fullscreen mode Exit fullscreen mode

After we have successfully downloaded the plugin, we’ll set it up in our backend folder. In the config folder, create a new file called plugins.js and paste the code below:

// config/plugins.js
Enter fullscreen mode Exit fullscreen mode
    module.exports = ({ env }) => ({
        email: {
            provider: 'sendgrid',
            providerOptions: {
                apiKey: env('SENDGRID_API_KEY')
            },
            settings: {
                defaultFrom: 'myemail@protonmail.com',
                defaultReplyTo: 'myemail@protonmail.com'
            }
        }
    });
Enter fullscreen mode Exit fullscreen mode

Replace the settings default emails with your SendGrid verified email. Also, in your .env file, add your SendGrid API key.

SENDGRID_API_KEY=SG.5hoLikrVQXudcUtgaV6n6g.aKttCp***********************************
Enter fullscreen mode Exit fullscreen mode

After that, head over to the api folder inside src folder and create a new folder called subscribe. Inside our subscribe folder, we will also create two extra folders: config and controllers. In our config folder, create a new routes.json file and add the code below.

// src/api/subscribe/config/routes.json
Enter fullscreen mode Exit fullscreen mode
    {
        "routes": [
            {
                "method": "POST",
                "path": "/email",
                "handler": "email.send",
                "config": {
                    "policies": []
                }
            }
        ]
    }
Enter fullscreen mode Exit fullscreen mode

Then, in our controllers folder create an email.js file, and add the following code

// src/api/subscribe/controllers/email.js
Enter fullscreen mode Exit fullscreen mode
    module.exports = {
        send: async (ctx) => {
            let options = ctx.request.body;

            await strapi.plugins.email.services.email.send({
                to: options.to,
                from: 'yourmail@gmail.com',
                replyTo: 'yourmail@gmail.com',
                subject: options.subject,
                text: options.html
            });

            ctx.send('Email sent!');
        }
    };
Enter fullscreen mode Exit fullscreen mode

We will now test our configuration in Postman and see what we get. Before that, make sure you make the email route publicly available in your Strapi admin settings.

(Settings > Users and Permissions Plugin > Roles > Public > Email)

Strapi Dashboard

Then, in our postman, let’s test our API to see if it works.

Postman Workspace

We can see we got a status of 200 meaning the request was sent successfully. Log in to your email account to see the test message.

Finally, We will now integrate our Strapi subscribe functionality into our React app.

Head over to your frontend folder. Under the hooks folder where we created our useFetch.js file, create a new file called usePost.js. We will be putting our POST logic here; then, we will import it into our Home file.

// frontend/src/hooks/usePost.js
Enter fullscreen mode Exit fullscreen mode
    import { useState } from 'react';
    import axios from 'axios';
    const usePost = (url) => {
        const [ signup, setSignup ] = useState('');
        const [ signupError, setError ] = useState(null);
        const [ signupMsg, setSignupMsg ] = useState('');
        const [ signupLoading, setSignupLoading ] = useState(true);

        const handleChange = (e) => {
            setSignup(e.target.value);
        };

        const handleSignup = (e) => {
            e.preventDefault();
            let userData = {
                to: signup,
                from: 'chimezieinnocent39@gmail.com',
                replyTo: 'chimezieinnocent39@gmail.com',
                subject: 'Thanks for signing up',
                html:
                    "<h3>Hi!,</h3> <p>You've been subscribed to our primary newsletter. You can expect to receive an email from us every few weeks, sharing the new things that we've published and new houses to check out. Occasionally, We'll share unique newsletter-only content as well</p><p>Thanks for choosing us!</p>"
            };
            axios
                .post(url, userData)
                .then((res) => {
                    setSignup(res);
                    setSignupMsg(true);
                    setSignupLoading(false);
                })
                .catch((signupError) => {
                    setError(signupError);
                    setSignupLoading(false);
                });
        };

        return { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup };
    };
    export default usePost;
Enter fullscreen mode Exit fullscreen mode

Let us import it in our Home file below:

// frontend/src/pages/home/Home.js
Enter fullscreen mode Exit fullscreen mode
    import React from 'react';
    import { Link } from 'react-router-dom';
    import useFetch from '../../hooks/useFetch';
    import usePost from '../../hooks/usePost';
    import classes from './home.module.css';

    export default function Homepage() {
        const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
        const { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup } = usePost(
            'http://localhost:1337/api/email'
        );

        if (loading && signupLoading) return <p> Loading... </p>;
        if (error) return <p> Error :( </p>;

        return (
            <div className={classes['home']}>
                <section>
                    <h2>Welcome to our Estate</h2>
                    <hr className={classes['horizontal-rule']} />
                    <p>We help you find your new home</p>
                    <form className={classes['home-form']} onSubmit={handleSignup}>
                        <h5>Interested in joining our Newsletter</h5>
                        <h6>Sign up with your email below</h6>
                        <label htmlFor="email">
                            {signupError ? <p> {signupError} </p> : null}
                            Email Address:
                            <input type="email" name="email" value={signup} onChange={handleChange} />
                            {signupMsg ? <p> Thanks for signing up!</p> : null}
                        </label>
                        <button>Signup</button>
                    </form>
                    {estate.splice(0, 2).map((house) => (
                        <article className={classes['home-article']} key={house.id}>
                            <h2>{house.attributes.name}</h2>
                            <section className={classes['home-article-description']}>
                                <img
                                    src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
                                    alt="img"
                                />
                                <div>
                                    <p>{house.attributes.price}</p>
                                    <p>{house.attributes.description}</p>
                                    <Link to={`estates/${house.id}`}>See More...</Link>
                                </div>
                            </section>
                        </article>
                    ))}
                </section>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

Go ahead and test your app.

Conclusion

We have seen how to use Strapi with React to build a startup website. We’ve also seen how to Integrate SendGrid with Strapi and React to create a newsletter email form.

Lastly, we have seen how to implement one of React’s new features— React Suspense — and what it does. I hope you understood what we did and can now implement same in your projects.

https://github.com/Vic-Orlands/Strapi-React

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .