DEV Community

Donna Brown
Donna Brown

Posted on • Edited on

How to Build a Developer Blog using Next JS 13 and Contentlayer - Part three

Add more directories and pages

Make directories: about, blog, projects under the app directory. Create a page.tsx in each directory. Since the layout is not changing you don’t need to have a layout.tsx file in each directory. Placeholder text is included for app/about/page.tsx.

About Page
app/about/page.tsx

import React from "react";

const About = () => {
  return (
    <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0'>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos laborum
        aut voluptates qui vitae incidunt iusto ipsam, nam molestiae
        reprehenderit quisquam cum molestias ut nesciunt? Culpa incidunt nobis
        libero?
      </p>
      <p>Voluptate natus maiores, alias sapiente nisi possimus?</p>
      <p>
        Ex amet eu labore nisi irure sit magna. Culpa minim dolor consequat
        dolore pariatur deserunt aliquip nisi eu ex dolor pariatur enim. Lorem
        pariatur cillum ullamco minim nulla ex voluptate. Occaecat esse mollit
        ipsum magna consectetur nulla occaecat non sit sint amet. Pariatur quis
        duis ut laboris ipsum velit fugiat do commodo consectetur adipisicing ut
        reprehenderit.
      </p>
    </div>
  );
};

export default About;

Enter fullscreen mode Exit fullscreen mode

Blog Page
app/blog/page.tsx

import React from "react";
import { allPosts } from "contentlayer/generated";
import { compareDesc } from "date-fns";
import PostCard from "@/components/PostCard";

import "../../app/globals.css";

const Blog = () => {
  const posts = allPosts.sort((a, b) =>
    compareDesc(new Date(a.date), new Date(b.date))
  );
  return (
    <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0'>
      <div className='space-y-2 pt-6 pb-8 md:space-y-5'>
        <h1 className='text-3xl mb-8'>Developer Blog</h1>

        {posts.map((post, idx) => (
          <div key={idx}>
            <hr className='grey-200 h-1 mb-10'></hr>
            <PostCard key={idx} {...post} />
          </div>
        ))}
      </div>
    </div>
  );
};

export default Blog;


Enter fullscreen mode Exit fullscreen mode

Projects Page
This page contains three components.
components/Fork.tsx which draws a fork.

https://github.com/donnabrown77/developer-blog/blob/main/components/Fork.tsx
components/Star.tsx which draws a star.
https://github.com/donnabrown77/developer-blog/blob/main/components/Star.tsx

components/Card.tsx which displays the github project data display in a card.
https://github.com/donnabrown77/developer-blog/blob/main/components/Card.tsx

Card.tsx uses types created in the file types.d.ts in the root directory.

export type PrimaryLanguage = {
  color: string;
  id: string;
  name: string;
};

export type Repository = {
  description: string;
  forkCount: number;
  id?: number;
  name: string;
  primaryLanguage: PrimaryLanguage;
  stargazerCount: number;
  url: string;
};

type DataProps = {
  viewer: {
    login: string;
    repositories: {
      first: number;
      privacy: string;
      orderBy: { field: string; direction: string };
      nodes: {
        [x: string]: any;
        id: string;
        name: string;
        description: string;
        url: string;
        primaryLanguage: PrimaryLanguage;
        forkCount: number;
        stargazerCount: number;
      };
    };
  };
};

export type ProjectsProps = {
  data: Repository[];
};

export type SvgProps = {
  width: string;
  height: string;
  href?: string;
};

Enter fullscreen mode Exit fullscreen mode

You can provide a link to your github projects instead of accessing them this way but I wanted to display them on my website instead of making the users leave.

You will need to generate a personal access token from github. The github token is included in the .env.local file in this format:
GITHUB_TOKEN="Your token"

Go to your Github and your profile. Choose Settings. It’s near the bottom of the menu.

Go to Developer Settings. It’s at the bottom of the menu. Go to Personal access tokens.

Choose generate new token ( classic ). You’ll see a menu with various permissions you can check. Everything is unchecked by default. At a minimum, you will want to check “public_repo”, which is under “repo”, and you’ll also want to check “read:user”, which is under “user.” Then click “Generate token”. Save that token (somewhere safe make sure it doesn’t make its way into your repository), and put it in your .env.local file. Now the projects should be able to be read with that token.

More information: https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token.

app/projects/page.tsx

import React from "react";
import { GraphQLClient, gql } from "graphql-request";
import Card from "@/components/Card";
import type { DataProps, Repository } from "@/types";

/**
 *
 * @param
 * @returns displays the list of user's github projects and descriptions
 */

export default async function Projects() {
  const endpoint = "https://api.github.com/graphql";

  if (!process.env.GITHUB_TOKEN) {
    return (
      <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0'>
        <div className='mx-auto divide-y'>
          <div className='space-y-2 pt-6 pb-8 md:space-y-5'>
            <h1 className='text-left text-3xl font-bold leading-9 tracking-tight sm:leading-10 md:text-3xl md:leading-14'>
              Projects
            </h1>
            <p className='text-lg text-left leading-7'>
              Invalid Github token. Unable to access Github projects.
            </p>
          </div>
        </div>
      </div>
    );
  }
  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
    },
  });

  const query = gql`
    {
      viewer {
        login
        repositories(
          first: 20
          privacy: PUBLIC
          orderBy: { field: CREATED_AT, direction: DESC }
        ) {
          nodes {
            id
            name
            description
            url
            primaryLanguage {
              color
              id
              name
            }
            forkCount
            stargazerCount
          }
        }
      }
    }
  `;

  const {
    viewer: {
      repositories: { nodes: data },
    },
  } = await graphQLClient.request<DataProps>(query);

  return (
    <>
      <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0'>
        <div className='mx-auto divide-y'>
          <div className='space-y-2 pt-6 pb-8 md:space-y-5'>
            <h1 className='text-left text-3xl font-bold leading-9 tracking-tight sm:leading-10 md:text-3xl md:leading-14'>
              Projects
            </h1>
            <p className='text-lg text-left leading-7'>
              List of GitHub projects
            </p>
          </div>
          <div className='container py-4 mx-auto'>
            <div className='flex flex-wrap md:flex-wrap:nowrap'>
              {data.map(
                ({
                  id,
                  url,
                  name,
                  description,
                  primaryLanguage,
                  stargazerCount,
                  forkCount,
                }: Repository) => (
                  <Card
                    key={id}
                    url={url}
                    name={name}
                    description={description}
                    primaryLanguage={primaryLanguage}
                    stargazerCount={stargazerCount}
                    forkCount={forkCount}
                  />
                )
              )}
            </div>
          </div>
        </div>
      </div>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

This code checks for the github environment variable. If this variable is correct, it then creates a GraphQLClient to access the github api. The graphql query is set up to return the first 20 repositories by id, name, description, url, primary language, forks, and stars. You can adjust this to your needs by changing the query. The results are displayed in a Card component.

Since we have not yet created a navigation menu type localhost://about, localhost://blog, localhost://projects to see your pages.

Header, Navigation Bar, and Theme Changer

Make a directory called _data at the top level. Add the file headerNavLinks.ts to this directory. This file contains names of your directories.

_data/headerNavLinks.ts

const headerNavLinks = [
{ href: "/blog", title: "Blog" },
{ href: "/projects", title: "Projects" },
{ href: "/about", title: "About" },
];


export default headerNavLinks;
Enter fullscreen mode Exit fullscreen mode

Now add:
components/Header.tsx

"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import Navbar from "./Navbar";

const Header = () => {
  // useEffect only runs on the client, so now we can safely show the UI
  const [hasMounted, setHasMounted] = useState(false);

  // When mounted on client, now we can show the UI
  // Avoiding hydration mismatch
  // https://www.npmjs.com/package/next-themes#avoid-hydration-mismatch
  useEffect(() => {
    setHasMounted(true);
  }, []);
  if (!hasMounted) {
    return null;
  }

  return (
    <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0 pt-0'>
      <header className='flex items-center justify-between py-10'>
        <div>
          <Link href='#'>
            <div className='flex lg:px-0'>
              {/* logo */}
              <Link href='/'>
                <div
                  id='logo'
                  className='flex-shrink-0 flex items-center bg-primary  h-16 w-25 border-radius'
                >
                  <span
                    id='logo-text'
                    className='text-blue-800  dark:text-blue-400 font-weight:bold text-3xl'
                  >
                    Logo
                  </span>
                </div>
              </Link>
            </div>
          </Link>
        </div>
        <Navbar />
      </header>
    </div>
  );
};

export default Header;


Enter fullscreen mode Exit fullscreen mode

Next is the navigation bar.

app/components/NavBar.tsx

"use client";
import React, { useState } from "react";
import Link from "next/link";
import ThemeChanger from "./ThemeChanger";
import Hamburger from "./Hamburger";
import LetterX from "./LetterX";
import headerNavLinks from "@/data/headerNavLinks";
// names of header links are in
// separate file which allow them to be changed without affecting this component
/**
 *
 * @returns jsx to display the navigation bar
 */
const Navbar = () => {
  const [navShow, setNavShow] = useState(false);

  const onToggleNav = () => {
    setNavShow((status) => {
      if (status) {
        document.body.style.overflow = "auto";
      } else {
        // Prevent scrolling
        document.body.style.overflow = "hidden";
      }
      return !status;
    });
  };

  return (
    <div className='flex items-center text-base leading-5 '>
      {/* show horizontal nav link medium or greater width */}
      <div className='hidden md:block'>
        {headerNavLinks.map((link) => (
          <Link
            key={link.title}
            href={link.href}
            className='p-1 font-medium sm:p-4 transition duration-150 ease-in-out'
          >
            {link.title}
          </Link>
        ))}
      </div>

      <div className='md:hidden'>
        <button
          type='button'
          className='ml-1 mr-1 h-8 w-8 rounded py-1'
          aria-controls='mobile-menu'
          aria-expanded='false'
          onClick={onToggleNav}
        >
          <Hamburger />
        </button>
        {/* when mobile menu is open move this div to x = 0
              when mobile menu is closed, move the element to the right by its own width,
              effectively pushing it out of the viewport horizontally.*/}

        <div
          className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-100 opacity-95 duration-300 ease-in-out dark:bg-black ${
            navShow ? "translate-x-0" : "translate-x-full"
          }`}
        >
          <div className='flex justify-end'>
            <button
              type='button'
              className='mr-5 mt-14 h-8 w-8 rounded'
              aria-label='Toggle Menu'
              onClick={onToggleNav}
            >
              {/* X */}
              <LetterX />
            </button>
          </div>
          <nav className='fixed mt-8 h-full'>
            {headerNavLinks.map((link) => (
              <div key={link.title} className='px-12 py-4'>
                <Link
                  href={link.href}
                  className='text-2xl  tracking-widest text-grey-900 dark:text-grey-100'
                  onClick={onToggleNav}
                >
                  {link.title}
                </Link>
              </div>
            ))}
          </nav>
        </div>
      </div>
      <ThemeChanger />
    </div>
  );
};

export default Navbar;


Enter fullscreen mode Exit fullscreen mode

Now for the theme change code.
app/components/ThemeChanger.tsx

"use client";
import React, { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import Moon from "./Moon";
import Sun from "./Sun";

/**
 *
 * @returns jsx to switch based on user touching the moon icon
 */
const ThemeChanger = () => {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  if (!mounted) return null;

  return (
    <div>
      {theme === "light" ? (
        <button
          className='ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4  text-gray-900 hover:text-gray-400'
          aria-label='Toggle light and dark mode'
          type='button'
          onClick={() => setTheme("dark")}
        >
          <Moon />
        </button>
      ) : (
        <button
          className='ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4  text-gray-50 hover:text-gray-400'
          aria-label='Toggle light and dark mode'
          onClick={() => setTheme("light")}
        >
          <Sun />
        </button>
      )}
    </div>
  );
};

export default ThemeChanger;


Enter fullscreen mode Exit fullscreen mode

Links to the svg components Hamburger, LetterX, Moon, Sun, LetterX :
https://github.com/donnabrown77/developer-blog/blob/main/components/Hamburger.tsx
https://github.com/donnabrown77/developer-blog/blob/main/components/LetterX.tsx
https://github.com/donnabrown77/developer-blog/blob/main/components/Moon.tsx
https://github.com/donnabrown77/developer-blog/blob/main/components/Sun.tsx

Now set up the theme provider which calls next themes.
app/components/Theme-provider.tsx

"use client";

import React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";

// https://github.com/pacocoursey/next-themes/issues/152#issuecomment-1364280564

export function ThemeProvider(props: ThemeProviderProps) {
  return <NextThemesProvider {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

app/providers.tsx

"use client";

import React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";

// https://github.com/pacocoursey/next-themes/issues/152#issuecomment-1364280564
// needs to be called NextThemesProvider not ThemesProvider
// not sure why
export function Providers(props: ThemeProviderProps) {
  return <NextThemesProvider {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

Modify app/layout.tsx to call the theme provider.

Add these two lines to the top:

import Header from "@/components/Header";
import { Providers } from "./providers";
Enter fullscreen mode Exit fullscreen mode

Wrap the calls to the providers around the call to children.

<Providers attribute='class' defaultTheme='system' enableSystem>
<Header />
{children}
</Providers>
Enter fullscreen mode Exit fullscreen mode

In tailwind.config.ts, after plugins[], add:
darkMode: "class",

Run npm dev. You should have everything working except the footer.
For the footer, you can use the social icons here:
https://github.com/donnabrown77/developer-blog/blob/main/components/social-icons/Mail.tsx
I created a social-icons directory under app/components for the icons.
The footer is a component:
https://github.com/donnabrown77/developer-blog/blob/main/components/Footer.tsx

Footer uses a file called siteMetaData.js that you customize for your site.
_data/siteMetData.js

const siteMetadata = {
url: "https://yourwebsite.com",
title: "Next.js Coding Starter Blog",
author: "Your name here",
headerTitle: "Developer Blog",
description: "A blog created with Next.js and Tailwind.css",
language: "en-us",
email: "youremail@email.com",
github: "your github link",
linkedin: "your linkedin",
locale: "en-US",
};


module.exports = siteMetadata;
Enter fullscreen mode Exit fullscreen mode

Now add in app/layout.tsx, like this:


<Header />
{children}
<Footer />
Enter fullscreen mode Exit fullscreen mode

SEO
Next JS 13 comes with SEO features.

In app/layout.tsx, you can modify the defaults such as this:

export const metadata: Metadata = {
title: "Home",
description: "A developer blog using Next JS 13",
};
Enter fullscreen mode Exit fullscreen mode

For the blog pages, add this to app/posts/[slug]/page.tsx. This uses dynamic information, such as the current route parameters to return a metadata object.

export const generateMetadata = ({ params }: any) => {
const post = allPosts.find(
(post: any) => post._raw.flattenedPath === params.slug
);
return { title: post?.title, excerpt: post?.excerpt };
};
Enter fullscreen mode Exit fullscreen mode

Link to github project:
https://github.com/donnabrown77/developer-blog

Some of the resources I used:
https://nextjs.org/docs/app/building-your-application/routing/colocation
https://darrenwhite.dev/blog/nextjs-tailwindcss-theming
https://nextjs.org/blog/next-13-2#built-in-seo-support-with-new-metadata-api
https://darrenwhite.dev/blog/dark-mode-nextjs-next-themes-tailwind-css
https://claritydev.net/blog/copy-to-clipboard-button-nextjs-mdx-rehype
https://blog.openreplay.com/build-a-mdx-powered-blog-with-contentlayer-and-next/
https://www.sandromaglione.com/techblog/contentlayer-blog-template-with-nextjs
https://jpreagan.com/blog/give-your-blog-superpowers-with-mdx-in-a-next-js-project
https://jpreagan.com/blog/fetch-data-from-the-github-graphql-api-in-next-js
https://dev.to/arshadyaseen/build-a-blog-app-with-new-nextjs-13-app-folder-and-contentlayer-2d6h

Top comments (0)