Build a Blog Site with Next.js and Firebase Part 3 - Reading Each Post
Published Tuesday, January 19, 2021 — 11 minute read
Welcome to the third post in my new "Build a Blog Site with Next.js and Firebase" series! This series is pretty similar to a series I wrote in 2019: "Build a React & Firebase Blog Site". Because it's been well over a year since I published that series, I decided to create a new series and use the Next.js React framework this time. It's a fun framework to use, and I know so many people that are curious about it. I hope you enjoy the series!
- Read Part 1 of the series
- Read Part 2 of the series
- Read Part 4 of the series
- Read Part 5 of the series
Create a Page Component for Each Post
In the site we've been building together, there currently isn't a way for people to read posts individually. We have the home page, which lists all blog posts, and we have a page for creating new posts. The good news is that we can handle rendering individual posts on their own pages pretty easily with Next.js Dynamic Routes. We just need to make sure we name the file correctly. Let's create a page component that just displays the slug
from the URL as a test.
- Add
post.module.scss
under thestyles
directory:
1.PostPage {
2 img {
3 max-width: 100%;
4 border-radius: 12px;
5 border: 1px solid black;
6 }
7
8 h1 {
9 margin-top: 20px;
10 margin-bottom: 8px;
11 }
12
13 a {
14 display: block;
15 padding: 8px;
16 color: black;
17 }
18}
19
- Add a
post
directory topages
- Add
[slug].js
under thepost
directory:
1import { useRouter } from 'next/router';
2import styles from '@styles/post.module.scss';
3
4const PostPage = () => {
5 const router = useRouter();
6 const { slug } = router.query;
7
8 return (
9 <div className={styles.PostPage}>
10 <h1>Hello, from post: {slug}!</h1>
11 </div>
12 );
13};
14
15export default PostPage;
16
- Go to
https://localhost:3000/post/my-first-blog-post
in your browser - The page should look something like this:
- Commit and push your work to your repository:
1git add .
2git commit -m "Adding basic PostPage component"
3git push
4
Link to Each Post on the Home Page
Now that we have the pages for each post dynamically generating, we can link to them on the home page.
- In
pages/index.js
, add this<a>
element below the<p>
where you setdangerouslySetInnerHTML
with the post content:
1<a href={`/post/${post.slug}`}>Continue Reading</a>
2
- In
styles/index.module.scss
, addmargin-bottom: 16px;
to thep
element. - Below that style, add one for the
a
that sets thecolor
to#1a73e8
. - The home page should now look something like this:
- Commit and push your work to your repository:
1git add .
2git commit -m "Adding links to each post"
3git push
4
Load and Render Posts by Slug
Our PostPage
component doesn't do much yet, so let's take care of that now. We'll need to add a function for getting a post's details from our Firebase Realtime Database and then update the PostPage
component to render those details nicely.
- Add a new
getPostBySlug
function tolib/firebase.js
for getting the post from our Firebase Realtime Database:
1/*
2Retrieves the data for a single post from a given slug.
3*/
4export const getPostBySlug = async (slug) => {
5 initFirebase();
6
7 return await firebase
8 .database()
9 .ref(`/posts/${slug}`)
10 .once('value')
11 .then((snapshot) => snapshot.val());
12};
13
- Import
getPostBySlug
at the top ofpages/post/[slug].js
(you can remove theuseRouter
import):
1import { getPostBySlug } from '@lib/firebase';
2
- Right before
export default PostPage
, addgetServerSideProps
topages/post/[slug].js
.
1export async function getServerSideProps(context) {
2 const post = await getPostBySlug(context.query.slug);
3
4 return {
5 props: {
6 post,
7 },
8 };
9}
10
- Update the
PostPage
component to accept apost
prop and render the post:
1const PostPage = ({ post }) => (
2 <div className={styles.PostPage}>
3 <img src={post.coverImage} alt={post.coverImageAlt} />
4 <h1>{post.title}</h1>
5 <span>Published {getFormattedDate(post.dateCreated)}</span>
6 <p dangerouslySetInnerHTML={{ __html: post.content }}></p>
7 </div>
8);
9
PostPage
uses the samegetFormattedDate
function as the home page, so let's add autils.js
file under thelib
directory and export the function from there. You will have to restart you development server after this step if it's running.
1export const getFormattedDate = (milliseconds) => {
2 const formatOptions = {
3 weekday: 'long',
4 month: 'long',
5 day: 'numeric',
6 year: 'numeric',
7 timeZone: 'UTC',
8 };
9 const date = new Date(milliseconds);
10 return date.toLocaleDateString(undefined, formatOptions);
11};
12
- Import
getFormattedDate
in bothpages/index.js
andpages/post/[slug].js
:
1import { getFormattedDate } from '@lib/utils';
2
- If you go to
http://localhost:3000/post/my-second-blog-post
in your browser, the page should look like this:
- Commit and push your work to your repository:
1git add .
2git commit -m "Rendering posts in PostPage"
3git push
4
Add Site Navigation and Semantic HTML
Add a Layout component
We've got our post pages, but now we need a way to navigate back to the home page! We also want the navigation to look the same on each page, so let's make a component for it that'll also help us consolidate some page styles.
- Add a
components
directory at the root of your project. - Update
jsconfig.json
for the newcomponents
directory:
1{
2 "compilerOptions": {
3 "baseUrl": "./",
4 "paths": {
5 "@components": ["components"],
6 "@lib/*": ["lib/*"],
7 "@styles/*": ["styles/*"]
8 }
9 }
10}
11
- Add an
index.js
file for exporting all components from the directory. - Add a
Layout
directory under thecomponents
directory. - Add
Layout.jsx
tocomponents/Layout
:
1const Layout = ({ children }) => <div>{children}</div>;
2
3export default Layout;
4
- Update
components/index.js
to export Layout:
1export { default as Layout } from './Layout/Layout';
2
- Import
Layout
intopages/post/[slug].js
:
1import { Layout } from '@components';
2
- Update
PostPage
so that the rendered contents are wrapped by the newLayout
component:
1const PostPage = ({ post }) => (
2 <Layout>
3 <div className={styles.PostPage}>
4 <img src={post.coverImage} alt={post.coverImageAlt} />
5 <h1>{post.title}</h1>
6 <span>Published {getFormattedDate(post.dateCreated)}</span>
7 <p dangerouslySetInnerHTML={{ __html: post.content }}></p>
8 </div>
9 </Layout>
10);
11
- Restart your development server if it's running.
- If you go to
http://localhost:3000/post/my-second-blog-post
in your browser, the page should still look like this:
- Commit and push your work to your repository:
1git add .
2git commit -m "Adding new Layout component"
3git push
4
Add HTML landmarks to Layout
- Add
Layout.module.scss
tocomponents/Layout
:
1.Layout {
2 nav {
3 padding: 24px;
4 background-color: #1a73e8;
5
6 span {
7 a {
8 font-size: 1.6rem;
9 font-weight: bold;
10 color: white;
11 text-decoration: none;
12
13 &:hover {
14 text-decoration: underline;
15 }
16 }
17 }
18 }
19
20 main {
21 padding: 24px;
22 }
23}
24
- Import the styles in
components/Layout/Layout.jsx
:
1import styles from './Layout.module.scss';
2
- Add a
className
to the<div>
inLayout
and add two elements:<nav>
and<main>
.
1const Layout = ({ children }) => (
2 <div className={styles.Layout}>
3 <nav>
4 <span>
5 <a href="/">My Next.js Blog</a>
6 </span>
7 </nav>
8 <main>{children}</main>
9 </div>
10);
11
- Go to
https://localhost:3000/post/my-second-blog-post
in your browser. - It should look something like this and you should be able to go to the home page with the new link at the top:
- Commit and push your work to your repository:
1git add .
2git commit -m "Adding nav and link to home page to Layout"
3git push
4
Consolidate Page Styles
Take a look styles in index.module.scss
, create.module.scss
, and post.module.scss
. You should notice that all three of them have the same max-width
, margin
, and padding
CSS properties set and the same values for each. This will be one benefit of having a Layout
component: It helps you consolidate your styles and avoid repetition.
It also allows you to make styling changes accross all of your pages at once. For example, if you wanted to make the main content of your site narrower, you could update max-width
in this one place to do so. Let's remove these three properties from index.module.scss
, create.module.scss
, and post.module.scss
and use the new Layout
component on these pages.
- Import
Layout
inpages/index.js
andpages/create.js
:
1import { Layout } from '@components';
2
- Wrap the rendered content in
pages/index.js
andpages/create.js
with the opening and closingLayout
tags. - Delete the
max-width
,margin
, andpadding
fromindex.module.scss
,create.module.scss
, andpost.module.scss
. - Update the
main
styles incomponents/Layout/Layout.module.scss
to handlemax-width
,margin
, andpadding
for page content:
1main {
2 max-width: 700px;
3 margin: 0 auto;
4 padding: 24px;
5}
6
- All of your pages should have the same blue naviation bar at the top now.
- Commit and push your work to your repository:
1git add .
2git commit -m "Using Layout in all pages and consolidating styles"
3git push
4
One last thing
What if someone tries to go to a post that doesn't exist? Let's try it. Go to https://localhost:3000/post/abcdefg
in your browser. You should see a Server Error error that says "Cannot read property 'coverImage' of null"
. This is because the PostPage
component is expecting its post
prop to be defined. Since the post isn't found in the database, our getPostBySlug
is returning null
. So, the post
object passed to PostPage
is null and doesn't have any attributes we can access off of it.
- Import
useRouter
again in thePostPage
component and check to see ifpost
is defined. Redirect to/404
if the post doesn't exist.
1const PostPage = ({ post }) => {
2 const router = useRouter();
3
4 if (!post) {
5 router.push('/404');
6 return;
7 }
8
9 return (
10 <Layout>
11 <div className={styles.PostPage}>
12 <img src={post.coverImage} alt={post.coverImageAlt} />
13 <h1>{post.title}</h1>
14 <span>Published {getFormattedDate(post.dateCreated)}</span>
15 <p dangerouslySetInnerHTML={{ __html: post.content }}></p>
16 </div>
17 </Layout>
18 );
19};
20
Uh oh! There's another Server Error: "No router instance found. you should only use "next/router" inside the client side of your app." This is happening because this code is running on the server side of our app and the router.push
method is not supported there. You can read more about it at the link mentioned in the error.
- To fix it, let's add an additional check to our
if
statement:
1if (!post && typeof window !== 'undefined') {
2 router.push('/404');
3 return;
4}
5
If we run this, we still get a Server Error: "Cannot read property 'coverImage' of null". The if
statement we just added is mainly for the client-side and doesn't tell the server-side what to do if the post
object is undefined.
- Add one more
if
statement below the first one that just returnsnull
ifpost
isn't defined.
1const PostPage = ({ post }) => {
2 const router = useRouter();
3
4 if (!post) {
5 router.push('/404');
6 return;
7 }
8
9 if (!post) {
10 return null;
11 }
12
13 return (
14 <Layout>
15 <div className={styles.PostPage}>
16 <img src={post.coverImage} alt={post.coverImageAlt} />
17 <h1>{post.title}</h1>
18 <span>Published {getFormattedDate(post.dateCreated)}</span>
19 <p dangerouslySetInnerHTML={{ __html: post.content }}></p>
20 </div>
21 </Layout>
22 );
23};
24
- Check that you're redirected to a 404 page.
- Commit and push your work to your repository:
1git add .
2git commit -m "Redirecting to 404 if post doesn't exist"
3git push
4
- Celebrate!!! You did it!!! 🎉