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:
/tips
page to view all the productivity tips in the database, andThis 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.
To follow this article, you will need knowledge of the following:
You also need the following installed:
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.
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.
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.
Before you start, make sure you have a GitHub Account, then follow the steps below:
yarn install
to install the dependencies.cp .env.example .env
to copy the environment variables from .env.example
to .env
.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:
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.
You can also add more tips to your application before continuing if you want.
Now, you are done with setting up the Strapi app. You can begin to create the frontend.
To create the remix app, follow the below steps:
cd
into the local repositorynpm install
to install its dependencies.This repository is split into five (5) branches, each showing the steps taken to build the application.
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:
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 {
2 Link,
3 Links,
4 LiveReload,
5 Meta,
6 Outlet,
7 Scripts,
8 ScrollRestoration,
9 } from "remix";
10
11 export function links () {
12 return [{
13 rel: "stylesheet",
14 href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
15 }]
16 }
17
18 export function meta() {
19 return { title: "Productivity Tips" };
20 }
21
22 export default function App() {
23 return (
24 <html lang="en">
25 <head>
26 <meta charSet="utf-8" />
27 <meta name="viewport" content="width=device-width,initial-scale=1" />
28 <Meta />
29 <Links />
30 </head>
31 <body>
32 <nav style={{marginLeft: 10}}>
33 <h1>
34 <Link to="/" style={{color: "var(--h1-color)"}}>
35 Productivity Tips
36 </Link>
37 </h1>
38 </nav>
39 <Outlet />
40 <ScrollRestoration />
41 <Scripts />
42 <LiveReload />
43 </body>
44 </html>
45 );
46 }
1 import {
2 Link,
3 Links,
4 LiveReload,
5 Meta,
6 Outlet,
7 Scripts,
8 ScrollRestoration,
9 } from "remix";
10
11 export function links () {
12 return [{
13 rel: "stylesheet",
14 href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
15 }]
16 }
17
18 export function meta() {
19 return { title: "Productivity Tips" };
20 }
21
22 export default function App() {
23 return (
24 <html lang="en">
25 <head>
26 <meta charSet="utf-8" />
27 <meta name="viewport" content="width=device-width,initial-scale=1" />
28 <Meta />
29 <Links />
30 </head>
31 <body>
32 <nav style={{marginLeft: 10}}>
33 <h1>
34 <Link to="/" style={{color: "var(--h1-color)"}}>
35 Productivity Tips
36 </Link>
37 </h1>
38 </nav>
39 <Outlet />
40 <ScrollRestoration />
41 <Scripts />
42 <LiveReload />
43 </body>
44 </html>
45 );
46 }
/
1 import {
2 Link,
3 Links,
4 LiveReload,
5 Meta,
6 Outlet,
7 Scripts,
8 ScrollRestoration,
9 } from "remix";
10
11 export function links () {
12 return [{
13 rel: "stylesheet",
14 href: "https://unpkg.com/@picocss/pico@latest/css/pico.min.css"
15 }]
16 }
17
18 export function meta() {
19 return { title: "Productivity Tips" };
20 }
21
22 export default function App() {
23 return (
24 <html lang="en">
25 <head>
26 <meta charSet="utf-8" />
27 <meta name="viewport" content="width=device-width,initial-scale=1" />
28 <Meta />
29 <Links />
30 </head>
31 <body>
32 <nav style={{marginLeft: 10}}>
33 <h1>
34 <Link to="/" style={{color: "var(--h1-color)"}}>
35 Productivity Tips
36 </Link>
37 </h1>
38 </nav>
39 <Outlet />
40 <ScrollRestoration />
41 <Scripts />
42 <LiveReload />
43 </body>
44 </html>
45 );
46 }
When you run this program, the web app will look the below:
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 import { Link } from "remix";
2
3 export default function Index() {
4 return (
5 <main className="container">
6 <p>
7 Over time everyone develops a Swiss army knife of tips, tricks,
8 and hacks to boost productivity. At Render, I created a
9 #productivity-tips Slack channel for anyone to share their best
10 productivity boosters with everyone on the team. Using
11 <a href="https://strapi.io">Strapi</a> and
12 <a href="https://remix.run">Remix</a>, we made a little web app to
13 catalog all of these tips and share them with others. 🤓
14 </p>
15 <Link to="/tips">👉 Productivity Tips</Link>
16 </main>
17 );
18 }
/tips
1 import { Link } from "remix";
2
3 export default function Index() {
4 return (
5 <main className="container">
6 <p>
7 Over time everyone develops a Swiss army knife of tips, tricks,
8 and hacks to boost productivity. At Render, I created a
9 #productivity-tips Slack channel for anyone to share their best
10 productivity boosters with everyone on the team. Using
11 <a href="https://strapi.io">Strapi</a> and
12 <a href="https://remix.run">Remix</a>, we made a little web app to
13 catalog all of these tips and share them with others. 🤓
14 </p>
15 <Link to="/tips">👉 Productivity Tips</Link>
16 </main>
17 );
18 }
/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:
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.1 import { Outlet } from "remix";
2
3 export default function TipsRoute() {
4 return (
5 <main className="container">
6 <Outlet />
7 </main>
8 );
9 }
index.jsx
file inside a new app/routes/tips
folder. This component will be rendered in place of the <Outlet />
above.1 import { Link, useLoaderData } from "remix";
2 import { checkStatus, checkEnvVars } from "~/utils/errorHandling";
3
4 export async function loader () {
5 checkEnvVars();
6
7 const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips?populate=*`, {
8 method: "GET",
9 headers: {
10 "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
11 "Content-Type": "application/json"
12 }
13 });
14
15 // Handle HTTP response code < 200 or >= 300
16 checkStatus(res);
17
18 const data = await res.json();
19
20 // Did Strapi return an error object in its response?
21 if (data.error) {
22 console.log('Error', data.error)
23 throw new Response("Error getting data from Strapi", { status: 500 })
24 }
25
26 return data.data;
27 }
28
29 export default function Tips() {
30 const tips = useLoaderData();
31
32 return (
33 <ul>
34 {tips.map((tip) => (
35 <li key={tip.attributes.Slug}>
36 <Link to={tip.attributes.Slug}>{tip.attributes.Name}</Link>
37 </li>
38 ))}
39 </ul>
40 );
41 }
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.1 // Custom error class for errors from Strapi API
2 class APIResponseError extends Error {
3 constructor(response) {
4 super(`API Error Response: ${response.status} ${response.statusText}`);
5 }
6 }
7
8 export const checkStatus = (response) => {
9 if (response.ok) {
10 // response.status >= 200 && response.status < 300
11 return response;
12 } else {
13 throw new APIResponseError(response);
14 }
15 }
16
17 class MissingEnvironmentVariable extends Error {
18 constructor(name) {
19 super(`Missing Environment Variable: The ${name} environment variable must be defined`);
20 }
21 }
22
23 export const checkEnvVars = () => {
24 const envVars = [
25 'STRAPI_URL_BASE',
26 'STRAPI_API_TOKEN'
27 ];
28
29 for (const envVar of envVars) {
30 if (! process.env[envVar]) {
31 throw new MissingEnvironmentVariable(envVar)
32 }
33 }
34 }
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.
1 import { Link, useLoaderData } from "remix";
2 import { checkStatus, checkEnvVars } from "~/utils/errorHandling";
3
4 export async function loader () {
5 checkEnvVars();
6
7 const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips?populate=*`, {
8 method: "GET",
9 headers: {
10 "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
11 "Content-Type": "application/json"
12 }
13 });
14
15 // Handle HTTP response code < 200 or >= 300
16 checkStatus(res);
17
18 const data = await res.json();
19
20 // Did Strapi return an error object in its response?
21 if (data.error) {
22 console.log('Error', data.error)
23 throw new Response("Error getting data from Strapi", { status: 500 })
24 }
25
26 return data.data;
27 }
28
29 export default function Tips() {
30 const tips = useLoaderData();
31
32 return (
33 <ul>
34 {tips.map((tip) => (
35 <li key={tip.attributes.Slug}>
36 <Link to={tip.attributes.Slug}>{tip.attributes.Name}</Link>
37 </li>
38 ))}
39 </ul>
40 );
41 }
Before running the application, you need to complete the following steps:
.env.example
file to .env
.1 STRAPI_URL_BASE=http://localhost:1337
2 STRAPI_API_TOKEN=a-secret-token-from-the-strapi-admin-gui
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 tohttp://127.0.0.1:1337
.
After running your application with the npm run dev
command, the web application should look like the below:
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 imagesIn the $tipId.jsx
file, the following is what happens:
tip.css
file into the $tipId.jsx
file.1 import { useLoaderData, Link } from "remix";
2 import { checkStatus, checkEnvVars } from "~/utils/errorHandling";
3
4 import stylesUrl from "~/styles/tip.css";
5
6 export function links () {
7 return [{ rel: "stylesheet", href: stylesUrl }];
8 }
9
10
11 export function meta ({ data }) {
12 return {
13 title: data.attributes.Name
14 }
15 }
16
17 export async function loader ({ params }) {
18 ...
TipRoute
component that is exported by default.1 ...
2 export default function TipRoute() {
3 const tip = useLoaderData();
4
5 return (
6 <div>
7 <Link to="/tips" style={{ textDecoration: 'none' }}>← back to list</Link>
8 <hgroup>
9 <h2>{tip.attributes.Name}</h2>
10 <h3>by {tip.attributes.Author.data?.attributes.firstname ?? 'an unknown user'}</h3>
11 </hgroup>
12
13 <p>
14 {tip.attributes.Description}
15 </p>
16 <div className="grid">
17 {tip.attributes.Screenshots.data.map((s) => (
18 <div key={s.attributes.hash}>
19 <img
20 src={s.attributes.formats.thumbnail.url}
21 alt={tip.attributes.Name + ' screenshot'}
22 />
23 </div>
24 ))}
25 </div>
26 </div>
27 );
28 }
loader
function for useLoaderData()
hook in the TipRoute component.1 import { useLoaderData, Link } from "remix";
2 import { checkStatus, checkEnvVars } from "~/utils/errorHandling";
3
4 import stylesUrl from "~/styles/tip.css";
5
6 export function links () {
7 return [{ rel: "stylesheet", href: stylesUrl }];
8 }
9
10 export function meta ({ data }) {
11 return {
12 title: data.attributes.Name
13 }
14 }
15
16 export async function loader ({ params }) {
17 checkEnvVars();
18
19 const res = await fetch(`${process.env.STRAPI_URL_BASE}/api/tips`
20 + `?populate=*&filters[Slug]=${params.tipId}`, {
21 method: "GET",
22 headers: {
23 "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
24 "Content-Type": "application/json"
25 }
26 })
27
28 // Handle HTTP response code < 200 or >= 300
29 checkStatus(res);
30
31 const data = await res.json();
32
33 // Did Strapi return an error object in its response?
34 if (data.error) {
35 console.log('Error', data.error)
36 throw new Response("Error getting data from Strapi", { status: 500 })
37 }
38
39 // Did Strapi return an empty list?
40 if (!data.data || data.data.length === 0) {
41 throw new Response("Not Found", { status: 404 });
42 }
43
44
45 const tip = data.data[0];
46
47 // For a Tip with no screenshot, replace API returned null with an empty array
48 tip.attributes.Screenshots.data = tip.attributes.Screenshots.data ?? [];
49
50 // Handle image URL being returned as just a path with no scheme and host.
51 // When storing media on the filesystem (Strapi's default), media URLs are
52 // return as only a URL path. When storing media using Cloudinary, as we do
53 // in production, media URLs are returned as full URLs.
54 for (const screenshot of tip.attributes.Screenshots.data) {
55 if (!screenshot.attributes.formats.thumbnail.url.startsWith('http')) {
56 screenshot.attributes.formats.thumbnail.url = process.env.STRAPI_URL_BASE +
57 screenshot.attributes.formats.thumbnail.url;
58 }
59 }
60 return tip;
61 }
62 ...
In the loader
function, the following happens:
checkEnvVars()
function to check if all the environment variables needed are present.http://localhost:1337/api/tips
route (The URL contains parameters that Strapi uses to specify the request).checkStatus()
function to check that the http status is alright ( between 200 to 299 ).When you run the application at this point, the http://localhost:3000/tips/tip page should look like the below:
Before deploying your project to the cloud, you need to follow the below steps:
strapiconf2022-workshop-strapi
repo.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
).1 services:
2 - type: web
3 name: productivity-tips-api
4 env: node
5 plan: free
6 # Update the following line with your Strapi GitHub repo
7 repo: https://github.com/render-examples/strapiconf2022-workshop-strapi
8 branch: main
9 buildCommand: yarn install && yarn build
10 startCommand: yarn start
11 healthCheckPath: /_health
12 envVars:
13 - key: NODE_VERSION
14 value: ~16.13.0
15 - key: NODE_ENV
16 value: production
17 - key: CLOUDINARY_NAME
18 sync: false
19 - key: CLOUDINARY_KEY
20 sync: false
21 - key: CLOUDINARY_SECRET
22 sync: false
23 - key: DATABASE_URL
24 fromDatabase:
25 name: strapi
26 property: connectionString
27 - key: JWT_SECRET
28 generateValue: true
29 - key: ADMIN_JWT_SECRET
30 generateValue: true
31 - key: API_TOKEN_SALT
32 generateValue: true
33 - key: APP_KEYS
34 generateValue: true
35
36 - type: web
37 name: productivity-tips-web
38 env: node
39 plan: free
40 # Update the following line with your Remix GitHub repo
41 repo: https://github.com/render-examples/strapiconf2022-workshop-remix
42 branch: step-5
43 buildCommand: npm install && npm run build
44 startCommand: npm start
45 envVars:
46 - key: STRAPI_URL_BASE
47 fromService:
48 type: web
49 name: productivity-tips-api
50 envVarKey: RENDER_EXTERNAL_URL
51
52 databases:
53 - name: strapi
54 plan: free # This database will expire 90 days after creation
1 $ git add render.yaml
2 $ git commit
3 $ git add
render.yaml
file. In this case, the strapiconf2022-workshop-strapi
repo.When you select the repo, you will be prompted to add the following details:
Service Group Name
. This is a unique name that is used to identify your project within your account.CLOUDINARY_NAME
CLOUDINARY_KEY
CLOUDINARY_SECRET
To get the value for the last three fields above, you need to do the following:
1. Login to the Cloudinary dashboard.
2. Click Start configuring.
3. Copy the values of the following fields:
- cloud_name
- api_key
- api_secret
CLOUDINARY_NAME
CLOUDINARY_KEY
CLOUDINARY_SECRET
After filling the fields, click Apply Then you wait for Render to deploy your application.
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 appproductivity-tips-api
for the Strapi appAfter 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:
/admin
).productivity-tips-web
serviceSTRAPI_API_TOKEN
and paste the generated API tokenAfter 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:
Chigozie is a technical writer. He started coding since he was young, and entered technical writing recently.