Building a Blog App with Remix and Strapi CMS

Strapi - Aug 31 '22 - - Dev Community

This article details the steps to building a blog app with Remix and Strapi CMS.

Author: Joseph Chege

Strapi is an open-source headless CMS based on Node.js. It lets you develop and manage any content of your application. This allows you to build backend API faster with efficiency and customizability advantages.

What is Remix?

On the other side, Remix is a good choice to consume your generated API endpoints. It offers data loading on the server side. Remix is a server-side rendered framework. It allows you to work seamlessly with the server and the client. Remix closely compare to Next.js framework.

Remix vs Next.js

Next.js is one of the popular React frameworks for server-side rendering. Traditionally, React apps are rendered on the client side. When the initial request is sent from the web app, the browser is sent a shell of a bare HTML page that has no pre-rendered content. The browser would finally fetch the JavaScript file responsible for rendering the actual HTML page.

With the introduction of Server-Side Rendering (SSR) frameworks such as Next.js and Remix, a page is fully rendered on-demand on the server. This approach fits dynamic content that changes frequently.

Some of the reasons Remix is a good choice include:

  • It's faster than Next.js.
  • Remix has better performance.
  • Remix leverages the native browser features. This allows Remix to reuse most platform-level features of HTML and HTTP to handle complex mutations. On the other side, if a Next.js app goes offline, the page will not respond, making it hard to handle mutations.
  • Remix uses native HTML and it can work without JavaScript. This makes it easier to handle server-based errors creating a good experience for the user.

Remix and Next.js are closely related, and the difference between them is minimal. Check this guide to get a bigger picture of how is Remix different from Next.js.

Prerequisites

To continue in this article, it is important to have the following:

  • Node.js runtime installed on your computer,
  • Basic knowledge of JavaScript, and
  • Prior experience working with a React-based framework such as Remix.

Setting up Strapi Locally

Let's dive in and create a Strapi CMS backend. This will serve the blog posts to the users. To set up Strapi CMS, create a project folder. Then, open a command that points to this directory, preferably using a text editor such as the Visual Studio Code. Run the following command to install Strapi CMS on your local computer:

    npx create-strapi-app@latest blog-app
Enter fullscreen mode Exit fullscreen mode

Ensure to execute this command based on the following installation type:

installation-types

This will create a blog-app folder that will host all the project files and dependencies that Strapi CMS will need to run locally. Once the installation is done, you will be redirected to the Strapi admin page on your default browser at http://localhost:1337/admin/auth/register-admin.

strapi-admin-page

Provide the login credentials to access the Strapi admin. If you haven't created an account yet, provide Sign up details. Finally, click the Let's start button to access the admin dashboard.

Build Blogs Collection with Strapi CMS

Here, we are creating a blog app. Therefore, we need to model the blog app backend content with Strapi. The first step is to set up the content type. On the Strapi admin panel, click Content-Type Builder.

step_one

Then click the + Create new collection type button to set up the collection. Enter blog as the content type name:

content_type_setup_name

Click Continue and set up the following blog fields:

  • Title as a short text - A brief heading for your blog post.
  • Excerpt as long text - A short summary of your post that will be displayed on the web app.
  • Hero: select media, single - A descriptive image for your blog post.
  • Content: as rich text - represents the content of your blog post. content_type_fields

Finally, create some test content for the above collection and its fields. Go ahead and click the + Create new entry button:

step_two

Fill in all the fields, and then make sure you publish the added content:

fields_data

Make sure you create at least three different blog posts using the same procedure as described above.

Adding Roles & Permissions

Strapi CMS runs a decoupled application structure. To access its content, you need to set up permissions and roles that define what data should be exposed to your user channels.

In this example, we need Remix to interact with the collection we have created. Thus, we need to set up access to this collection so that Remix can execute the right request and get the expected responses.

To access the created blog collection, generate an API token that Remix will use to communicate with Strapi. To create the token:

  • Navigate to the Setting section of the Strapi Dashboard.
  • Click the API Tokens section.
  • Click the Create new API Token button to set a token to consume the API. step_3

Fill in the name and description in the respective fields. Finally, select a Read-only token type and then click save.

In this example, Remix will only read the content of our collection. If you need to add operations such as writing to Strapi CMS, ensure you modify your token type accordingly.

creating_api_token

The API token will be generated. Copy the created token as we will use it with Remix.

Setting up the Remix Application

Let's now create a frontend application to consume the content we have created with Strapi. On your project directory, you created earlier, run the following command to set up Remix:

    npx create-remix@latest remix-blog-app
Enter fullscreen mode Exit fullscreen mode

remix-setup

The above command will create a remix-blog-app folder that will host all the project files and dependencies that Remix will need to run a local web app.

Once the installation is done, proceed to the newly created directory:

    cd remix-blog-app
Enter fullscreen mode Exit fullscreen mode

You can test if the scaffolded Remix application works correctly by running:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000/ on a browser and you should be served with a hello world version of the Remix web app.

In the project Remix root directory (remix-blog-app), create a .env file and add in the following Remix configurations:

STRAPI_API_TOKEN="your_access_token"
STRAPI_URL_BASE="http://your_local_ip_address:1337"
Enter fullscreen mode Exit fullscreen mode

Paste in your access token from the Strapi dashboard to replace the your_access_token.

Setting up the Remix Components

Components set the application user interface. Remix uses components to set up different sections of a web application. Here will create the following components:

  • A Navigation Bar: A basic web application navigation bar
  • Blogpost Cards: Remix cards for displaying posts and their properties
  • Layout: To set the overall layout of the web page

To set up these components, create a components folder in the app directory of the Remix project. Inside the components folder, create the following files and the respective code blocks to create the components:

Navbar.jsx:

    export default function Navbar() {
        return (
            <nav className="navbar">
                <div className="nav-wrapper">
                    <a href="/" className="brand-logo">Blog App</a>
                </div>
            </nav>
        )
    }
Enter fullscreen mode Exit fullscreen mode

Here, we are creating a Navbar that displays the application brand logo as a Blog App text. A click on this brand logo will redirect the user to the application home page.

BlogCard.jsx:

    import { Link } from '@remix-run/react';
    import url from '../utils/url';

    export default function BlogCard({ blog }) {
        let data = blog.attributes;
        return (
            <div className="card">
                <div className="card-content">
                    <div className="card-img">
                        <img src={`${url}${data.hero.data.attributes.url}`} alt={data.hero.data.attributes.alternativeText} />
                    </div>
                    <div className="card-details">

                        <Link to={`/posts/${blog.id}`} className="card-title">
                            {data.title}
                        </Link>

                        <p className="card-excerpt">{data.excerpt}</p>
                    </div>
                </div>
            </div>
        )
    }
Enter fullscreen mode Exit fullscreen mode

This BlogCard will display and arrange blogs in your application. Here, each post card will display the post title, hero image, and the post excerpt.

Layout.jsx:

    import Navbar from './Navbar';

    export default function Layout({ children }) {
        return (
            <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
                <Navbar />
                <div className="container">
                    {children}
                </div>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

The Layout component runs a container that wraps the Navbar component to the main application.

style.css

To style the above components, add the following CSS code to a style.css file:

    /* Navbar */
    .navbar {
      box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
      margin-bottom: 0;
      padding: 12px;
      position: relative;
    }

    .navbar .nav-wrapper a {
      text-decoration: none;
      font-size: 20px;
    }

    /* Layout */
    .container {
      width: 50%;
      margin: 0px auto;
    }

    /* Blog card */
    .card {
      width: 100%;
      padding: 10px;
      margin: 10px 0px;
      border-radius: 5px;
      box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
    }

    .card-content {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }

    .card-content .card-img {
      width: 50%;
      height: 100%;
    }

    .card-img img {
      width: 90%;
      height: 100%;
      border-radius: 5px;
    }

    .card-details .card-title {
      text-decoration: none;
    }
Enter fullscreen mode Exit fullscreen mode

index.jsx:

To execute the above components, create an index.jsx file and export it, as shown in the following code block:

    import BlogCard from "./BlogCard";
    import Layout from "./Layout";
    import Navbar from "./Navbar";
    export { BlogCard, Layout, Navbar };
Enter fullscreen mode Exit fullscreen mode

Exporting these components makes it easier for other application modules to access and use them according.

Setting up Remix Utilities

Create a utils folder inside the Remix app directory. Inside the utils folder, create the following files:

  • errorHandling.js: For handling exemptions when connecting to the Strapi backend API:
    // 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
  • url.js: Host Strapi localhost URL.
    export default "http://localhost:1337";
Enter fullscreen mode Exit fullscreen mode

Setting up the Routes

To access the posts, we'll create Remix routes. In this case, we will have two major routes:

  • Route one to fetch all posts.
  • Route two, which is a child route for every post, to help us access a single post.

Let's create these routes.

Fetching all Posts

Navigate to the app/routes/index.jsx file and fetch all the blog posts as follows:

  • Import all the necessary modules:
    import { useLoaderData } from '@remix-run/react';
    import { checkEnvVars, checkStatus } from '../utils/errorHandling';
    import {Layout, BlogCard} from '../components';
    import styles from '../components/style.css';
Enter fullscreen mode Exit fullscreen mode
  • Configure the styles for the BlogCard component:
export const links = () => [
        { rel: "stylesheet", href: styles },
    ];
Enter fullscreen mode Exit fullscreen mode
  • Configure the loader for fetching the posts from Strapi:
    export async function loader() {
        checkEnvVars(); // check environmental variables
        const response = await fetch(`${process.env.STRAPI_URL_BASE}/api/blogs?populate=hero`, {
            method: "GET",
            headers: {
                "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
                "Content-Type": "application/json"
            }
        }); // get the blogs

        checkStatus(response); // check the status

        const data = await response.json(); // get the json response

        if (data.error) { // error check
            throw new Response("Error loading data from strapi", { status: 500 });
        }

        return data.data; // return the data
    }
Enter fullscreen mode Exit fullscreen mode
  • Edit the render function to get the posts and render them on the BlogCard component:
export default function Index() {
        const blogs = useLoaderData();
        return (
            <Layout>
                {
                    blogs.length > 0 ? (
                        blogs.map(blog => (
                            <BlogCard key={blog.id} blog={blog} />
                        ))
                    ) : (
                        <p>No blog posts found!</p>
                    )
                }
            </Layout>
        );
    }
Enter fullscreen mode Exit fullscreen mode

This will be enough to display the blog posts on Remix. To test if it works as expected, ensure that the Remix development server is up and running using the following command:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

This will serve your application on http://localhost:3000. Use this link and open the application on a browser. Depending on the posts added, your page should be similar to:

posts_page

Fetching a Single Post

At this point, Remix can serve the blog posts. However, we can't access the content on every single post. To do so:

  • Create a posts directory inside app/routes directory.
  • Create a $postId.jsx file in the posts directory.
  • In the $postId.jsx file, import the necessary modules:
    import { useLoaderData } from '@remix-run/react';
    import { checkEnvVars, checkStatus } from '../../utils/errorHandling';
    import url from '../../utils/url';
    import { Layout } from '../../components';
    import styles from '../../components/style.css';
Enter fullscreen mode Exit fullscreen mode
  • Configure the styles:
    export const links = () => [
        { rel: "stylesheet", href: styles },
    ];
Enter fullscreen mode Exit fullscreen mode
  • Configure a loader function to get each post:
    export async function loader({ params }) {
        const { postId } = params; // get the post id
        checkEnvVars(); // check the environmental variables
        const response = await fetch(`${process.env.STRAPI_URL_BASE}/api/blogs/${postId}?populate=hero`, {
            method: "GET",
            headers: {
                "Authorization": `Bearer ${process.env.STRAPI_API_TOKEN}`,
                "Content-Type": "application/json"
            }
        }); // send a request to strapi backend to get the post

        checkStatus(response); // check the response status

        const data = await response.json(); // get the json data

        if (data.error) {// check if we have an error
            throw new Response("Error loading data from strapi", { status: 500 });
        }

        return data.data; // return the data
    }
Enter fullscreen mode Exit fullscreen mode
  • Configure a render function, to get the data fetched and display it:
    export default function Post() {
        const blog = useLoaderData();
        const blogData = blog.attributes;
        return (
            <Layout>
                <div className="blog-post">
                    <div className="blog-post-hero">
                        <img src={`${url}${blogData.hero.data.attributes.url}`} alt={`${blogData.hero.data.attributes.alternativeText}`} />
                    </div>
                    <div className="blog-post-title">
                        <h1>{blogData.title}</h1>
                    </div>
                    <div className="blog-post-content">
                        <div dangerouslySetInnerHTML={{ __html: blogData.content }} />
                    </div>
                </div>
            </Layout>
        )
    }
Enter fullscreen mode Exit fullscreen mode

Ensuring that the development server is up and running, click on any post on the home page. You will be directed to a page with the content of each specific blog as shown below:

post_page

Conclusion

Strapi CMS is used to develop and manage the content of any application. It helps you scaffold any API faster and consume the content via Restful APIs and GraphQL. In this guide, we have created a Strapi Restful API and then consumed it to build a minimalistic Remix blog application.

You can get the source code used on this project on the GitHub repository for the Remix Frontend part.

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