Week 2Week 4

Week 3 - Continuing with Next.js!

~52 min read

Pre-Workshop

Play around with the App Router Playground. It's a great way to get familiar with Next.js and how it works. Like actually mess around in it, or this is gonna be a rough workshop.

Also, read through the following resources. We'll cover them again in the workshop so you don't have to understand everything, but it's good to have a general idea of what we'll be talking about.

Update ESLint

ESLint is a static code analysis tool that checks your JavaScript code for common problems, such as syntax errors, formatting issues, code style violations, and potential bugs.

However, sometimes we want to change what it checks for. For example, we want to use apostrophes in our tags for text. We can't do that with the default ESLint configuration, so we need to update it. It gets annoying to have to escape them every time, so we're just going to disable the rule.

Open .eslintrc and update it to look like this:

.eslintrc
{
  "extends": "next/core-web-vitals",
  "rules": {
    "react/no-unescaped-entities": "off"
  }
}

What is Routing?

This is where we start entering the "confusing" part of our workshops. Routing is a very important concept in web development, and it's something that you'll see in every web application. Next.js provides two options for routers:

  • App Router
  • Pages Router

The App Router is the newer router, and it's the one we'll be using. While it's conceptually harder to understand, it's much more powerful and flexible than the Pages Router. The Pages Router is the older router, and it's the one you'll see in most Next.js tutorials. It's easier to understand, but it's not as powerful or flexible as the App Router.

When it came to designing this curriculum, we chose towards teaching technology that will be used in the future, rather than technology that is used now. This is why we're using the App Router instead of the Pages Router. While projects still utilize the Pages Router (Next.js says it will stay supported and the App Router is opt-in), the App Router is the future of Next.js.

Let's start off with some important terminology.

Next.js Routing Terminology Visual

  • Tree: A convention for visualizing a hierarchical structure. For example, a component tree with parent and children components, a folder structure, etc.
  • Subtree: Part of a tree, starting at a new root (first) and ending at the leaves (last).
  • Root: The first node in a tree or subtree, such as a root layout.
  • Leaf: Nodes in a subtree that have no children, such as the last segment in a URL path.

If you've worked with Binary Search Trees before, this should be familliar to you. (Just more than two children.)

Files and Folders

Next.js uses a file-system based router where:

  • Folders are used to define routes. A route is a single path of nested folders, following the file-system hierarchy from the root folder down to a final leaf folder that includes a page.js file.
  • Files are used to create UI that is shown for a route segment.
    • page.js - Unique UI of a route and make routes publicly accessible. Think of this as the "main" file for a route. (Required)
    • layout.js - A layout component that wraps other components. Think of this as a "wrapper" for a route. (Highly Recommended)
    • not-found.js - A 404 page. (Optional)
    • loading.js - A loading page. (Optional)
    • and more

Taken from Next.js App Router Documentation on Routing Fundamentals

Working with Files

With all of these files, how exactly is a page made?

File to Hierarchy

As seen in the image above, Next.js compiles and combines all of our code from these various files into the Component Hierarchy of a single page.

This part will make more sense later today.

Talking about Layout

We talked a bit about the layout.js file and it's purpose in routing. You may have noticed that we have a layout.js file in our /app folder. This is called our Root Layout. The root layout is defined at the top level of the app directory and applies to all routes. This layout enables you to modify the initial HTML returned from the server.

While layouts are optional, the Root Layout isn't. This is because it contains the following:

/app/layout.js
import "./globals.css";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

The <html> and <body> tags should be familliar. The <head> in Next.js is automatically generated, so we don't need to worry about that.

Creating a new page

Create a new folder inside of /app called /blog. Inside of /blog, create a new file called page.js. Then, add the following code:

/app/blog/page.js
export default function Blog() {
  return <p>This is the blog!</p>;
}

Now, if we go to localhost:3000/blog, we should see our new page!

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Nested Routes

We made a new page by creating a folder and adding a page.js file to it. With nested routes, we can add deeper pages within the same URL path:

Route Segment

Nested Route File Structure

We won't be doing this right now, but it's important you know how we can make pages that are within the Component Hierarchy.

Nested Route Example

Colocation

Colocation is when we put files that are related to each other in the same folder. For example, we can put our page.js and layout.js files in the same folder. This is called colocating our files.

In addition to special files, you have the option to colocate your own files (e.g. components, styles, tests, etc) inside folders in the app directory.

This is because while folders define routes, only the contents returned by page.js or route.js are publicly addressable.

Colocation Example

Creating a global component

We just made a new page for our blog, but it's pretty boring. Let's add our Navbar to it. It surely isn't as tedious as copying and pasting the code from /app/page.js to /app/blog/page.js, right?

Well, with the idea of Colocation, we can actually create a global component for our Navbar that we can use on any page we want. This is because we can import components from other files into our pages. Let's do that now.

  1. First, let's create a folder for these "global" components.
  2. Create a new folder inside of /app called /components.
  3. Inside of /components, create a new file called Navbar.js. \
  4. Then, cut the code from /app/page.js and paste it into /app/components/Navbar.js.
  5. Now, we can import the Navbar component into any page we want!
/app/components/Navbar.js
export default function Navbar() {
  return (
    <nav className="border-b sticky top-0 bg-gray-900 text-gray-100 border-gray-800 z-10">
      <div className="h-14 max-w-7xl p-4 mx-auto flex items-center justify-between">
        <a href="/" className="font-medium text-lg md:hover:underline">
          My Website
        </a>
        <ul className="hidden md:flex items-center justify-end space-x-4 text-sm font-medium">
          <li className="md:hover:underline">
            <a href="/blog">Blog</a>
          </li>
          <li className="md:hover:underline">
            <a href="/photos">Photos</a>
          </li>
        </ul>
      </div>
    </nav>
  );
}
/app/page.js
import Navbar from "./components/Navbar";

export default function Home() {
  return (
    <div>
      <Navbar />
      <p>This is the home page!</p>
    </div>
  );
}
/app/blog/page.js
import Navbar from "../components/Navbar";

export default function Blog() {
  return (
    <div>
      <Navbar />
      <p>This is the blog!</p>
    </div>
  );
}

Now we have the Navbar on both pages!

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

export default

By now, you've probably noticed that we're using export default when making pages and for components. For any page.js, we can only have one export, and it must be a export default. For any other file that isn't special, we can have multiple exports. You're still restricted to one export default, but you can have multiple exports.

We reccomend using export default for components, but you can use export if you want. It's just a matter of preference. If you have multiple components stored in a file and need to export them, you can use export for each component. You can label the most important component as export default and the rest as export.

Using the Root Layout to simplify our code

Here's a question: "Do we want our Navbar on every page of our website?" The answer is yes. (Unless you don't. Then don't follow this part.)

We want our Navbar on every page of our website. So, instead of having to import the Navbar component into every page, we can import it directly into the Root Layout.

/app/layout.js
import "./globals.css";
import { Inter } from "next/font/google";
import Navbar from "./components/Navbar";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar />
        {children}
      </body>
    </html>
  );
}

Now if we go back, you'll see we have TWO Navbars on our pages. We can now remove the component and its import from /app/page.js and /app/blog/page.js.

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Now that we have the Navbar on every page, we can click on the Blog link and...we go to the Blog! But there's a problem you might've not noticed.

The page refreshes. This is not what we want. We want to be able to navigate to different pages without the page refreshing. One of the benefits of using Next.js is that we can do this, especially with the App Router.

How do we do this? We use something called the Link component.

You should be familiar with anchor tags from HTML: <a>

They let us link pages and websites together - we can still use them in Next.js, but we actually don’t want it for our website. We’ll want to use a Link component.

<Link> is a React component that extends the HTML <a> element to provide prefetching and client-side navigation between routes. It is the primary way to navigate between routes in Next.js.

How do we use it?

Open up /app/components/Navbar.js and replace the anchor tags with Link components:

/app/components/Navbar.js
import Link from "next/link";

export default function Navbar() {
  return (
    <nav className="border-b sticky top-0 bg-gray-900 text-gray-100 border-gray-800 z-10">
      <div className="h-14 max-w-7xl p-4 mx-auto flex items-center justify-between">
        <Link href="/" className="font-medium text-lg md:hover:underline">
          My Website
        </Link>
        <ul className="hidden md:flex items-center justify-end space-x-4 text-sm font-medium">
          <li className="md:hover:underline">
            <Link href="/blog">Blog</Link>
          </li>
          <li className="md:hover:underline">
            <Link href="/photos">Photos</Link>
          </li>
        </ul>
      </div>
    </nav>
  );
}

Notice how we had to import the Link component from next/link. This is a Next.js specific component, so we need to make sure we import it into the file. Now, if we click on the links, we should be taken to the correct page without the page refreshing!

If you're linking to an external website, you should still use the anchor tag. For example, if you want to link to Google, you can do <a href="https://google.com">Google</a> It's only for internal links that we use the Link component.

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Setting up our Project Structure

Now that you know how to create pages, create/use components, and link pages, let's create our overall site hierarchy. We'll be using the following structure:

app
├── components
│   ├── Navbar.js
│   └── ...
├── blog
│   ├── page.js
│   ├── layout.js
│   ├── ??? (mystery directory, what could it be?)
│   └── ...
├── photos
│   ├── page.js
│   ├── layout.js
│   └── ...
├── page.js
├── layout.js
└── ...

Create /app/blog/layout.js and /app/photos/layout.js and add the following code:

import Navbar from "../components/Navbar";

export default function Layout({ children }) {
  return <>{children}</>;
}

We're not going to be using these files right now, but we'll be using them later.

Create /app/photos/page.js and make sure it looks like the following:

/app/photos/page.js
export default function Photos() {
  return <p>This are my photos!</p>;
}

Do the same for /app/blog/page.js:

/app/blog/page.js
export default function Blog() {
  return <p>This is the blog!</p>;
}

Remember, since we added the Navbar to the Root Layout, we don't need to add it to the page. Now, if we go to localhost:3000/blog or localhost:3000/photos, we should see our new pages! The Navbar also helps us navigate between all three of our pages!

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Making everything look pretty/fancy/cool

We're going to create a theme for our website, which consists of a primary and secondary color scheme. The primary color scheme will be used for the Navbar and major elements, while the secondary color scheme will be used for minor elements.

Updating Tailwind CSS Configuration

Open up tailwind.config.js and update it to look like this:

tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      black: colors.black,
      white: colors.white,
      primary: colors.neutral,
      secondary: colors.blue,
    },
    extend: {},
  },
  plugins: [],
};

The colors we're using are from the Tailwind CSS Color Palette. We're using the neutral color palette for our primary color scheme and the blue color palette for our secondary color scheme.

If you want to use a different color palette, you can find all of the color palettes here.

But there's a problem - we don't have gray, one of our Navbar color schemes, in our tailwind.config.js file. Open up Navbar.js and update it to use our new primary color scheme:

This is because we didn't `extend`` the Tailwind CSS theme - we overrode it.

/app/components/Navbar.js
export default function Navbar() {
  return (
    <nav className="border-b sticky top-0 bg-primary-900 text-primary-100 border-primary-800 z-10">
      <div className="h-14 max-w-7xl p-4 mx-auto flex items-center justify-between">
        <Link href="/" className="font-medium text-lg md:hover:underline">
          My Website
        </Link>
        <ul className="hidden md:flex items-center justify-end space-x-4 text-sm font-medium">
          <li className="md:hover:underline">
            <Link href="/blog">Blog</Link>
          </li>
          <li className="md:hover:underline">
            <Link href="/photos">Photos</Link>
          </li>
        </ul>
      </div>
    </nav>
  );
}

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Customizing the Font

Now, let's customize our font. Open up /app/layout.js and you'll see the following code:

/app/layout.js
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

Next.js has built-in Font optimization with Google Fonts. We're using the Inter font, but you can use any font you want. Just replace Inter with the font you want to use. Make sure it's on Google Fonts!

Here are some examples (make sure you update the variable name as well):

import { Roboto } from "next/font/google";
const roboto = Roboto({ subsets: ["latin"] });
import { Poppins } from "next/font/google";
const poppins = Poppins({ subsets: ["latin"] });
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({ subsets: ["latin"] });

Working with Metadata

You'll also see the following in /app/layout.js:

/app/layout.js
export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

This is the information about a website you see when you share it, see it online, etc. It's similar to the title and description tags in HTML. We can use this to customize our website's metadata. Feel free to change the title and description to whatever you want.

/app/layout.js
export const metadata = {
  title: "My website",
  description: "Welcome to my website!",
};

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Adding more components (to fill time)

Here are some additional components you can add to your website. Feel free to add more or less if you want!

/app/components/Footer.js
export default function Footer() {
  return (
    <footer className="border-t text-center p-4 text-sm font-semibold bg-primary-100 text-primary-900 border-primary-200">
      <p>Me &copy; {new Date().getFullYear()}</p>
    </footer>
  );
}

Usage: <Footer />

Little hack: new Date().getFullYear() gets the current year. You'll always be up to date!

A smart place to import this is in the /app/layout.js file. This way, it's on every page.

Container

/app/components/Container.js
export default function Container({ children }) {
  return <div className="max-w-7xl mx-auto p-4">{children}</div>;
}

Usage: <Container>...</Container>

This is a wrapper component, meaning it wraps around other components. It's useful for adding padding, margins, etc. to a group of components. We'll be using this for responsive design purposes.

Buttons

/app/components/Buttons.js
export function FilledButton({ children }) {
  return (
    <DefaultButton style="bg-secondary-700 border-secondary-700 text-secondary-100 md:hover:bg-secondary-900 md:hover:border-secondary-900 md:hover:text-secondary-300">
      {children}
    </DefaultButton>
  );
}

export function OutlinedButton({ children }) {
  return (
    <DefaultButton style="border-primary-200 text-primary-200 md:hover:border-primary-400 md:hover:text-primary-400">
      {children}
    </DefaultButton>
  );
}

function DefaultButton({ children, style }) {
  return (
    <button
      className={`inline font-medium bg-transparent border rounded-full md:px-4 px-3.5 md:py-2 py-1.5 md:text-base text-sm transition-colors ${style}`}
    >
      {children}
    </button>
  );
}

Usage: <FilledButton>...</FilledButton> or <OutlinedButton>...</OutlinedButton>

This file exports two types of buttons: filled and outlined. But they are both based on a default button. The default button is the one that actually has the styles applied to it. The filled and outlined buttons just pass in a style prop to the default button.

Component props are very similar to that of function parameters. You can pass in a prop to a component and use it inside of the component. For example, we pass in a style prop to the default button and use it in the className attribute.

We'll be exploring more of this in the next workshop.

Section Header

/app/components/SectionHeader.js
export default function SectionHeader({ title, text }) {
  return (
    <div className="text-center space-y-2">
      <h2 className="text-4xl font-semibold">{title}</h2>
      <p className="text-primary-500 font-medium">{text}</p>
    </div>
  );
}

Usage: <SectionHeader title="..." text="..." />

This is a great time to make a commit! Make sure you add a commit message, and then push it to GitHub.

Practice

These workshops are growing in complexity, but you'll get more out of them if you practice. Practice also helps with understanding what we're going over, because it's some heavy stuff. Here are some ideas:

  • Create new pages!
  • Make even more components!
    • We have some examples above!
  • Go crazier with colors!
    • You can remove the code we added and go back to what it originally was to get the full Tailwind CSS color palette.
  • Mess with fonts!
  • Customize metadata!
    • If you add the metadata constant to other layout.js, you can customize their unique metadata as well. (Keep in mind it does pass downwards unless a page is overridden with its own metadata constant.)

Workshops