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;
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;
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; };
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> </> ); }
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;
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;
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;
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;
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} />; }
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} />; }
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";
Wrap the calls to the providers around the call to children.
<Providers attribute='class' defaultTheme='system' enableSystem> <Header /> {children} </Providers>
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;
Now add in app/layout.tsx, like this:
<Header /> {children} <Footer />
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", };
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 }; };
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)