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)