Archived Build a Blog Site with Next.js and Firebase Part 3 - Reading Each Post
Archived
đ¨ ATTENTION đ¨
You are currently viewing an archived post. The information here may no longer be accurate or maybe Ashlee just decided not to publish the post as publicly anymore.
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:
.PostPage {
img {
max-width: 100%;
border-radius: 12px;
border: 1px solid black;
}
h1 {
margin-top: 20px;
margin-bottom: 8px;
}
a {
display: block;
padding: 8px;
color: black;
}
}
- Add a
post
directory topages
- Add
[slug].js
under thepost
directory:
import { useRouter } from 'next/router';
import styles from '@styles/post.module.scss';
const PostPage = () => {
const router = useRouter();
const { slug } = router.query;
return (
<div className={styles.PostPage}>
<h1>Hello, from post: {slug}!</h1>
</div>
);
};
export default PostPage;
- 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:
git add .
git commit -m "Adding basic PostPage component"
git push
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:
<a href={`/post/${post.slug}`}>Continue Reading</a>
- 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:
git add .
git commit -m "Adding links to each post"
git push
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:
/*
Retrieves the data for a single post from a given slug.
*/
export const getPostBySlug = async (slug) => {
initFirebase();
return await firebase
.database()
.ref(`/posts/${slug}`)
.once('value')
.then((snapshot) => snapshot.val());
};
- Import
getPostBySlug
at the top ofpages/post/[slug].js
(you can remove theuseRouter
import):
import { getPostBySlug } from '@lib/firebase';
- Right before
export default PostPage
, addgetServerSideProps
topages/post/[slug].js
.
export async function getServerSideProps(context) {
const post = await getPostBySlug(context.query.slug);
return {
props: {
post,
},
};
}
- Update the
PostPage
component to accept apost
prop and render the post:
const PostPage = ({ post }) => (
<div className={styles.PostPage}>
<img src={post.coverImage} alt={post.coverImageAlt} />
<h1>{post.title}</h1>
<span>Published {getFormattedDate(post.dateCreated)}</span>
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
</div>
);
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.
export const getFormattedDate = (milliseconds) => {
const formatOptions = {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
};
const date = new Date(milliseconds);
return date.toLocaleDateString(undefined, formatOptions);
};
- Import
getFormattedDate
in bothpages/index.js
andpages/post/[slug].js
:
import { getFormattedDate } from '@lib/utils';
- 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:
git add .
git commit -m "Rendering posts in PostPage"
git push
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:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@components": ["components"],
"@lib/*": ["lib/*"],
"@styles/*": ["styles/*"]
}
}
}
- Add an
index.js
file for exporting all components from the directory. - Add a
Layout
directory under thecomponents
directory. - Add
Layout.jsx
tocomponents/Layout
:
const Layout = ({ children }) => <div>{children}</div>;
export default Layout;
- Update
components/index.js
to export Layout:
export { default as Layout } from './Layout/Layout';
- Import
Layout
intopages/post/[slug].js
:
import { Layout } from '@components';
- Update
PostPage
so that the rendered contents are wrapped by the newLayout
component:
const PostPage = ({ post }) => (
<Layout>
<div className={styles.PostPage}>
<img src={post.coverImage} alt={post.coverImageAlt} />
<h1>{post.title}</h1>
<span>Published {getFormattedDate(post.dateCreated)}</span>
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
</div>
</Layout>
);
- 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:
git add .
git commit -m "Adding new Layout component"
git push
Add HTML landmarks to Layout
- Add
Layout.module.scss
tocomponents/Layout
:
.Layout {
nav {
padding: 24px;
background-color: #1a73e8;
span {
a {
font-size: 1.6rem;
font-weight: bold;
color: white;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
main {
padding: 24px;
}
}
- Import the styles in
components/Layout/Layout.jsx
:
import styles from './Layout.module.scss';
- Add a
className
to the<div>
inLayout
and add two elements:<nav>
and<main>
.
const Layout = ({ children }) => (
<div className={styles.Layout}>
<nav>
<span>
<a href="/">My Next.js Blog</a>
</span>
</nav>
<main>{children}</main>
</div>
);
- 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:
git add .
git commit -m "Adding nav and link to home page to Layout"
git push
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
:
import { Layout } from '@components';
- 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:
main {
max-width: 700px;
margin: 0 auto;
padding: 24px;
}
- All of your pages should have the same blue naviation bar at the top now.
- Commit and push your work to your repository:
git add .
git commit -m "Using Layout in all pages and consolidating styles"
git push
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.
const PostPage = ({ post }) => {
const router = useRouter();
if (!post) {
router.push('/404');
return;
}
return (
<Layout>
<div className={styles.PostPage}>
<img src={post.coverImage} alt={post.coverImageAlt} />
<h1>{post.title}</h1>
<span>Published {getFormattedDate(post.dateCreated)}</span>
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
</div>
</Layout>
);
};
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:
if (!post && typeof window !== 'undefined') {
router.push('/404');
return;
}
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.
const PostPage = ({ post }) => {
const router = useRouter();
if (!post) {
router.push('/404');
return;
}
if (!post) {
return null;
}
return (
<Layout>
<div className={styles.PostPage}>
<img src={post.coverImage} alt={post.coverImageAlt} />
<h1>{post.title}</h1>
<span>Published {getFormattedDate(post.dateCreated)}</span>
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
</div>
</Layout>
);
};
- Check that youâre redirected to a 404 page.
- Commit and push your work to your repository:
git add .
git commit -m "Redirecting to 404 if post doesn't exist"
git push
- Celebrate!!! You did it!!! đ