DEV Community

Cover image for Remix Blog Tutorial - Remix, React, Prisma, MongoDB, Vercel (Part 1)
Chris Benjamin
Chris Benjamin

Posted on • Updated on

Remix Blog Tutorial - Remix, React, Prisma, MongoDB, Vercel (Part 1)

What this Remix Tutorial Is?

  • This remix tutorial is a simple app that show cases some of the functionality of Remix and some of the cool features I've recently blogged or tweeted about Remix.
  • It will be built using the default create-remix method
  • It will be hosted on Vercel
  • It will use JavaScript

This tutorial takes the remix blog tutorial available on the Remix docs and expands on it to include editing, and MongoDB Storage and some styling. I re-wrote it entirely from my learning experience. I tried to explain most of the pieces used, the Remix docs are wonderful and explain things with examples very well.

You can see a functional version of the app we are making here https://remix-blog-ten.vercel.app

Full Source Code is available on Github here

This is part 1 of the tutorial, please see the end of the post for information on Part 2.

What this Remix Tutorial is Not?

  • This is not a full scope tutorial to teach you everything about Remix.
  • This is not a full scope tutorial on MongoDB
  • This tutorial will not be using TypeScript

Let's get started! ๐Ÿš€

Yay!

Your First Remix.Run App!

Let's create a simple Remix.Run app to get you started on your journey to learning Remix!

I'm going to be using VSCode for this tutorial, you can use your editor of choice.

Let's install Remix

Open the terminal and run the following command
npx create-remix@latest

If this is your first time using remix. It will prompt you that you need to install the following packages:
create-remix@latest. Enter 'y' and press Enter

You should get a message - Welcome to remix! Let's get you setup with a new project. Here, we are going to enter the name for our project. I'm going to call mine remix-blog. Feel free to name yours however you like.

Remix Welcome

Remix will now ask you where you want to deploy. For this tutorial, we are going to use Vercel.
If you wish to use something else, you may have a slightly different experience, the Readme.MD file will include helpful information if you change your hosting provider.
Note: A vercel account will be needed when we deploy. It's free and easy to do, visit Vercel to register.

Using your Arrow keys, press down until Vercel is highlighted and press enter.

Remix will now ask you which JS Language, plain JavaScript or TypeScript. For this tutorial, we are going to choose JavaScript.

Remix will now ask if you want to run npm install we are going to say yes, so put in y and then press enter to get up and running quickly.

Your screen should look like this

Remix First Steps

If everything worked, you should be presented with this

Remix Success

Let's go into the directory that Remix Created. I'm going to type cd remix-blog if you called your project something else, be sure to replace remix-blog with your project name.

The README.md has some very important information in there about how to get up and running with the hosting setup chosen. Read this carefully and fully, it's worth it.

Let's get Vercel hosting setup which is required for running app in development

First you'll need the Vercel CLI:

Run this command to install Vercel CLI npm i -g vercel

Once this is complete we need to link it to a vercel project.
Run the command vercel link

  • When asked to setup the project, input Y and press enter
  • When asked which scope, this should be your vercel username
  • When asked to link to existing project say no
  • When asked for projects name, use default or give it a new name for what you will see in Vercel dashboard
  • Which directory is your code location, use default ./
  • Auto-Detected Project Settings (Remix). It will ask if you want to override the settings, we are going to say No, input n and press enter. You should get an output like this

Vercel Output

Let's run the default Remix App

We are going to do a test run to make sure you can get Remix up and running in a dev server environment with live reload before we continue.

npm run dev

Now lets open your demo site to make sure it works! Open a browser and navigate to http://localhost:3000 or click the link below.
Browse to http://localhost:3000

You should be presented with the Welcome to Remix! page which was created by the npx create-remix@latest app. If so, fanstastic!!! If not, check your terminal for errors and go back if necessary.

Great job so far, let's keep it up!

Working Demo Site

Take some time to look around at this demo site before moving on. There is a lot of valuable information here along with links to their fantastic documentation. When you are ready, scroll down to continue with our first app, but I don't mind if you get lost reading the docs for a while, just make sure you finish this tutorial ๐Ÿ˜Š

Let's build our Remix project!

We are going to break this down into 2 sections

  • Setting up the app functionality
  • Setting up the database storage functionality
  • We will finish off by Deploying to Vercel to share with the world!

Project Cleanup

We are going to leave all existing files intact so you have reference code to look at. We will simply expand on this default application.

Understanding folder setup

The App folder is the most important folder. This is the basis of your entire site. In this folder, we will place our Routes, Styles, and Utils folders. We will also have three important files, the entry.client, entry.server and root. More information on these is available on the Remix Docs site, there's a link in your project to get there in a jiffy.

Project Setup

These steps will setup our initial project. Since we are going to add to the existing data, we are going to make this quick and easy.

Project Dependencies

We will be installing the following depdencies throughout this project. If you are familiar with this process, you can knock out all of these installs up front, or you can follow along and install them at each step in the blog.

  • npm install @prisma/client (DB Client)
  • npm install marked (Markdown to HTML converter)
  • npm install tiny-invariant (Invariant handling for TS)
  • npm install prisma --save-dev (Dev Dependency for Prisma)

Setup root.jsx

We need to update the Root.jsx to have nav links to our pages we are going to create.

We are going to add 2 routes

  • Blogs- This will retrieve our blog articles from the database and display them for reading.
  • Admin - This will show our blog articles, allow us to edit them, and allow us to add new blog articles.

Let's open root.jsx and make some adjustments.

Locate the function Layout({ children }) within root.jsx and update the return statement to the following code, adding 2 Link tags right below the Home link.

    // Home Link...
              <li>
                <Link to="/blogs">Blog</Link>
              </li>
              <li>
                <Link to="/admin">Admin</Link>
              </li>
    // Remix Docs... 
Enter fullscreen mode Exit fullscreen mode

MongoDB Setup

For this blog, we will be using Prisma with experimental features for connecting to a MongoDB database to store our blog articles. This is quick, simple, and free to start so it will be great for our small project.
Let's install the dev dependency. Run npm install prisma --save-dev
Let's invoke the Prisma command line interface (CLI), run npx prisma If you get an error, then Prisma did not install correctly, check the terminal output.
Let's now initialize our Prisma project, let's run npx prisma init which will create our Prisma folder and

Get MongoDB URL:

We are going to use the Free Shared DB, it's free, no credit card required to start, and you get leverage the power of cloud database. This section assumes you have never used MongoDB before, if you are already familiar with Mongo or have a Cluster setup, you can skip ahead to the next section ๐Ÿ˜Ž

  1. Go to https://account.mongodb.com/account/register?tck=docs_atlas and create an account
  2. Choose the Free Shared account
  3. Choose any cluster, I'm choosing AWS and Oregon (us-west-2) for my deployment, and create the cluster.
  4. In the Security QuickStart, create a Username and Password authentication. Save this information as we will need it soon. I'm going to create remix_user with a secure password. Be sure to click Create User.
  5. For IP Access List, we are going to put in 0.0.0.0 as the IP to ensure that our database get's up and running quickly for testing. You will want to restrict this for production apps.
  6. You should now be redirected to your Database Deployments showing Cluster0. Mongo Database Deployments Setup
  7. Click Connect button under Cluster 0
  8. Click Connect your application
  9. Copy the connection string provided.
  10. In your Remix app, look for the .env file in the root folder. This is a local environment file that we will store your mongo URL secret in since it contains username and password to your database. Open this up and you will see that Prisma already put some information in there.
  11. Let's update the DATABASE_URL to be our new MongoDB server address. DATABASE_URL="mongodb+srv://remix_user:supersecretpassword@cluster0.cvvbu.mongodb.net/MyFirstDatabase"

Mongo DB Schema Setup

In your MongoDB Dashboard we need to setup our database structure.

  1. Under Database Deployment click Browse Collections
  2. Your database should be empty and Mongo want's to help us add sample data, we don't want to do that, we want to create our own so let's choose Add My Own Data
  3. For Database Name put in Remix_Blog
  4. For Collection Name put in posts
  5. Click Create Create Database Screen
  6. Great work so far! Now we just need to setup one sample blog that Remix can use to pull down our existing blogs and Prisma can understand our layout.
  7. Click Insert Document
  8. We are going to add 3 fields, all of them will be of type String.
  9. Add slug with a value of my-first-post
  10. Add markdown with a value of #This is my first post
  11. Add title with a value of My First Post
  12. Click Insert

Finalize Prisma Setup

Now we need to get our Prisma Schema file ready for MongoDB

  1. We need to update our .env file to reflect our new database.
  2. Let's update the DATABASE_URL to be our new MongoDB server address, only the end of the file should change to end with mongodb.net/Remix_Blog; DATABASE_URL="mongodb+srv://remix_user:supersecretpassword@cluster0.cvvbu.mongodb.net/Remix_Blog"
  3. In your Remix project, open the prisma folder, then open the schema.prisma file
  4. In here we are going to update the datasource DB to the following:
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["mongoDb"]
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode
  1. Now that we have our generator, our data source. We are ready to run introspection. This will have Prisma inspect our MongoDB schema by sampling data in the given database and inferring the schema of that database.
  2. Run npx prisma db pull (If this fails, you should check the .env file was setup correct and the username/password are correct and that the database URL was updated to end with your database name) Prisma DB Pull Command
  3. Now if you look at the schema.prisma file, you will see that Prisma automatically generated our model posts{} object. When using MongoDB with Prisma, Prisma will pull down your model, with other providers you may use Prisma to push up your model.
model posts {
  id       String @id @default(dbgenerated()) @map("_id") @db.ObjectId
  markdown String
  slug     String
  title    String
}
Enter fullscreen mode Exit fullscreen mode
  1. Run npx prisma generate

  2. You should get the following output
    npx prisma generate command

Hooray! We are ready to begin using Prisma in our application! ๐ŸŽ‰

Blog Route Setup

Here we will begin laying out the files for our blog. First we need a Blogs route so Remix knows what to load when users navigate to /blogs. Under the app folder, create a folder named blogs (./app/blogs/)

In this folder create a file index.jsx

We will also need a CSS stylesheet so let's throw that in while we are creating files. Under ./app/styles create a file called blogs.css (./app/styles/blogs.css).

Now open up the index.jsx file we just created in blogs.

Let's create a very basic return statement to make sure our route is working.

export default function Blogs() {
    return <h1>Blogs Route</h1>
}
Enter fullscreen mode Exit fullscreen mode

Now on your server, if it's not running make sure you run npx run dev then click the Blogs link at the top. We should now be presented with Blogs Route at the top which means our routing is working. If you get a 404 error, go back and make sure your file is named correctly. In some instances you may need to stop and restart the server.

example blogs route

Now let's set up the rest of our Blog.

Get Posts

Before we can load data in our route, we first need to get our data.
We will do this in another file called a module. In the root of your app folder, create a file called post.js, (./app/post.js)

Open up this post.js file and let's add import Prisma so we can use it in our app, and let's create our getPosts() function.

import { PrismaClient } from '@prisma/client'
// let's create a reference to prisma
const prisma = new PrismaClient();
// async function since we will be loading external data
export async function getPosts(){
// await prisma connection
        await prisma.$connect()
// let's grab all posts using findMany()
// the posts in prisma.posts is the collection we created in Mongo.db
        const allPosts = await prisma.posts.findMany();
// let's cleanup our connection
    prisma.$disconnect();
        // let's see what we are returning
       console.log(allPosts)
        return allPosts;
}
Enter fullscreen mode Exit fullscreen mode

Now we need to tie in this function to our Blogs route. Go back to the index.jsx in you blogs route (./app/route/blog/index.jsx)
We just want to test that we are pulling our first blog from MongoDB. Let's add the following code above the export default function Blogs()

import { getPosts } from '~/post';
export let loader = () => {
    return getPosts();
}
Enter fullscreen mode Exit fullscreen mode

Now Remix should automatically rebuild and you should see in your VSCode terminal, the output we are logging

[
  {
    id: '61a914e90b627f455a212145',
    markdown: '#This is my first post',
    slug: 'my-first-post',
    title: 'My First Post'
  }
]
Enter fullscreen mode Exit fullscreen mode

Here is the terminal output:
VSCode terminal Output

Awesome, so our Remix app is using it's loader function to talk to MongoDB through Prisma. We are ready to load our blog article into our app.
Lets go into the blogs index.jsx file (./app/blogs/index.jsx)
We need to import 2 more pieces from React, the Link tool and the useLoaderData hook.

import { Link, useLoaderData } from 'remix';
Enter fullscreen mode Exit fullscreen mode

Sweet, those are ready to use. Now let's update our Posts() function to use our loader data and render it on the screen. We are going to map through the data and render the posts by Title and using Remix Link so the user can click on our blog post to read it. We will add 2 classes now for styling in a later step. We will use the slug as the Key on the <li> tag since this should be unique.

Update the Posts() function as follows

// our Posts function which will return the rendered component on the page .
export default function Posts() {
    let posts = useLoaderData();
    return (
        <div>
            <h1>My Remix Blog</h1>
            <p>Click on the post name to read the post</p>
            <ul>
                {posts.map(post => (
                    <li className="postList" key={post.slug}>
                        <Link className="postTitle" to={post.slug}>{post.title}</Link>
                    </li>
                ))}
            </ul>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Perfect, we should see our blog article being rendered as a unordered list. If you click on it right now, you will get a 404, that's because we haven't told Remix what to do yet. Let's set that up now.

First we need a way to pull the information for a single post from our MongoDB database. Lets go to the root post.js folder and create a function to do that. We will also be using a library called Marked to convert our current markdown content into HTML to render the blog article directly as HTML on our component.

Let's install the dependency. Run npm install marked in your VSCode terminal.
In your post.js file let's import this at the top
import { marked } from "marked";

We will also be using a library called Tiny-Invariant which will help us check if we are loading a slug as expected from params.
Let's install this dependency. Run npm install tiny-invariant in your VSCode terminal

Ok now we are ready to update post.js module with the getPost() function.
Add the following code to your post.js file.

// this function is used to load a single post from a passed through slug
export async function getPost(slug){
    //setup our prisma connection
    await prisma.$connect();

    // we will find the first database entry that matches the passed slug
   const foundSlug = await prisma.blogs.findFirst({
        where: {
            slug: slug
        }
    })

    //let's extract the title 
    let title = foundSlug.title;
    // using marked, we are going to convert the markdown into HTML so the blog post can render as entered in Markdown. 
    let html = marked(foundSlug.markdown)
    // we need to cleanup our database connection
    prisma.$disconnect();

    // let's send back the slug, the title, and our markdown converted to html 
    return { slug, title, html};
}
Enter fullscreen mode Exit fullscreen mode

Perfect, now we can query a single post by passing in the slug, see how simple this can be?

We are going to use Remix nested routes to dynamically load our blog posts using a single route. This is accomplished by using a dynamic segment in the URL. To do this, let's create a file in our blogs route folder called $slug.jsx.

This file is going to need to import our a getPost() function from our post module we just created, it will have a loader to extract the dynamic segment from the parameter and then use that to go get our post. Then we will need a default function which will render our child component to the screen.

Update ./app/routes/blogs/$slug.jsx with this code

import { useLoaderData } from 'remix';
import invariant from 'tiny-invariant';
import { getPost } from "~/post";

export let loader = async({params}) => {
    invariant(params.slug, "expected params.slug");
    return getPost(params.slug);
}

export default function PostSlug() {
    let post = useLoaderData();
    return (
        <div className="postDisplay" dangerouslySetInnerHTML={{__html: post.html}}/>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now you are ready to go test this out. Go to your browser where your Remix app is running, and now try to click on the title of your blog post. Your page should now load based on the dynamic segment being passed, and you should see your content returned as HTML.

Blog Post Rendered

We will style this page after we setup the admin page.

Recap

Awesome progress! So far, we have setup a database, we put our first blog entry directly to the database, and we are now pulling that into our app and the user can read our blogs that are entered as markdown and displayed as HTML. Give yourself a pat on the back!

Let's take this a step further now. We don't want to input our blog posts directly into MongoDB. We want to be able to create them right from our app. For this we are going to create an admin panel which will let us create new posts, and edit existing posts. Head on over to the next section to set this up.

Blog Admin Panel

Our blog needs a way to create new blog posts and edit existing blog posts directly from our Remix app. We are going to setup a new route for this to separate it out.
As an expansion to this tutorial, you can setup Authentication on your app and then only render the Admin route if the logged in user should have access

Route Setup

In your project, we are going to create a new route and an admin page.
In your Routes folder, create a new file called admin.jsx.
Also in the Routes folder, create a new folder called admin
In this new admin folder, create an index.jsx file.

Let's work on these separately.

Our admin page is going to be pretty similar to our blog page, but with some extra features. So let's copy some of that code. Update your admin.jsx as follows

import { Outlet, Link, useLoaderData } from 'remix';
import { getPosts } from "~/post";
import adminStyles from "~/styles/admin.css";

//create a stylesheet ref for the admin.css file 
export let links = () => {
    return [{rel: "stylesheet", href: adminStyles}]
}

export let loader = () => {
    return getPosts();
}

export default function Admin() {
    let posts = useLoaderData();
    return (
        <div className="admin">
            <h1 className="adminTitle">Admin</h1>
            <nav>
             <p>Click on a post to edit the blog post</p>
                <ul>
                    {posts.map(post => (
                        <li key={post.slug}>
                        <Link to={post.slug}>{post.title}</Link>
                        </li>
                    ))}
                </ul>
                <main>
                    {/* Outlet renders the /admin/index.jsx */}
                    <Outlet />
                </main>
            </nav>

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now if you check your app, you should be able to click the Admin link at the top nav and see your admin page and your blog posts. If you click on that link, you are going to get an error because we haven't told Remix what to do yet.

Admin Page

Sweet! Keep up the great work, not a lot of work left!

Create a blog post

Let's setup the ability to add new blog posts.
In your routes/admin folder, open your index.jsx
This will be a really simple component to return a button that will redirect our user to the new page we will create.

Update index.jsx (./app/routes/admin/index.jsx) as follows:

import { Link } from 'remix';

export default function AdminIndex() {
    return (
            <div className="adminNewPostLink">
            <Link to="new"> <button className="adminNewPostButton">Create a New Post</button></Link>
            </div>       
    )
}
Enter fullscreen mode Exit fullscreen mode

To get this button to display inside our Admin component we need to use the Outlet. With Remix, the index.jsx file in the nested routing will automatically render inside of our outlet.

Open admin.jsx and lets update our import to include Outlet
import { Outlet, Link, useLoaderData } from 'remix';

Perfect, now let's place that in our <main> tag we setup earlier in this same file.

Update the main tag to include the Outlet

    <main>
       {/* Outlet renders the /admin/index.jsx */}
       <Outlet />
   </main>
Enter fullscreen mode Exit fullscreen mode

Now you should have a button on your admin page to create a new post. Let's go make that button work now!

Admin page with create button

In your admin route, create the file new.jsx. This will be used to create a new blog post. This file is going to use a Remix <Form> instead of a standard HTML <form> and post to the current route. We will use a Remix action to get our formData in the request sent by that post, perform some client side validation, and then call a createPost() function from our post.js module that will add our new blog post to the database on MongoDB.

import { redirect, Form, useActionData, useTransition } from "remix";
import { createPost } from "~/post";

export let action = async ({ request }) => {
    let formData = await request.formData();
    let title = formData.get("title");
    let slug = formData.get("slug")
    let markdown = formData.get("markdown")

    let errors = {};
    if (!title) errors.title = true;
    if (!slug) errors.slug = true;
    if (!markdown) errors.markdown = true;

    if (Object.keys(errors).length) {
        return errors;
    }

    await createPost({title, slug, markdown});

    return redirect("/admin")
}


export default function NewPost() {
    // pull in errors from our action using the useActionData() hook
    let errors = useActionData();
    // transition will allow us to create a better user experience by updating the text of the submit button while creating the blog post
    let transition = useTransition();
    // we are going to create the slug for the user
    let slug = ''

    // as the Title input is updated we will generate the slug automatically. 
    // My First Post slug would equal 'my-first-post'. We will convert to lower case and we will strip spaces and replace with hyphens 
    const handleChange = (e) =>{
        let text = e.target.value 
        // using regex and replace, let's convert spaces to dashes
       slug = text.replace(/\s/g , '-');
       // lets set the value of the slug text box to be our new slug in lowercase 
       document.getElementById("slugInput").value = slug.toLowerCase();
    }
  return (
      <Form method="post">
          <p>
              <label htmlFor="">
                  Post Title: {" "} {errors?.title && <em>Title is required</em>} <input onChange={handleChange} type="text" name="title"/>
              </label>
            </p>
            <p>
                <label htmlFor=""> Post Slug: {" "} {errors?.slug && <em>Slug is required</em>} 
                <input placeholder={slug} id="slugInput" type="text" name="slug"/>
            </label>
            </p>
            <p>
                <label htmlFor="markdown">Markdown:</label>{" "} {errors?.markdown && <em>Markdown is required</em>} 
                <br />
                <textarea name="markdown" id="" rows={20} cols={30}/>
            </p>
            <p>
                <button type="submit">{transition.submission ? "Creating..." : "Create Post"}</button>
            </p>
      </Form>
  )
} 
Enter fullscreen mode Exit fullscreen mode

Awesome! Let's check our app and make sure we are seeing the form we will work on the createPost() function next.
It should look like this:

Form on remix blog app

Sweet, our app is coming right along, let's setup the post.js module to allow us to create entries in our MongoDB database.

Open your post.js and add the following createPost() function

    export async function createPost(post){
        //Prisma connection 
        await prisma.$connect()
        // prisma create
        await prisma.posts.create({
            data: {
                title: post.title,
                slug: post.slug,
                markdown: post.markdown
            }
        })
        // cleanup prisma connection 
        prisma.$disconnect();
        // let's send back the slug we created
        return getPost(post.slug)    
}
Enter fullscreen mode Exit fullscreen mode

Creating Posts

Alright it's the moment we have been waiting for. Let's create our first blog post from our app!
Check our your app and create a new blog post.

Create new blog

Whoa! Check that out, the button switched to 'Creating...' for a few seconds and then your admin component was reloaded to and your new blog post is showing up now, incredible!!

Blog created successfully

Go take a look at your Blogs route. You will see the new blog is already on that page too!

New Blog on Blogs page

Go Crazy and add a few more blog articles so we have some sample data to work with, you want at least 5 test blogs. If you go over to your Blogs page, you can click on the links and see your markdown converted to HTML for your blog articles.

Markdown rendered as HTML

Editing Blog Articles

Now some day we may need to edit our blog posts. Let's add that functionality. We are going to use another dynamic segment to get the post slug for editing.

Create a new file in your admin route folder titled $edit.jsx.
This page is going to be pretty similar to our new.jsx page with a few slight changes. We need to get the post from the route of the blog post we clicked, then we need to populate the form with that data, perform the same validation, and update that data if needed.

Update your $edit.jsx file (./app/routes/admin/$edit.jsx)

import invariant from 'tiny-invariant';
import { getPostEdit } from "~/post";
import { redirect, Form, useActionData, useTransition, useLoaderData } from "remix";
import { updatePost } from "~/post";


export let loader = async({params}) => {
    invariant(params.edit, "expected params.edit");
    return getPostEdit(params.edit);
}

export let action = async ({ request }) => {
    let formData = await request.formData();

    let title = formData.get("title");
    let slug = formData.get("slug")
    let markdown = formData.get("markdown")
    let id = formData.get("id");

    let errors = {};
    if (!title) errors.title = true;
    if (!slug) errors.slug = true;
    if (!markdown) errors.markdown = true;

    if (Object.keys(errors).length) {
        return errors;
    }

    console.log('calling updatePost with id, title, slug, markdown: ', id, title, slug, markdown)
    await updatePost({id, title, slug, markdown});

    return redirect("/admin")
}

export default function PostSlug() {
    let errors = useActionData();
    let transition = useTransition();
    let post = useLoaderData();
    return (
            <Form method="post">
                <p>
                    <input className="hiddenBlogID" name="id" value={post.id}>
                    </input>
                </p>
                <p>
                    <label htmlFor="">
                        Post Title: {" "} {errors?.title && <em>Title is required</em>} <input type="text" name="title" defaultValue={post.title}/>
                    </label>
                  </p>
                  <p>
                      <label htmlFor=""> Post Slug: {" "} {errors?.slug && <em>Slug is required</em>} 
                      <input defaultValue={post.slug} id="slugInput" type="text" name="slug"/>
                  </label>
                  </p>
                  <p>
                      <label htmlFor="markdown">Markdown:</label>{" "} {errors?.markdown && <em>Markdown is required</em>} 
                      <br />
                      <textarea defaultValue={post.markdown} name="markdown" id="" rows={20} cols={30}/>
                  </p>
                  <p>
                      <button type="submit">{transition.submission ? "Updating..." : "Update Post"}</button>
                  </p>
            </Form>
        )
}
Enter fullscreen mode Exit fullscreen mode

Awesome. Now if you notice, this calls for getPostEdit and updatePost which don't exist in our Post module. Let's go add those.
Open post.js and the following 2 functions

//when we edit the post we want to return different data including the ID field
export async function getPostEdit(slug){
    //setup our prisma connection
    await prisma.$connect();

    // we will find the first database entry that matches the passed slug
   const foundSlug = await prisma.posts.findFirst({
        where: {
            slug: slug
        }
    })
    let id = foundSlug.id
    //let's extract the title 
    let title = foundSlug.title;
    // since we are editing and not rendering we want to pull the original markdown value stored in the db
    let markdown = foundSlug.markdown
    // we need to cleanup our database connection
    prisma.$disconnect();

    // let's send back the slug, the title, and our markdown 
    return { id, slug, title, markdown};
}
// When updating we need to reference the ID being updated
export async function updatePost(post){
    //Prisma connection 
    await prisma.$connect()
    // prisma create
    console.log('updatePost id', post.id)
    await prisma.posts.update({
        where: {
            id: post.id
        },
        data: {
            title: post.title,
            slug: post.slug,
            markdown: post.markdown
        }
    })

    // cleanup prisma connection 
    prisma.$disconnect();
    // let's send back the slug we created
    return getPost(post.slug)    
}
Enter fullscreen mode Exit fullscreen mode

Let's go test it out. On your Remix app, go to your Admin page and click on a post. The fields should populate with the blog data and you can edit it and then update your blog posts.
You may notice a field showing some strange letters and numbers, this is the post ID from the database, we will hide this in CSS. If we hide it in HTML then it's not sent with our post request.

Splendid! We now have a functional blog where we can read blog posts, we can create new blog posts, and we can edit existing blog posts. The only thing left to do is add some styling.

Styling your Remix App

We haven't done any of our own styling yet, just using the default create-remix styling. Let's expand on those by adding route specific styles that will only be added if the route matches.

In your App folder, create 2 CSS files admin.css and posts.css.

Styling Admin Pages

We are going to work with the admin.css file first.
I am going to define the following styles

.admin {
    display: flex;
    flex-direction: row;
}

.admin > h1 {
    padding-right: 2em;
}

.admin > nav {
    flex: 1;
    border-left: solid 2px #555;
    padding-left: 2em;
}

.hiddenBlogID {
    display: none;
}


.adminNewPostButton{
    margin-top: 2em;
    background-color: royalblue;
    color: white;
    border-radius: 10px;
    padding: 1em;
}



.adminTitle {
    font-size: x-large;
    color: crimson;
}

.remix-app__header{
    background-color: rgb(141, 20, 20);
}mix-app__header{
    background-color: crimson;
}
Enter fullscreen mode Exit fullscreen mode

Perfect, our Admin style sheet is ready to go. We are going to add it to our admin.jsx file using the remix export links method so that the CSS styles are only added when our admin page is rendered and removed when our admin page is not being rendered. We are going to import the stylesheet and then export it as a links.

Add the following to the top of your admin.jsx

import adminStyles from "~/styles/admin.css";

//create a stylesheet ref for the admin.css file 
export let links = () => {
    return [{rel: "stylesheet", href: adminStyles}]
}
Enter fullscreen mode Exit fullscreen mode

Go have a look at your Admin page, it should now look like this:

Styled Admin Page

We added a red banner in the top nav to show case we are in an admin route. We also Styled the Admin header and used a bit of flex. Additionally, we hid that blog id input. If you navigate to any other page, the styles are removed. Take a look at the dev tools and the CSS file disappears.

Styling Posts Page

Let's add some CSS to our posts.css file. I have designed some heading styles, and background just to put some styles on the page.

Edit post.css with the following:

.postTitle{
    text-decoration: none;
    color: rebeccapurple;
}

.postTitle:hover{
    text-decoration: underline;
    color: red;
}

.postList{
    list-style-type:circle;
}

.postDisplay  h1, h2, h3 {
    font-size: 72px;
    background: -webkit-linear-gradient(rgb(0, 255, 64), rgb(0, 71, 204));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.postDisplay p {
    font-size: 2em;
    margin-left: 2em;
}

.postDisplay ul {
    display: flex;
    flex-direction: column;
    text-transform: capitalize;
    background-color: whitesmoke;
    row-gap: 10px;
    margin-left: 2em;
}

.postDisplay > ul > li {
    color: #333;
    font-size: x-large;
    list-style-type: decimal-leading-zero;
}
Enter fullscreen mode Exit fullscreen mode

Awesome we have some styling being applied. Let's go import the styles to our blogs route files. At the top of the blogs index.jsx and the $slug.jsx file. Add the following same two lines.

import postStyles from "~/styles/posts.css";
export let links = () => {
    return [{rel: "stylesheet", href: postStyles}]
}
Enter fullscreen mode Exit fullscreen mode


`
Here is my Second Blog Post page with some styling applied

Blog Post Styled

Recap

Awesome, we now have a blog application, with our data being stored in MongoDB. We can add new articles, we can update existing articles, we can add styles and see how Remix applies it's logic to our application.

Final Folder Structure

Here is the Folder Structure at the end.
Final Folder Structure

Takeaways

Remix is an app for creating amazing user experiences, sometimes with React.
Remix is not just a react framework, though you can quickly get it up and running with React.
There is much more to Remix, this is just a quick app to showcase some features.
The back end of your application is added to the module, if you want to change that from MongoDB to SQL for example, you can simply update the post.js file and entirely change the back end to another provider. As I was writing this tutorial, I was initially using local storage, then local SQLLite through Prisma, and then switched to MongoDB because I wanted to showcase how to work with a cloud provider to separate our data storage from our app.

Deploying to Vercel

Let's get our application published to our Vercel site.

Close your running dev server.

Let's run a build npm run build

Let's deploy run vercel deploy ๐Ÿš€๐Ÿš€๐Ÿš€

My site was published to https://remix-blog-ten.vercel.app
Go take a look and play with it.

Full Source Code is available on Github here

Conclusion

I do hope that you learned something with this tutorial. This is my first tutorial of this magnitude, and on Remix. I'm a web developer but a junior React dev and still learning so I'm open to feedback if I've made mistakes or can improve this tutorial.

Part 2

Part 2 for this tutorial is live which covers Authentication, and Deleting posts. This protects the Admin Page from non-authenticated users. Check it out here: Part 2

Iโ€™m on @buymeacoffee. If you like my work, you can buy me a taco and share your thoughts ๐ŸŽ‰๐ŸŒฎ
Buy Me A Coffee

Top comments (6)

Collapse
 
vadimtm8 profile image
Vadim Egorov

Just what I was looking for!
Thank you so much.

Collapse
 
chrisbenjamin profile image
Chris Benjamin

Awesome, glad I could help!

Collapse
 
carnevalemno profile image
Matthew Carnevale

Hi, there! Thanks for this. It's really cool. Quick question, how would you protect visitors from accessing the admin page?

Collapse
 
chrisbenjamin profile image
Chris Benjamin

Iโ€™m glad you liked the tutorial. Please check out Part 2 of this tutorial that covers authentication and protection of the admin page. dev.to/chrisbenjamin/remix-blog-tu...

Collapse
 
carnevalemno profile image
Matthew Carnevale

Whoops now I see it, thanks lol.

Thread Thread
 
chrisbenjamin profile image
Chris Benjamin

I updated the articles today after your comment to include part 1 and and links to Part 2 l, I realized I forgot to do that previously and your comment reminded me so thank you!