How to build a productivity tips application with Render, Remix, and Strapi
Published: 2022-05-30 10:22:00

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](https://github.com/TheGhoulRe/strapiconf2022-workshop-strapi) and [the Remix repo](https://github.com/TheGhoulRe/strapiconf2022-workshop-remix).

## Prerequisites

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

- JavaScript
- Basic Remix
- Basic Strapi

You also need the following installed:

- [Git](https://github.com/)
- [NodeJS](https://nodejs.org/)
- [Yarn](https://yarnpkg.com/)
- [Npm](https://www.npmjs.com/)

## 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.](https://remix.run/) 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.](https://strapi.io) 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.](https://render.com) 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](https://render.com/docs/custom-domains).

## The Strapi Application

Before you start, make sure you have a [GitHub Account](https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F&source=header-home), then follow the steps below:

1. [Open this repo](https://github.com/render-examples/strapiconf2022-workshop-strapi) 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](https://www.dropbox.com/s/p9crs8flfs0nho9/Screenshot91.png?raw=1)
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](https://www.dropbox.com/s/1w7yvrplk2noead/Screenshot92.png?raw=1)

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](https://www.dropbox.com/s/x1porvjmyw1fcwp/Screenshot93.png?raw=1)

##### 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](https://www.dropbox.com/s/hojhqm1mf83utkn/Screenshot96.png?raw=1)

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](https://github.com/render-examples/strapiconf2022-workshop-remix) 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.](https://github.com/render-examples/strapiconf2022-workshop-remix) 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](https://www.dropbox.com/s/qd8142jj6992a00/Screenshot97.png?raw=1)

## Step 2: Add Styling

[The link to this step is available here.](https://github.com/render-examples/strapiconf2022-workshop-remix/tree/step-2)

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](https://picocss.com/docs/) library.

```js
    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>
      );
    }
```

##### 2. Change the page title

```js
    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>
      );
    }
```

##### 3. Add "Productivity Tips" header and link it to `/`

```js
    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>
      );
    }
```
When you run this program, the web app will look the below:
![A Sample Screenshot](https://www.dropbox.com/s/515hl0km4aon22j/Screenshot99.png?raw=1)


## Step 3: Edit the Content [Add the Tips]

[The link to this step can be found here](https://github.com/render-examples/strapiconf2022-workshop-remix/tree/step-3).
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:

```js
    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>
      );
    }
```

#### 2. Add a link to `/tips`
```js
    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>
      );
    }
```
`/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](https://www.dropbox.com/s/tbs4ke0also4p9d/Screenshot100.png?raw=1)

## Step 4: Create New Files
[The link to this step can be found here](https://github.com/render-examples/strapiconf2022-workshop-remix/tree/step-4)

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.
```js
    import { Outlet } from "remix";
    
    export default function TipsRoute() {
      return (
        <main className="container">
          <Outlet />
        </main>
      );
    }
```

- `index.jsx` file inside a new `app/routes/tips` folder. This component will be rendered in place of the `<Outlet />` above.

```js
    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>
      );
    }
```

- `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.


```js    
    // 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)
            }
        }
    }
```

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. 

```js
    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>
      );
    }
```
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](https://www.dropbox.com/s/8ue8l1gpaomnyh6/Screenshot101.png?raw=1)
3. Click **Save** after filling the necessary fields.
![The Save button is at the top-right](https://www.dropbox.com/s/5b3dwearebmtdft/Screenshot102.png?raw=1)
4. Copy the generated API token.
![A sample screenshot](https://www.dropbox.com/s/hcrja66okiuuxy1/Screenshot104.png?raw=1)
5. Copy the new contents of the `.env.example` file to `.env`.
```env
    STRAPI_URL_BASE=http://localhost:1337
    STRAPI_API_TOKEN=a-secret-token-from-the-strapi-admin-gui
```
6. 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](https://www.dropbox.com/s/x2pwq757fpbvmkr/Screenshot106.png?raw=1)

![a reference to the tips in the Strapi backend](https://www.dropbox.com/s/ivt4sr88jr6bffz/Screenshot107.png?raw=1)

## Step 5: Define Dynamic Routes and Style
[Here's the link to this step](https://github.com/render-examples/strapiconf2022-workshop-remix/tree/step-5)

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.
```js
    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 }) {
      ...
```

2. Create the `TipRoute` component that is exported by default.
```js
    ...
    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>
      );
    }
```

3. Create a `loader` function for `useLoaderData()` hook in the TipRoute component.
```js
    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;
    }
    ...
```

**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:*


![](https://www.dropbox.com/s/nt7tni65j1dfvf4/Screenshot108.png?raw=1)



## 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 `repo`s 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
```

3. Save the changes, and push to the remote repository.

```
    $ git add render.yaml
    $ git commit
    $ git add
```
4. Login to the [Render dashboard](https://dashboard.render.com/).
5. Click on **New** and select **Blueprint.**
6. Add the repository that the *render* application can find the `render.yaml` file. In this case, the `strapiconf2022-workshop-strapi` repo.

![A screenshot](https://www.dropbox.com/s/j469jwe77f3mipa/Screenshot111.png?raw=1)


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](https://www.dropbox.com/s/9uacj32v70h7y2a/Screenshot113.png?raw=1)

**To get the value for the last three fields above, you need to do the following:**
1. Login to the [Cloudinary dashboard](https://cloudinary.com/console/).
![A screenshot](https://paper-attachments.dropbox.com/s_4E376C58A268517B56C5592393DD4D307422574E4EAE160FA261455BAE784436_1653342968786_Screenshot+119.png)
2. Click **Start configuring**.
![A screenshot](https://www.dropbox.com/s/tswp5hbgjm7woet/Screenshot115.png?raw=1)
3. Copy the values of the following fields:
    - `cloud_name`
    - `api_key`
    - `api_secret`
![A screenshot](https://www.dropbox.com/s/xvll5sasu6wy5hi/Screenshot116.png?raw=1)

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](https://www.dropbox.com/s/rrydp55kp58sliw/Screenshot117.png?raw=1) 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](https://dashboard.render.com/), 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:

- [Remix Docs](https://remix.run/docs)
- [Jokes App - Remix tutorial](https://remix.run/docs/en/v1/tutorials/jokes)
- [Deploying Strapi applications to Render](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment/hosting-guides/render.html)
- [Render Hosting Docs and Integrations](https://render.com/docs)