How to build a productivity tips application with Render, Remix, and Strapi

Shada - May 30 '22 - - Dev Community

In this tutorial, you will learn how to build a productivity tips app using Render, Remix and Strapi.

The productivity app will have the following:

  • A landing page that describes the app,
  • A /tips page to view all the productivity tips in the database, and
  • A page that displays the other details of a productivity tip.

This productivity tips web app is an app that displays a list of tips to improve productivity. This app works by passing tips and information about them from the appโ€™s database to the frontend.

You can get the final project in the Strapi repo and the Remix repo.

Prerequisites

To follow this article, you will need knowledge of the following:

  • JavaScript
  • Basic Remix
  • Basic Strapi

You also need the following installed:

Why Remix?

Remix is a web framework that allows you to concentrate on the user interface, while taking care of the speed and responsiveness of your application. Remix uses nested routing, which promotes effective interaction between a frontend and one or more backends.

The productivity tips application consists of both a frontend using Remix and a backend using Strapi. Effective communication between ends of the application leads to a better and faster user experience.

Why Strapi?

Strapi is an open-source headless CMS that allows you to build GraphQL and RESTful APIs. A headless CMS allows developers to easily create APIs that can be integrated with any frontend.

A CMS (Content Management System) is a software that allows users to create and maintain contents for a web application. A headless CMS is just like a regular CMS, but it does not have a frontend that people interact with.

With Strapi you can create an API that is the following:
  • Easily integrated with any frontend,
  • Customizable,
  • Easy to maintain, and
  • Easily developed.

Why Render?

Render is a cloud service that allows you to deploy and run your websites, apps, and much more. With render you can set up and deploy your entire project with a single configuration file. This configuration file acts as a blueprint for setting up your project.

To get your Remix website and the Strapi app running, you need to deploy them on a cloud. After deploying the applications, you can access them using the domain name provided by the cloud provider, or you can add a custom domain name.

The Strapi Application

Before you start, make sure you have a GitHub Account, then follow the steps below:

  1. Open this repo in your browser.
  2. Click Use this template button to copy the repo to your account. The โ€œUse this templateโ€ button is the green button
  3. Click Create repository from template after filling any name as the repo name (you don't need to include all branches).
  4. Clone your copy of the repo to your local machine.
  5. Navigate to the local repo in your terminal.
  6. Run yarn install to install the dependencies.
  7. Run cp .env.example .env to copy the environment variables from .env.example to .env.
  8. Run yarn develop to start the strapi application in development mode.

After setting up and starting the Strapi application, you need to do a few things:

  1. Open the http://localhost:1337/admin page in your browser.
  2. Create an admin account in the admin page.

Strapi Welcome Page

The Strapi application in the repo already has the productivity tip's Content Type setup. You can see it by navigating to Content Type Builder -> Tip.

Content-Type Builder Page

Before using your API, you need to add sample productivity tips:
  1. Navigate to Content Manager.
  2. Click Create New Entry.
  3. Fill the boxes and choose an author.
  4. Click Save.

You can also add more tips to your application before continuing if you want.
The image above shows where to choose an author

Now, you are done with setting up the Strapi app. You can begin to create the frontend.

The Remix App

To create the remix app, follow the below steps:

  1. Fork this repository to your GitHub account.
  2. Clone the forked repository to your local machine.
  3. cd into the local repository
  4. Run npm install to install its dependencies.

This repository is split into five (5) branches, each showing the steps taken to build the application.

Step 1: Initialize the Remix App

The link to this step is available here. In this branch, the remix application was created and initialized. When you run npm run dev in this branch, the web application looks like the below:

A sample screenshot

Step 2: Add Styling

The link to this step is available here.

To switch to this branch, use git checkout step-2. The changes made in this step were to the app/root.tsx file:

1. Import the Pico CSS library.
    import {
      Link,
      Links,
      LiveReload,
      Meta,
      Outlet,
      Scripts,
      ScrollRestoration,
    } from "remix";

    export function links () {
      return [{
        rel: "stylesheet",
        href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
      }]
    }

    export function meta() {
      return { title: "Productivity Tips" };
    }

    export default function App() {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width,initial-scale=1" />
            <Meta />
            <Links />
          </head>
          <body>
            <nav style={{marginLeft: 10}}>
              <h1>
                <Link to="/" style={{color: "var(--h1-color)"}}>
                  Productivity Tips
                </Link>
              </h1>
            </nav>
            <Outlet />
            <ScrollRestoration />
            <Scripts />
            <LiveReload />
          </body>
        </html>
      );
    }
Enter fullscreen mode Exit fullscreen mode
2. Change the page title
    import {
      Link,
      Links,
      LiveReload,
      Meta,
      Outlet,
      Scripts,
      ScrollRestoration,
    } from "remix";

    export function links () {
      return [{
        rel: "stylesheet",
        href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
      }]
    }

    export function meta() {
      return { title: "Productivity Tips" };
    }

    export default function App() {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width,initial-scale=1" />
            <Meta />
            <Links />
          </head>
          <body>
            <nav style={{marginLeft: 10}}>
              <h1>
                <Link to="/" style={{color: "var(--h1-color)"}}>
                  Productivity Tips
                </Link>
              </h1>
            </nav>
            <Outlet />
            <ScrollRestoration />
            <Scripts />
            <LiveReload />
          </body>
        </html>
      );
    }
Enter fullscreen mode Exit fullscreen mode
3. Add "Productivity Tips" header and link it to /
    import {
      Link,
      Links,
      LiveReload,
      Meta,
      Outlet,
      Scripts,
      ScrollRestoration,
    } from "remix";

    export function links () {
      return [{
        rel: "stylesheet",
        href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
      }]
    }

    export function meta() {
      return { title: "Productivity Tips" };
    }

    export default function App() {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width,initial-scale=1" />
            <Meta />
            <Links />
          </head>
          <body>
            <nav style={{marginLeft: 10}}>
              <h1>
                <Link to="/" style={{color: "var(--h1-color)"}}>
                  Productivity Tips
                </Link>
              </h1>
            </nav>
            <Outlet />
            <ScrollRestoration />
            <Scripts />
            <LiveReload />
          </body>
        </html>
      );
    }
Enter fullscreen mode Exit fullscreen mode

When you run this program, the web app will look the below:
A Sample Screenshot

Step 3: Edit the Content [Add the Tips]

The link to this step can be found here.
To switch to this branch use the git checkout step-3 command.

The file changed in this step is src/index.tsx, and the following were the changes made:

1. Add a paragraph that describes the app:

    import { Link } from "remix";

    export default function Index() {
      return (
        <main className="container">
          <p>
            Over time everyone develops a Swiss army knife of tips, tricks,
            and hacks to boost productivity. At Render, I created a
            #productivity-tips Slack channel for anyone to share their best
            productivity boosters with everyone on the team. Using 
            <a href="https://strapi.io">Strapi</a> and 
            <a href="https://remix.run">Remix</a>, we made a little web app to
            catalog all of these tips and share them with others. ๐Ÿค“
          </p>
          <Link to="/tips">๐Ÿ‘‰ Productivity Tips</Link>
        </main>
      );
    }
Enter fullscreen mode Exit fullscreen mode

2. Add a link to /tips

    import { Link } from "remix";

    export default function Index() {
      return (
        <main className="container">
          <p>
            Over time everyone develops a Swiss army knife of tips, tricks,
            and hacks to boost productivity. At Render, I created a
            #productivity-tips Slack channel for anyone to share their best
            productivity boosters with everyone on the team. Using 
            <a href="https://strapi.io">Strapi</a> and 
            <a href="https://remix.run">Remix</a>, we made a little web app to
            catalog all of these tips and share them with others. ๐Ÿค“
          </p>
          <Link to="/tips">๐Ÿ‘‰ Productivity Tips</Link>
        </main>
      );
    }
Enter fullscreen mode Exit fullscreen mode

/tips is a page that will be created in the next step. This page holds a list of all the tips stored in the Strapi backend.

When you run the program at this step, the web application looks like the below:

A Sample Screenshot

Step 4: Create New Files

The link to this step can be found here

The following are new files created in this step:

  • app/routes/tips.jsx file, which is the template that shows both the content of the list of tips and individual tips details.
    import { Outlet } from "remix";

    export default function TipsRoute() {
      return (
        <main className="container">
          <Outlet />
        </main>
      );
    }
Enter fullscreen mode Exit fullscreen mode
  • index.jsx file inside a new app/routes/tips folder. This component will be rendered in place of the <Outlet /> above.
    import { Link, useLoaderData } from "remix";
    import { checkStatus, checkEnvVars } from "~/utils/errorHandling";

    export async function loader () {
      checkEnvVars();

      const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips?populate=*`, {
        method: "GET",
        headers: {
          "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
          "Content-Type": "application/json"
        }
      });

      // Handle HTTP response code < 200 or >= 300
      checkStatus(res);

      const data = await res.json();

      // Did Strapi return an error object in its response?
      if (data.error) {
        console.log('Error', data.error)
        throw new Response("Error getting data from Strapi", { status: 500 })
      }

      return data.data;
    }

    export default function Tips() {
      const tips = useLoaderData();

      return (
        <ul>
          {tips.map((tip) => (
            <li key={tip.attributes.Slug}>
              <Link to={tip.attributes.Slug}>{tip.attributes.Name}</Link>
            </li>
          ))}
        </ul>
      );
    }
Enter fullscreen mode Exit fullscreen mode
  • errorHandling.js inside a new app/utils folder. The errorHandling.js folder provides the checkEnvVars() and checkStatus folder that the app.routes/tips/index.jsx folder.
    // Custom error class for errors from Strapi API
    class APIResponseError extends Error {
        constructor(response) {
            super(`API Error Response: ${response.status} ${response.statusText}`);
        }
    }

    export const checkStatus = (response) => {
        if (response.ok) {
            // response.status >= 200 && response.status < 300
            return response;
        } else {
            throw new APIResponseError(response);
        }
    }

    class MissingEnvironmentVariable extends Error {
        constructor(name) {
            super(`Missing Environment Variable: The ${name} environment variable must be defined`);
        }
    }

    export const checkEnvVars = () => {
        const envVars = [
            'STRAPI_URL_BASE',
            'STRAPI_API_TOKEN'
        ];

        for (const envVar of envVars) {
            if (! process.env[envVar]) {
                throw new MissingEnvironmentVariable(envVar)
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

In the index.jsx file, the useLoaderData() function calls the loader function on lines 4-27. the loader function is helpful for separating the program that interacts with APIs, from the view.

    import { Link, useLoaderData } from "remix";
    import { checkStatus, checkEnvVars } from "~/utils/errorHandling";

    export async function loader () {
      checkEnvVars();

      const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips?populate=*`, {
        method: "GET",
        headers: {
          "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
          "Content-Type": "application/json"
        }
      });

      // Handle HTTP response code < 200 or >= 300
      checkStatus(res);

      const data = await res.json();

      // Did Strapi return an error object in its response?
      if (data.error) {
        console.log('Error', data.error)
        throw new Response("Error getting data from Strapi", { status: 500 })
      }

      return data.data;
    }

    export default function Tips() {
      const tips = useLoaderData();

      return (
        <ul>
          {tips.map((tip) => (
            <li key={tip.attributes.Slug}>
              <Link to={tip.attributes.Slug}>{tip.attributes.Name}</Link>
            </li>
          ))}
        </ul>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Before running the application, you need to complete the following steps:

  1. Open the Strapi dashboard.
  2. Navigate to Settings --> API Tokens --> Create new API, to create an API token A sample screenshot
  3. Click Save after filling the necessary fields. The Save button is at the top-right
  4. Copy the generated API token. A sample screenshot
  5. Copy the new contents of the .env.example file to .env.
    STRAPI_URL_BASE=http://localhost:1337
    STRAPI_API_TOKEN=a-secret-token-from-the-strapi-admin-gui
Enter fullscreen mode Exit fullscreen mode
  1. Replace a-secret-token-from-the-strapi-admin-gui with the API token, then click Save. >If you are using windows, you might need to change the STRAPI_URL_BASE's value to http://127.0.0.1:1337.

After running your application with the npm run dev command, the web application should look like the below:

A sample screenshot

a reference to the tips in the Strapi backend

Step 5: Define Dynamic Routes and Style

Here's the link to this step

In this step, the following files are created:

  • app/routes/tips/$tipId.jsx. Remix uses files that begin with a dollar sign to define dynamic URL routes.
  • app/styles/tip.css. This file holds contains the styling for the grid of screenshot images

In the $tipId.jsx file, the following is what happens:

  1. Import the tip.css file into the $tipId.jsx file.
    import { useLoaderData, Link } from "remix";
    import { checkStatus, checkEnvVars } from "~/utils/errorHandling";

    import stylesUrl from "~/styles/tip.css";

    export function links () {
      return [{ rel: "stylesheet", href: stylesUrl }];
    }


    export function meta ({ data }) {
      return {
        title: data.attributes.Name
      }
    }

    export async function loader ({ params }) {
      ...
Enter fullscreen mode Exit fullscreen mode
  1. Create the TipRoute component that is exported by default.
    ...
    export default function TipRoute() {
      const tip = useLoaderData();

      return (
        <div>
          <Link to="/tips" style={{ textDecoration: 'none' }}>โ† back to list</Link>
          <hgroup>
            <h2>{tip.attributes.Name}</h2>
            <h3>by {tip.attributes.Author.data?.attributes.firstname ?? 'an unknown user'}</h3>
          </hgroup>

          <p>
            {tip.attributes.Description}
          </p>
          <div className="grid">
            {tip.attributes.Screenshots.data.map((s) => (
              <div key={s.attributes.hash}>
                <img
                  src={s.attributes.formats.thumbnail.url}
                  alt={tip.attributes.Name + ' screenshot'}
                />
              </div>
            ))}
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode
  1. Create a loader function for useLoaderData() hook in the TipRoute component.
    import { useLoaderData, Link } from "remix";
    import { checkStatus, checkEnvVars } from "~/utils/errorHandling";

    import stylesUrl from "~/styles/tip.css";

    export function links () {
      return [{ rel: "stylesheet", href: stylesUrl }];
    }

    export function meta ({ data }) {
      return {
        title: data.attributes.Name
      }
    }

    export async function loader ({ params }) {
      checkEnvVars();

      const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips`
        + `?populate=*&filters[Slug]=${params.tipId}`, {
        method: "GET",
        headers: {
          "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
          "Content-Type": "application/json"
        }
      })

      // Handle HTTP response code < 200 or >= 300
      checkStatus(res);

      const data = await res.json();

      // Did Strapi return an error object in its response?
      if (data.error) {
        console.log('Error', data.error)
        throw new Response("Error getting data from Strapi", { status: 500 })
      }

      // Did Strapi return an empty list?
      if (!data.data || data.data.length === 0) {
        throw new Response("Not Found", { status: 404 });
      }


      const tip = data.data[0];

      // For a Tip with no screenshot, replace API returned null with an empty array
      tip.attributes.Screenshots.data = tip.attributes.Screenshots.data ?? [];

      // Handle image URL being returned as just a path with no scheme and host.
      // When storing media on the filesystem (Strapi's default), media URLs are
      // return as only a URL path. When storing media using Cloudinary, as we do
      // in production, media URLs are returned as full URLs.
      for (const screenshot of tip.attributes.Screenshots.data) {
        if (!screenshot.attributes.formats.thumbnail.url.startsWith('http')) {
          screenshot.attributes.formats.thumbnail.url = process.env.STRAPI_URL_BASE +
            screenshot.attributes.formats.thumbnail.url;
        }
      }
      return tip;
    }
    ...
Enter fullscreen mode Exit fullscreen mode

In the loader function, the following happens:

  1. Use checkEnvVars() function to check if all the environment variables needed are present.
  2. Make a request to the http://localhost:1337/api/tips route (The URL contains parameters that Strapi uses to specify the request).
  3. Use the checkStatus() function to check that the http status is alright ( between 200 to 299 ).
  4. Check if strapi returned an error in its response. Sometimes, Strapi responds with an error object while having an OK http status.
  5. Check if strapi returned an empty list. If there's no productivity tip at the id, Strapi returns an empty list.
  6. Handle routing for the images in a tip. Strapi uses the local filesystem when it is storing file uploads, and uses a cloud server. When it is from the development server, Strapi doesn't respond with the URL to the file, it only responds with its local path.

When you run the application at this point, the http://localhost:3000/tips/tip page should look like the below:

Deploy to Render

Before deploying your project to the cloud, you need to follow the below steps:

  1. Open the strapiconf2022-workshop-strapi repo.
  2. Change the value of the repos in the /render.yaml file to URL of your remote strapi and remix repo ( In your case it might be https://github.com/your-username/strapiconf2022-workshop-strapi and https://github.com/your-username/strapiconf2022-workshop-remix ).
    services:
      - type: web
        name: productivity-tips-api
        env: node
        plan: free
        # Update the following line with your Strapi GitHub repo
        repo: https://github.com/render-examples/strapiconf2022-workshop-strapi
        branch: main
        buildCommand: yarn install && yarn build
        startCommand: yarn start
        healthCheckPath: /_health
        envVars:
          - key: NODE_VERSION
            value: ~16.13.0
          - key: NODE_ENV
            value: production
          - key: CLOUDINARY_NAME
            sync: false
          - key: CLOUDINARY_KEY
            sync: false
          - key: CLOUDINARY_SECRET
            sync: false
          - key: DATABASE_URL
            fromDatabase:
              name: strapi
              property: connectionString
          - key: JWT_SECRET
            generateValue: true
          - key: ADMIN_JWT_SECRET
            generateValue: true
          - key: API_TOKEN_SALT
            generateValue: true
          - key: APP_KEYS
            generateValue: true

      - type: web
        name: productivity-tips-web
        env: node
        plan: free
        # Update the following line with your Remix GitHub repo
        repo: https://github.com/render-examples/strapiconf2022-workshop-remix
        branch: step-5
        buildCommand: npm install && npm run build
        startCommand: npm start
        envVars:
          - key: STRAPI_URL_BASE
            fromService:
              type: web
              name: productivity-tips-api
              envVarKey: RENDER_EXTERNAL_URL

    databases:
      - name: strapi
        plan: free # This database will expire 90 days after creation
Enter fullscreen mode Exit fullscreen mode
  1. Save the changes, and push to the remote repository.
    $ git add render.yaml
    $ git commit
    $ git add
Enter fullscreen mode Exit fullscreen mode
  1. Login to the Render dashboard.
  2. Click on New and select Blueprint.
  3. Add the repository that the render application can find the render.yaml file. In this case, the strapiconf2022-workshop-strapi repo.

A screenshot

When you select the repo, you will be prompted to add the following details:

  • The Service Group Name. This is a unique name that is used to identify your project within your account.
  • The CLOUDINARY_NAME
  • The CLOUDINARY_KEY
  • The CLOUDINARY_SECRET

A screenshot

To get the value for the last three fields above, you need to do the following:

  1. Login to the Cloudinary dashboard. A screenshot
  2. Click Start configuring. A screenshot
  3. Copy the values of the following fields:

    • cloud_name
    • api_key
    • api_secret A screenshot
  4. Paste the copied values in the following respectively:

    • The CLOUDINARY_NAME
    • The CLOUDINARY_KEY
    • The CLOUDINARY_SECRET

After filling the fields, click Apply
A screenshot Then you wait for Render to deploy your application.

Conclusion

Once your application has been deployed, you can see the address of your deployed application by navigating to the dashboard, then select:

  • productivity-tips-web for the Remix app
  • productivity-tips-api for the Strapi app

After selecting any of the above, you will see the domain name that anyone can use to access your web application.

To get the application up and running, you need to do the following:

  1. Navigate to the Strapi application's admin dashboard ( at /admin ).
  2. Navigate to Settings --> API Tokens --> Create new API token
  3. Copy the generated API token.
  4. Open the productivity-tips-web service
  5. Navigate to Environment and click Add Environment Variables
  6. Set the key as STRAPI_API_TOKEN and paste the generated API token
  7. Click on Save Changes to redeploy your application.

After setting up your application, you can now add productivity tips to your application from the Strapi backend, and you can now fully use the web application.

To further your knowledge, be sure to check out the following links:

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