Introduction
Building full-stack web applications has never been this easy.
You don't need to write backend and frontend code to create fully functional web applications that authenticate users, store data, and upload files to cloud storage. Firebase allows you to build these applications easily within a few minutes.
In this tutorial, you'll learn how to build a blog that allows anyone to read posts and create an account as post authors to write posts, comment, and interact with other users.
The application uses Firebase Google authentication methods to sign users into the application and Firebase Firestore to perform CRUD operations when users create, delete, and comment on posts.
PS: A basic knowledge of React/Next.js is required to fully understand this article.
What is Firebase?
Firebase is a Backend-as-a-Service (Baas) owned by Google that enables us to build full-stack web applications in a few minutes. Services like Firebase make it very easy for front-end developers to build full-stack web applications with little or no backend programming skills.
Firebase provides various authentication methods, a NoSQL database, a real-time database, file storage, cloud functions, hosting services, and many more.
Why you should use Firebase?
Firebase is an exceptional platform that empowers developers to build full-stack applications effortlessly. Let's explore some unique features that set Firebase apart from other Backend as a Service (BaaS) platforms.
Complete Authentication System:
Firebase offers a complete authentication system that allows you to easily manage user authentication in your application. With Firebase Authentication, you can authenticate users using email/password, phone numbers, Google, Facebook, and more.
Cloud Firestore
Firebase Firestore is a flexible, scalable database for mobile, web, and server development. It allows you to store and sync data between your users in real-time, making it ideal for applications that require CRUD operations and complex data queries.
File Storage
Firebase provides an efficient Cloud Storage that enables you to upload and download files easily within your application. It seamlessly integrates with Firebase Authentication and Firebase Firestore, making it easy to build complex applications that requires serving and retrieving various file formats.
Hosting
Firebase provides hosting services that allow you to deploy web apps and static content with a single command. It provides SSL encryption, global CDN delivery, and continuous deployment, ensuring fast and reliable performance for your users.
Building the application interface with Next.js
Here, I'll walk you through creating the various components and pages within the blogging application.
The application is divided into four pages:
- Home page
- Login page
- Create Post page
- Post Slug page that displays the posts' content
Before we proceed, create a types.ts
file at the root of your Next.js project to define the attributes of the posts and comments within the application.
export interface Comment {
id: string;
content: string;
author_name: string;
pub_date: string;
}
export interface Post {
post_id: string;
author_id: string;
title: string;
content: string;
author_name: string;
image_url: string;
pub_date: string;
slug: string;
comments: Comment[];
}
Next, create a utils.ts
file within the Next.js app folder and copy the code snippet below into the file:
// Within the utils.ts file
export const getCurrentDate = (): string => {
const currentDate = new Date();
const day = currentDate.getDate();
const monthIndex = currentDate.getMonth();
const year = currentDate.getFullYear();
const suffixes = ["th", "st", "nd", "rd"];
const suffix = suffixes[(day - 1) % 10 > 3 ? 0 : (day - 1) % 10];
const months = [
"Jan.",
"Feb.",
"Mar.",
"Apr.",
"May",
"Jun.",
"Jul.",
"Aug.",
"Sept.",
"Oct.",
"Nov.",
"Dec.",
];
const month = months[monthIndex];
return `${day}${suffix} ${month} ${year}`;
};
//ππ» Create a unique slug from posts' titles
export const slugifySentences = (sentence: string): string => {
// Remove special characters and replace spaces with hyphens
const slug = sentence
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-");
// Generate 5 random letters
const randomLetters = Array.from({ length: 5 }, () =>
String.fromCharCode(97 + Math.floor(Math.random() * 26))
).join("");
return `${slug}-${randomLetters}`;
};
The getCurrentDate
function accepts the current date as a parameter and returns it in a more readable format, suitable for use within blog posts. The slugifySentences
function creates a slug from each post's title.
The Home Page
The application home page displays all the available posts and allow users to read their preferred posts.
Copy the code snippet below into the app/page.tsx
file.
"use client";
import Image from "next/image";
import Nav from "./components/Nav";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function Home() {
const [posts, setPosts] = useState<Post[]>();
const shortenText = (text: string): string => {
return text.length <= 55 ? text : text.slice(0, 55) + "...";
};
useEffect(() => {
fetchAllPosts();
}, []);
const fetchAllPosts = async () => {
// ππ» fetch all posts
};
return (
<div>
<Nav />
<main className='p-8 w-full flex lg:flex-row flex-col items-center gap-5 flex-wrap justify-center'>
{posts?.map((post) => (
<Link
href={`/posts/${post.slug}`}
className='cursor-pointer lg:w-1/3 rounded-lg w-full border-2 h-[400px] bg-white'
key={post.post_id}
>
<Image
src={post.image_url}
alt='Image'
width={300}
height={300}
className='w-full h-2/3 rounded-t-lg'
/>
<section className='h-1/3 w-full p-4 flex items-center'>
<p className='font-semibold text-xl text-blue-500'>
{shortenText(post.title)}
</p>
</section>
</Link>
))}
</main>
</div>
);
}
The code snippet above fetches all the existing blog posts and displays their titles and cover images.
There is also a Nav component that displays the blog title and the Sign In button.
Create a components
folder containing the Nav
component within the Next.js app folder and update it as shown below.
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { handleSignOut } from "../utils";
import { usePathname } from "next/navigation";
export default function Nav() {
const [displayName, setDisplayName] = useState<string>("");
const pathname = usePathname();
useEffect(() => {
if (localStorage.getItem("user")) {
const user = JSON.parse(localStorage.getItem("user") || "");
setDisplayName(user.displayName);
}
}, []);
return (
<nav className='w-full py-4 border-b-2 px-8 text-center flex items-center justify-between sticky top-0 bg-[#f5f7ff]'>
<Link href='/' className='text-2xl font-bold'>
Blog
</Link>
{displayName ? (
<div className='flex items-center gap-5'>
<p className='text-sm font-semibold'>{displayName}</p>
{pathname !== "/posts/create" && (
<Link href='/posts/create' className='underline text-blue-500'>
Create Post
</Link>
)}
<button
className='bg-red-500 text-white px-6 py-3 rounded-md'
onClick={handleSignOut}
>
Log Out
</button>
</div>
) : (
<Link
href='/login'
className='bg-blue-500 text-white px-8 py-3 rounded-md'
>
Sign in
</Link>
)}
</nav>
);
}
The Nav component checks if the user is authenticated to determine which buttons to display. If the user is not authenticated, it renders the Sign-in button; otherwise, it displays the user's name, a Create Post button, and a Log Out button.
The Login Page
The Login page simply displays a Google sign-in button that enables users to log in using their Gmail account.
To create the Login page, first, create a login folder within the Next.js app folder. Inside the login folder, create two files: layout.tsx
and page.tsx
.
cd app
mkdir login && cd login
touch layout.tsx page.tsx
Copy the code snippet below into the page.tsx
file. It renders a button that enables users to sign in to the application using their Gmail account.
"use client";
import { useState } from "react";
export default function Nav() {
const [loading, setLoading] = useState<boolean>(false);
const handleGoogleSignIn = () => {
setLoading(true);
//ππ» handle Firebase Google authentication
};
return (
<div className='w-full min-h-screen flex flex-col items-center justify-center'>
<h2 className='font-bold text-2xl mb-6'>Sign into Blog</h2>
<button
className='w-1/3 border-2 border-gray-600 mb-6 rounded-md hover:bg-black hover:text-white px-8 py-4'
disabled={loading}
onClick={handleGoogleSignIn}
>
{loading ? "Signing in..." : "Sign in with Google"}
</button>
</div>
);
}
Update the layout.tsx
file as shown below. It changes the page's title.
import type { Metadata } from "next";
import { Sora } from "next/font/google";
const inter = Sora({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Log in | Blogging App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body className={inter.className}>{children}</body>
</html>
);
}
The Posts pages
First, create a posts folder containing a create
folder and a [..slug]
folder within the Next.js app folder.
The create folder adds a /posts/create
client route to the application, and the [..slug]
folder creates a dynamic route for each blog post.
Next, create a page.tsx
file within the /posts/create
folder and add a form that enables users to enter the details of a blog post.
"use client";
import { useEffect, useState } from "react";
import Nav from "@/app/components/Nav";
import { getCurrentDate, slugifySentences } from "@/app/utils";
export default function CreatePost() {
const [coverPhoto, setCoverPhoto] = useState<string>("");
const [content, setContent] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [uploading, setUploading] = useState<boolean>(false);
const handleFileReader = (e: React.ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();
if (e.target.files && e.target.files[0]) {
reader.readAsDataURL(e.target.files[0]);
}
reader.onload = (readerEvent) => {
if (readerEvent.target && readerEvent.target.result) {
setCoverPhoto(readerEvent.target.result as string);
}
};
};
const handleCreatePost = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setUploading(true);
console.log({
title,
content,
author_name: userData.displayName,
pub_date: getCurrentDate(),
slug: slugifySentences(title),
comments: [],
});
//ππ» save blog post to the database
};
return (
<div>
<Nav />
<main className='md:px-8 py-8 px-4 w-full'>
<form className='flex flex-col w-full' onSubmit={handleCreatePost}>
<label htmlFor='title' className='text-sm text-blue-600'>
Title
</label>
<input
type='text'
name='title'
id='title'
value={title}
required
onChange={(e) => setTitle(e.target.value)}
className=' px-4 py-3 border-2 rounded-md text-lg mb-4'
/>
<label htmlFor='content' className='text-sm text-blue-600'>
Content
</label>
<textarea
name='content'
rows={15}
value={content}
required
onChange={(e) => setContent(e.target.value)}
id='content'
className=' px-4 py-3 border-2 rounded-md mb-4'
></textarea>
<label htmlFor='upload' className='text-sm text-blue-600'>
Upload Cover Photo
</label>
<input
type='file'
name='upload'
id='upload'
required
onChange={handleFileReader}
className=' px-4 py-3 border-2 rounded-md mb-4'
accept='image/jpeg, image/png'
/>
<button
type='submit'
className='bg-blue-600 mt-4 text-white py-4 rounded-md'
disabled={uploading}
>
{uploading ? "Creating post..." : "Create Post"}
</button>
</form>
</main>
</div>
);
}
The [...slug]
page displays the contents of a blog post. Add a page.tsx
file within the folder and copy the code snippet below into the file.
"use client";
import Comments from "@/app/components/Comments";
import Nav from "@/app/components/Nav";
import { extractSlugFromURL } from "@/app/utils";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
export default function Post() {
const params = usePathname();
const router = useRouter();
const slug = extractSlugFromURL(params);
const [loading, setLoading] = useState<boolean>(true);
const [post, setPost] = useState<Post | null>(null);
return (
<div>
<Nav />
<main className='w-full md:p-8 px-4'>
<header className='mb-6 py-4'>
<h2 className='text-3xl text-blue-700 font-bold mb-2'>
{post?.title}
</h2>
<div className='flex'>
<p className='text-red-500 mr-8 text-sm'>
Author: <span className='text-gray-700'>{post?.author_name}</span>
</p>
<p className='text-red-500 mr-6 text-sm'>
Posted on: <span className='text-gray-700'>{post?.pub_date}</span>
</p>
</div>
</header>
<div>
<p className=' text-gray-700 mb-3'>{post?.content}</p>
</div>
</main>
<Comments comments={post?.comments} post_id={post?.post_id} />
</div>
);
}
The code snippet above displays the post's title, author, published date, and content, and passes the post's comments into a Comments
component.
Finally, create a Comments
component that accepts a post's comments as props and renders the available comments, including an input field for adding new comments.
"use client";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { getCurrentDate } from "../utils";
export const generateCommentID = () =>
Math.random().toString(36).substring(2, 10);
export default function Comments({
comments,
post_id,
}: {
comments: Comment[] | undefined;
post_id: string | undefined;
}) {
const [user, setUser] = useState({ displayName: "", u_id: "" });
const [newComment, setNewComment] = useState<string>("");
const [postingComment, setPostingComment] = useState<boolean>(false);
useEffect(() => {
if (localStorage.getItem("user")) {
const user = JSON.parse(localStorage.getItem("user") || "");
setUser(user);
}
}, []);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ππ» postComment();
};
if (!user.displayName)
return (
<main className='p-8 mt-8'>
<h3 className=' font-semibold'>
You need to{" "}
<Link href='/login' className='text-blue-500 underline'>
sign in
</Link>{" "}
before you can comment on this post
</h3>
</main>
);
return (
<main className='p-8 mt-8'>
<h3 className='font-semibold text-xl mb-4 text-blue-500'>Comments</h3>
<form onSubmit={handleSubmit} className='mb-8'>
<textarea
className='w-full p-4 border border-gray-300 rounded-md mb-2'
placeholder='Leave a comment'
value={newComment}
required
onChange={(e) => setNewComment(e.target.value)}
/>
<button
className='bg-blue-500 text-white px-4 py-2 rounded-md'
disabled={postingComment}
>
{postingComment ? "Posting..." : "Post Comment"}
</button>
</form>
{comments && comments.length > 0 && (
<h3 className='font-semibold text-xl mb-4 text-blue-500'>
Recent comments
</h3>
)}
<div className='w-full flex items-start md:flex-row flex-col justify-center gap-4 flex-wrap'>
{comments?.map((comment) => (
<div
className='bg-white p-4 rounded-lg md:w-1/4 w-full shadow-lg'
key={comment.id}
>
<p className='mb-4'>{comment.content}</p>
<div>
<h4 className='font-semibold text-sm'>{comment.author_name}</h4>
<p className='text-gray-500 text-sm'>{comment.pub_date}</p>
</div>
</div>
))}
</div>
</main>
);
}
The code snippet above accepts the post's comments as props and renders them within the component. However, it ensures that only authenticated users can create and view existing comments. Unauthenticated users can only read a blog post.
Congratulations! You've completed the user interface for the application.
How to add Firebase to a Next.js app
In this section, you'll learn how to set up Firebase project and add it to a Next.js application. A Firebase project has all the features provided by Firebase and enables you to use them within your software applications.
First, you need to visit the Firebase console and sign in with a Gmail account.
Create a Firebase project and select the </> icon to create a new Firebase web app.
Provide your app name and register it.
Install the Firebase SDK in your Next.js project by running the code snippet below.
npm install firebase
Next, Create a firebase.ts file at the root of your Next.js project and copy the Firebase configuration code for your app into the file.
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth, GoogleAuthProvider } from "firebase/auth";
import { getStorage } from "firebase/storage";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "***********",
authDomain: "***********",
projectId: "**************",
storageBucket: "**********",
messagingSenderId: "************",
appId: "********************",
};
//ππ» Initialize Firebase
const app =
getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
//ππ» required functions
const db = getFirestore(app);
const auth = getAuth(app);
const storage = getStorage(app);
const googleProvider = new GoogleAuthProvider();
//ππ» required exports
export { db, auth, googleProvider, storage };
The code snippet above sets up Firebase in your Next.js application. It initializes Firebase Firestore, authentication, and storage instances, allowing you to access these features within the application.
Before you can start using these features, you'll need to set them up.
Configuring the Firebase features
For this application, you'll need to set up Firebase Storage for saving cover images for posts, Firebase Firestore for storing post contents, and Firebase Authentication for user authentication.
To get started, select Build from the left-hand panel, then click on Firestore Database to create a database.
Create the database in test mode, and pick your closest region.
Next, select Storage from the sidebar menu to set up the cloud storage for the files within the application.
Finally, select Authentication and pick Google from the list of authentication providers.
Select the support email for the Firebase project and click Save to enable Google authentication.
Congratulations. You can start saving files to the Firebase Cloud Storage, interacting with the Firebase Firestore for CRUD operations, and authenticating users via Firebase Google Authentication.
Handling user authentication within the application
Here, you'll learn how to sign users in and out of the application and how to retrieve the current user details from Firebase.
Within the login/page.tsx
file, update the handleGoogleSignIn
function as done below:
"use client";
import { useState } from "react";
import { handleSignIn } from "../utils";
import { googleProvider } from "../../../firebase";
import { GoogleAuthProvider } from "firebase/auth";
export default function Nav() {
const [loading, setLoading] = useState<boolean>(false);
const handleGoogleSignIn = () => {
setLoading(true);
handleSignIn(googleProvider, GoogleAuthProvider);
};
return (
<div className='w-full min-h-screen flex flex-col items-center justify-center'>
<h2 className='font-bold text-2xl mb-6'>Sign into Blog</h2>
<button
className='w-1/3 border-2 border-gray-600 mb-6 rounded-md hover:bg-black hover:text-white px-8 py-4'
disabled={loading}
onClick={handleGoogleSignIn}
>
{loading ? "Signing in..." : "Sign in with Google"}
</button>
</div>
);
}
The code snippet above triggers the handleSignIn
function when a user clicks the Sign in button.
Add the handleSignIn
function to the utils.ts
file. It redirects the user to the Google authentication page to enable the application to access the user's profile details.
import { signInWithPopup } from "firebase/auth";
import { auth } from "../../firebase";
export const handleSignIn = (provider: any, authProvider: any) => {
signInWithPopup(auth, provider)
.then((result) => {
const credential = authProvider.credentialFromResult(result);
const token = credential?.accessToken;
//ππ» sign in successful
if (token) {
const user = result.user;
//ππ» log user's details to the console
console.log({ displayName: user.displayName, u_id: user.uid });
window.location.replace("/");
}
})
.catch((error) => {
console.log({ error: error.message });
alert(
`Refresh page! An error occurred while signing out - ${error.message}`
);
});
};
To sign users out of the application, create a handleSignOut
function within the utils.ts
file and copy the code snippet below into the file. It logs the current user out of the application.
import { signOut } from "firebase/auth";
import { auth } from "../../firebase";
export const handleSignOut = () => {
signOut(auth)
.then(() => {
window.location.replace("/");
})
.catch((error) => {
alert(
`Refresh page! An error occurred while signing out - ${error.message}`
);
});
};
Protecting pages from unauthenticated users
Firebase allows us to determine whether the current user is signed in or not. This can be helpful when you need to protect certain pages from unauthenticated users, such as the Post creation page.
Add the code snippet below to the posts/create/page.tsx
file. It listens to the authentication state of the signed-in user and updates the current view of the application based on the authentication status.
import { onAuthStateChanged } from "firebase/auth";
const [userData, setUserData] = useState<any>({});
export default function PostCreate() {
useEffect(() => {
onAuthStateChanged(auth, (user) => {
user ? setUserData(user) : router.back();
});
}, [router]);
return <div>{/** -- UI elements --*/}</div>;
}
How to save and retrieve posts from Firebase Firestore
Here, you'll learn how to save and retrieve from Firebase, and also how to update each posts when a user adds a comment on a post.
Creating new posts
Update the handleCreatePost
function within the posts/create/page.tsx
file as done below:
import { addDoc, collection, updateDoc, doc } from "firebase/firestore";
import { auth, db, storage } from "../../../../firebase";
import { getCurrentDate, slugifySentences } from "@/app/utils";
import { ref, uploadString, getDownloadURL } from "firebase/storage";
const handleCreatePost = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setUploading(true);
//ππ» add post to Firebase Firestore
const docRef = await addDoc(collection(db, "posts"), {
author_id: userData.uid,
title,
content,
author_name: userData.displayName,
pub_date: getCurrentDate(),
slug: slugifySentences(title),
comments: [],
});
//ππ» creates a storage reference using the docRef id
const imageRef = ref(storage, `posts/${docRef.id}/image`);
if (coverPhoto) {
await uploadString(imageRef, coverPhoto, "data_url").then(async () => {
//ππ» Gets the image URL
const downloadURL = await getDownloadURL(imageRef);
//ππ» Updates the docRef, by adding the logo URL to the document
await updateDoc(doc(db, "posts", docRef.id), {
image_url: downloadURL,
});
});
setUploading(false);
alert("Post created successfully!");
router.push("/");
}
};
Now, let me explain the function:
The code snippet below adds the author's ID and name, the post's title, content, published date, and slug to a posts
collection within the database.
const docRef = await addDoc(collection(db, "posts"), {
author_id: userData.uid,
title,
content,
author_name: userData.displayName,
pub_date: getCurrentDate(),
slug: slugifySentences(title),
comments: [],
});
After saving the post's details to Firebase Firestore, the function uploads the post's cover image to Firebase Cloud Storage and updates the posts collection with a new attribute (image_url
) containing the URL of the uploaded cover image.
// creates a storage reference using the docRef id
const imageRef = ref(storage, `posts/${docRef.id}/image`);
if (coverPhoto) {
await uploadString(imageRef, coverPhoto, "data_url").then(async () => {
//ππ» Gets the uploaded image URL
const downloadURL = await getDownloadURL(imageRef);
//ππ» Updates the docRef, by adding the cover image URL to the document
await updateDoc(doc(db, "posts", docRef.id), {
image_url: downloadURL,
});
});
}
Fetching existing posts
To fetch all the saved posts from Firebase, add the code snippet below within the app/page.tsx
file.
import { collection, getDocs } from "firebase/firestore";
import { useEffect, useState } from "react";
import { db } from "../../firebase";
export default function Home() {
const [posts, setPosts] = useState<Post[]>();
useEffect(() => {
fetchAllPosts();
}, []);
const fetchAllPosts = async () => {
const querySnapshot = await getDocs(collection(db, "posts"));
const posts: any = [];
querySnapshot.forEach((doc) => {
posts.push({ ...doc.data(), post_id: doc.id });
});
setPosts(posts);
};
return <div>{/** -- UI elements --*/}</div>;
}
The code snippet above retrieves the saved posts from Firebase and displays them within the application.
Adding comments to posts
To add comments to a post, update the postComment
function with the Comments.tsx
file to fetch the particular post and add the newly created comment to the comments
array attribute.
import { getCurrentDate } from "../utils";
import { doc, updateDoc, arrayUnion } from "firebase/firestore";
import { db } from "../../../firebase";
//ππ» generate comment ID
export const generateCommentID = () => {
return Math.random().toString(36).substring(2, 10);
};
const postComment = async (post_id: string) => {
try {
const postRef = doc(db, "posts", post_id);
await updateDoc(postRef, {
comments: arrayUnion({
id: generateCommentID(),
content: newComment,
author_name: user.displayName,
pub_date: getCurrentDate(),
}),
});
alert("Comment posted successfully");
} catch (err) {
console.log(err);
alert("An error occurred while posting your comment. Please try again.");
}
};
Congratulations. You've completed the tutorial.
Conclusion
So far! Youβve learned the following:
- what is Firebase
- how to add Firebase to a Next.js app
- how to work with Firebase Auth, Storage, and Database
- how to build a blog app with Next.js and Firebase
The source for this tutorial is available here.
Thank you for reading and If you enjoyed this blog and want to learn more, check out my other articles.
Top comments (1)
this is a solid guide. thanks