20
loading...
This website collects cookies to deliver better user experience
npx create-remix@latest
create-remix@latest
. Enter 'y' and press Enter remix-blog
. Feel free to name yours however you like. npm install
we are going to say yes, so put in y
and then press enter to get up and running quickly.cd remix-blog
if you called your project something else, be sure to replace remix-blog with your project name. npm i -g vercel
vercel link
n
and press enter.
You should get an output like thisnpm run dev
npx create-remix@latest
app. If so, fanstastic!!! If not, check your terminal for errors and go back if necessary. root.jsx
and make some adjustments. 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...
npm install prisma --save-dev
npx prisma
If you get an error, then Prisma did not install correctly, check the terminal output. npx prisma init
which will create our Prisma folder and .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. DATABASE_URL="mongodb+srv://remix_user:[email protected]/MyFirstDatabase"
Remix_Blog
posts
slug
with a value of my-first-post
markdown
with a value of #This is my first post
title
with a value of My First Post
mongodb.net/Remix_Blog
;
DATABASE_URL="mongodb+srv://remix_user:[email protected]/Remix_Blog"
generator client {
provider = "prisma-client-js"
previewFeatures = ["mongoDb"]
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
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)
model posts {
id String @id @default(dbgenerated()) @map("_id") @db.ObjectId
markdown String
slug String
title String
}
export default function Blogs() {
return <h1>Blogs Route</h1>
}
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. post.js
, (./app/post.js)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;
}
export default function Blogs()
import { getPosts } from '~/post';
export let loader = () => {
return getPosts();
}
[
{
id: '61a914e90b627f455a212145',
markdown: '#This is my first post',
slug: 'my-first-post',
title: 'My First Post'
}
]
import { Link, useLoaderData } from 'remix';
<li>
tag since this should be unique. // 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>
)
}
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. npm install marked
in your VSCode terminal. post.js
file let's import this at the topimport { marked } from "marked";
npm install tiny-invariant
in your VSCode terminal post.js
module with the getPost() function. 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};
}
$slug.jsx
. ./app/routes/blogs/$slug.jsx
with this codeimport { 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}}/>
)
}
admin.jsx
. admin
index.jsx
file. admin.jsx
as followsimport { 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>
)
}
index.jsx
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>
)
}
admin.jsx
and lets update our import to include Outlet import { Outlet, Link, useLoaderData } from 'remix';
<main>
tag we setup earlier in this same file. <main>
{/* Outlet renders the /admin/index.jsx */}
<Outlet />
</main>
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>
)
}
post.js
module to allow us to create entries in our MongoDB database.post.js
and add the following createPost() functionexport 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)
}
$edit.jsx
. 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. $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>
)
}
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)
}
create-remix
styling. Let's expand on those by adding route specific styles that will only be added if the route matches. admin.css
and posts.css
. admin.css
file first. .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;
}
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.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}]
}
posts.css
file. I have designed some heading styles, and background just to put some styles on the page. 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;
}
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}]
}
npm run build
vercel deploy
🚀🚀🚀