Go to home page

Build a Blog Site with Next.js and Firebase Part 3 - Reading Each Post

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!


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.

  1. Add post.module.scss under the styles 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;
  }
}
  1. Add a post directory to pages
  2. Add [slug].js under the post 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;
  1. Go to https://localhost:3000/post/my-first-blog-post in your browser
  2. The page should look something like this:

White webpage with black text that reads "Hello, from post: my-first-blog-post!".

  1. 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.

  1. In pages/index.js, add this <a> element below the <p> where you set dangerouslySetInnerHTML with the post content:
<a href={`/post/${post.slug}`}>Continue Reading</a>
  1. In styles/index.module.scss, add margin-bottom: 16px; to the p element.
  2. Below that style, add one for the a that sets the color to #1a73e8.
  3. The home page should now look something like this:

Web page listing blog posts. Each has a black and white image of a random cat, a title in bold text, a date, excerpt, and blue link that reads "Continue Reading".

  1. 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.

  1. Add a new getPostBySlug function to lib/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());
};
  1. Import getPostBySlug at the top of pages/post/[slug].js (you can remove the useRouter import):
import { getPostBySlug } from '@lib/firebase';
  1. Right before export default PostPage, add getServerSideProps to pages/post/[slug].js.
export async function getServerSideProps(context) {
  const post = await getPostBySlug(context.query.slug);

  return {
    props: {
      post,
    },
  };
}
  1. Update the PostPage component to accept a post 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>
);
  1. PostPage uses the same getFormattedDate function as the home page, so let's add a utils.js file under the lib 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);
};
  1. Import getFormattedDate in both pages/index.js and pages/post/[slug].js:
import { getFormattedDate } from '@lib/utils';
  1. If you go to http://localhost:3000/post/my-second-blog-post in your browser, the page should look like this:

Webpage showing a black and white image of a random cat from PlaceKitten.com, the blog post title in bold font, the publish date, and the post content.

  1. 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.

  1. Add a components directory at the root of your project.
  2. Update jsconfig.json for the new components directory:
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@components": ["components"],
      "@lib/*": ["lib/*"],
      "@styles/*": ["styles/*"]
    }
  }
}
  1. Add an index.js file for exporting all components from the directory.
  2. Add a Layout directory under the components directory.
  3. Add Layout.jsx to components/Layout:
const Layout = ({ children }) => <div>{children}</div>;

export default Layout;
  1. Update components/index.js to export Layout:
export { default as Layout } from './Layout/Layout';
  1. Import Layout into pages/post/[slug].js:
import { Layout } from '@components';
  1. Update PostPage so that the rendered contents are wrapped by the new Layout 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>
);
  1. Restart your development server if it's running.
  2. If you go to http://localhost:3000/post/my-second-blog-post in your browser, the page should still look like this:

Webpage showing a black and white image of a random cat from PlaceKitten.com, the blog post title in bold font, the publish date, and the post content.

  1. Commit and push your work to your repository:
git add .
git commit -m "Adding new Layout component"
git push

Add HTML landmarks to Layout

  1. Add Layout.module.scss to components/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;
  }
}
  1. Import the styles in components/Layout/Layout.jsx:
import styles from './Layout.module.scss';
  1. Add a className to the <div> in Layout 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>
);
  1. Go to https://localhost:3000/post/my-second-blog-post in your browser.
  2. It should look something like this and you should be able to go to the home page with the new link at the top:

The same webpage as before but with a blue bar at the top and a white link to the home page that reads "My Next.js Blog".

  1. 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.

  1. Import Layout in pages/index.js and pages/create.js:
import { Layout } from '@components';
  1. Wrap the rendered content in pages/index.js and pages/create.js with the opening and closing Layout tags.
  2. Delete the max-width, margin, and padding from index.module.scss, create.module.scss, and post.module.scss.
  3. Update the main styles in components/Layout/Layout.module.scss to handle max-width, margin, and padding for page content:
main {
  max-width: 700px;
  margin: 0 auto;
  padding: 24px;
}
  1. All of your pages should have the same blue naviation bar at the top now.
  2. 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.

  1. Import useRouter again in the PostPage component and check to see if post 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.

  1. 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.

  1. Add one more if statement below the first one that just returns null if post 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>
  );
};
  1. Check that you're redirected to a 404 page.
  2. Commit and push your work to your repository:
git add .
git commit -m "Redirecting to 404 if post doesn't exist"
git push
  1. Celebrate!!! You did it!!! <span role="img" aria-label="party popper emoji">🎉</span>