Max Next – Section 11 – Complete App Example


Courses

Updated Sep 13th, 2022

Table of Contents

Chapter Details

Chapter 173. Module Introduction

Creating a blog with articles written in markdown.

Chapter 174. Setting Up The Core Pages

Always a good place to start. Contact.js and “posts” folder with “index.js” inside and dynamic “[slug].js” to represent a post-detail page.

Chapter 175. Getting Started With The Home Page

Creates a “hero” component and “featured-posts” component and brings in two CSS files.

Chapter 176. Adding The “Hero” Component

Simple hero image, title and paragraph. Bring in Image from next/image. Create in public folder a images sub folder and a site folder within that and so path will be “/images/site/yourPic.png” and define inline width and height props.

Chapter 177. Adding Layout & Navigation

function Layout(props) {
  return(
  <Fragment>
    <MainNavigation />
    <main>{props.children}</main>
  </Fragment>
  )
}

export default Layout

Note the main div that holds props.children may be able to be hidden when a mainNavigation menu is open.

In “main-navigation.js” we use Link from “next/link” and wrap Logo component in Link.

Note: Special behavior of link, when the child you pass to Link is not plain text, so for example, when you pass it your own component or any other html content, then Link will not automatically render an anchor tag by default so we should bring our own. Keep the href on the Link. This is not a bug.

Chapter 178. Time To Add Styling & A Logo

Download and bring in some CSS files for “main-navigation.js” and “logo.js”

Chapter 179. Starting Work On The “Featured Posts” Part

Start by outputting dummy posts. Create a “components/home-page/featured-posts.js” file.

Also a “posts-grid” and “post-items” all in a “components/posts” folder. In “posts-grid.js” have an unordered list that maps over posts to render each post-item component.

Chapter 180. Adding A Post Grid & Post Items

In the post-item import Link and bring your own anchor tag because we have complex html structure. He wants the entire item to be clickable so there is an “li>Link>a>div” structure.

const {title, image, excerpt, date, slug} = props.post

Add a posts subfolder in the images folder. Each post gets it’s own folder for each image. Is this necessary?

Will need to add a “formattedDate” constant. I just wrote similar code:

const formattedDate = new Date(date).toLocaleString("en-US", {
  day: "numeric",
  month: "long",
  year: "numeric"
})

Note: He uses the Link tag to render the card’s images with a height of 200 and width of 300 and also changing the “layout” property to “responsive.” But images can not be optimized at build time using next export, meaning skip next Link if building a static site.

Need to construct constant variables for “imagePath” and “linkPath”:

const imagePath = `/images/posts/${slug}/${image}`
const linkPath = `/posts/${slug}`

Chapter 181. Rendering Dummy Post Data

Add a post prop and key prop to the <PostItem /> comp in the “post-grid.js” file.

Mentions he will end up with a pretty long prop chain, and could solve with redux or context but will stick with the prop drilling, (HomePage > FeaturedPosts > PostsGrid > PostItem).

Temporarily add DUMMY_POSTS array of objects to the “index.js” file homepage.

In “post-item.js,” add subfolders for each post in the “posts” folder.

Chapter 182. Adding the “All Posts” Page

Create an “all-posts.js” file in “components/posts” folder to build out the “AllPosts” component which leverages the “PostsGrid” component for the second time.

Also create, in the “pages/posts/index.js” file, build out the “AllPostsPage” component to return the <AllPosts /> component.

Note: Max says he wants to keep his page component lean and focused on data fetching.

Copies the DUMMY_POSTS array of objects to the “pages/posts/index.js” file and passes this to the “AllPosts” component.

Chapter 183. Working On The “Post Detail” Page

In the “[slug].js” file we build out the “PostDetailPage” component to show the actual content of a post and will do so by rending a “PostContent” component. In the “components/posts” folder we add a subfolder named “post-detail” and inside of that folder two more subfolders, “post-content.js” and “post-header.js” folders.

Note: At this point I am really scratching my head wondering why so many nested components? Part of it is he is using CSS modules and has a CSS file for each comp.

Build out “post-header.js” file/component.

In “post-content.js” file, he types out a single DUMMY_POST above the main component, adding a “content” key set to a string of markdown text and then reference down in the returned JSX after translating to markdown with a third party package.

Intro to markdown. It’s less code and can be translated into html or JSX with third party packages.

Chapter 184. Rendering Markdown As JSX

Install “react-markdown” package and import ReactMarkdown from “react-markdown.” Back in the “post-content.js” file we add the “ReactMarkdown” component tags and in between pass the content and see the translated markdown text.

Chapter 185. Adding Markdown Files As A Data Source

Where do we store our actual posts. A database is a decent choice but we are developers, we can write the blog posts in the IDE and use our local file system.

We create a “content/posts” or just a “posts” folder in the root folder. In this folder we create files with the “.md” file extension. We need to add metadata as key-values pairs in YAML format using the gray-matter concept and library, which is just three dashes above and below the necessary metadata.

npm install gray-matter
---
title: 'Getting Started with Markdown and NextJS'
date: '2022-10-15'
image: getting-started-nextjs.png
excerpt: 'This is an excerpt'
isFeatured: true
---

# This is a title

This is some regular text with a [link](https://google.com)

Name the markdown file the same as the folder name in public/images/posts folder because we will connect these!

Chapter 186. Adding Functions To Read & Fetch Data From Markdown Files

The goal is to load all of the posts, and the featured posts, using our new data-source strategy. Add a “lib” folder in the root folder with a “posts-util.js” file.

Install new package “gray-matter” and import “matter” which allows us to split metadata from markdown in markdown file.

In the “posts-util.js” file, ‘import fs from “fs”‘ and ‘import, “path” from “path,”‘ and build out a “getAllPosts” function with help of the “fs.readdirSync()” method.

Note: fs.readdirSync will read all the contents synchronously, so in a blocking way. This means it will read the contents of the entire directory in on go.

Create the path you will pass “fs.readdirSync” and keep in mind “process.cwd()” points to the overall project folder.

const postsDirectory = path.join(process.cwd(), 'posts')

function getPostData(filename) {

  const filePath = path.join(postsDirectory, filename)
  const fileContent = fs.readFileSync(filePath, 'utf-8')
  const {data, content} = matter(fileContent)

  const postSlug = filename.replace(/\.md$/, '') // removes the file extension

  const postData = {slug: postSlug, ...data, content,}

  return postData
}

function getAllPosts() {
  const postFiles = fs.readdirSync(postsDirectory)

  const allPosts = postFiles.map(postFile => {
    return getPostData(postFile)
  })

  // get the latest posts first
  const sortedPosts = allPosts.sort((postA, postB) => postA.date > postB.date ? -1 : 1)

  return sortedPosts
} 

Note: We are using a map function instead of a “for…of” loop because we want to return an array. Here is the “for…of” loop code:

for (const postFile of postFiles) {
    const postData = getPostData(postFile)  
  }

And to get the featured posts, create the following function:

function getFeaturedPosts() {
  const allPosts = getAllPosts()
  
  const featuredPosts = allPosts.filter(post => post.isFeatured)

  return featuredPosts 
}

Export the functions created in this file.

Chapter 187. Using Markdown Data For Rendering Posts

To show featured posts on the homepage leverage the “getStaticProps” data-fetching function in the “pages/index.js” file, above the “export default HomePage” line:

export function getStaticProps() {
  const featuredPosts = getFeaturedPosts()

  return {
    props: {
      posts: featuredPosts
    }
  }
}

Our posts will not change that frequently so GSSP is not needed. Also do not need a revalidate key.

For the “allPosts” page, in the “pages/posts/index.js” file, again above the “export default” line:

export function getStaticProps() {
  const allPosts = getAllPosts()

  return {
    props: {
      posts: allPosts
    }
  }
}

Chapter 188. Rendering Dynamic Post Pages & Paths

Now working on the single-post-page, rendering the actual post content.

We can leverage with just a little re-factoring the “getPostData” function we just created to be more flexible by being able to receive a filename/slug with or without an extension.

Rename the parameter to “postIdentifier” an then create the slug right away as the first step.

const postsDirectory = path.join(process.cwd(), 'posts')

function getPostData(postIdentifier) {
  const postSlug = fileName.replace(/\.md$/, '') // removes the file extension
  const filePath = path.join(postsDirectory, `${postSlug}.md` )
  const fileContent = fs.readFileSync(filePath, 'utf-8')
  const {data, content} = matter(fileContent)

  const postData = {slug: postSlug, ...data, content,}

  return postData
}

Now add a “getStaticProps” function in the “[slug].js” file getting “params” from “context”

Here we can make a case for adding revalidate. Adding revalidate for single posts makes sense. Update without rebuilding entire app to fix typo. We also need to pair “getStaticProps” with “getStaticPaths” so we can tell it which posts to pre-generate.

export function getStaticProps(context) {
   const {params} = context
   const {slug} = params

   const postData = getPostData(slug)

   return {
     props: {
       post: postData
     },
     revalidate: 600 // once every ten minutes
   }
}

export function getStaticPaths() {
  const postFilenames = getPostsFiles()
  // removes the file extension
  const slugs = postFilenames.map(fielName => fileName.replace(/\.md$/, ""))
  

  return {
    paths: slugs.map(slug => ({params: {slug: slug} })),
    fallback: false
  }
}

We can use the previously-created “getAllPosts” function, but this is more than we need, so we can refactor that a bit, move out the line “fs.readdirSync(postsDirectory)” into it’s own “getPostsFiles” function.

export function getPostsFiles() {
  return fs.readdirSync(postsDirectory)
}

We can style as needed.

Chapter 189. Rendering Custom HTML Elements with React Markdown

In the next two lectures, we’ll fine-tune ReactMarkdown to overwrite some default HTML elements it renders.

Unfortunately, since I recorded these lectures, ReactMarkdown received a major update and the shown syntax changed slightly (i.e. what will be shown in the next two lectures has to change).

But thankfully, only minor adjustments are needed.

This link here shows the differences between all the code we’ll write in the next lectures and adjustments that will be needed for React Markdown v6: https://github.com/mschwarzmueller/nextjs-course-code/compare/3b99cd0…a9fb64b

So when we write code that is marked as “red” in the linked document, you should switch to the “green” version instead (when using ReactMarkdown v6 – check your package.json file to find out which version you’re using).

Chapter 190. Rendering Images With The “Next Image” Component (From Markdown)

We need to be able to display images and code-snippets in the blog-post content. Rendering this will face some small challenges.

image in markdown

![text](path.png)

Next renders image to basic img tag unless you override

ReactMarkdown gets special “renderers” prop where you can set to use next/image with necessary attributes

next/link nested gives error so get tricky by looking for paragraphs

Chapter 191. Rendering Code Snippets From Markdown

Add code snippet in markdown with three backticks as opening and closing tags. with code like paragraph. Also install “react-syntax-highlighter” package to hilite code very simply. google to find docs/repo. we use prism w one minor adj.

import {Prism of SyntaxHighlighter} from 'react-syntax-highlighter'
import {atomDark} from 'react-syntax-highlighter/dist/cjs/styles/prism'

There are other themes, you can use auto-completion to checkout.

code(code) {
  const {} = code
  return <SyntaxHighighter style={atomDark} language={language} children={value} />
}

Looks good but is the code snippet scrollable?

Chapter 192. Preparing The Contact Form

In “contact-form.js” build out ContactForm component function so you can add this to “contact.js” file. Structure of the form is form.controls.control.label&input with a button.

Goal is to send form data on submit to API route that talks to the database.

Chapter 193. Adding The Contact API Route

Create “contact.js” file in the “api” folder to create a route of “/api/contact.” Within the file add a handler function that checks the type of request and runs some validation. If it fails the validation it send a status code with some JSON. If it passes it cleans-up/sets a data object, connects to the database ,and stores (console.log for now). Also sends a 201 status code and a successful JSON message.

Chapter 194. Sending Data From The Client To The API Route

In “contact-form.js” file have a sendMessageHandler function connected to the form onSubmit prop lower in code. In this function we send an fetch request to send data to the API route.

For this form data capture this we leverage the useState hook to go for the two-way-binding approach to listen to inputs (instead of the useRef hook).

// just under function ContactForm()

const [enteredEmail, enteredEmail] = useState('')
const [enteredName, enteredName] = useState('')
const [enteredMessage, enteredMessage] = useState('')

// down in the form on the inputs add onChange={} and value={}
// repeat for all

value={enteredEmail}
onChange={() => setEnteredEmail(event.target.value)}

We will want to add client-side validation (we skip here), and then JSON.stringify({}) this in the fetch body key in config object.

Note that you need to make sure the field names match the one’s expected in the API route.

Chapter 195. Storing Messages With MongoDB In A Database

let client

try {
  client = await MongoClient.connect('connectionstring')
} catch (error) {
  res.status(500).json({message: 'Could not connect to database'})
  return
}

const db = client.db()

try {
  const result = await db.collection('messages').insertOne(newMessage)
  newMessage.id = result.insertedId
} catch (error) {
  client.close()
  res.status(500).json({message: 'Storing Message Failed'})
}

client.close()

res.status(201).json({message: 'Successfully stored message!', message: newMessage})

No logic for fetching data since the user shouldn’t see. We need to build a UI for ourselves as the admin or site owner. We could also just dive into the database manually.

Chapter 196. Adding UI Feedback With Notifications

Download “ui” folder into the components folder which has a “notification.js” file and CSS file. Earlier in the course when managing notifications we leveraged the “useContext” hook/API because we triggered the notification message from multiple components/different places in our application. In this chapter we just set up locally.

In the “contact-form.js” file import Notification from “../ui/Notification.” Need new state for [requestStatus]. This time around we will use async/await with fetch function instead of .then().catch() since it is a bit more convenient. Do this by converting sendMessageHandler function to async an storing the result of await fetch in a constant named response. Now we use the response:

const data = await response.json()

if (!response.ok) {
  throw new Error(data.message || "Something went wrong!")
}

Grab the bulk of this code and move it into a separate function named “sendContactData.” We add higher in the same “contact-form.js” file.

async function sendContactData (contactDetails) {
  const response = await fetch('/api/contact', {
    method: 'POST',
    body: JSON.stringify({
      contactDetails
    })
    headers: {'Content-Type':'application/json'}
})

const data = await response.json()

if (!response.ok) {
  throw new Error(data.message || "Something went wrong!")
}
}

Re-configure the “sendMessageHandler” function to call this new function but also set the “setRequestStatus” state to ‘pending’ at first and success if it works and fails if it doesn’t work with the help of a try/catch block.

async function sendMessageHandler(event) {
  setRequestStatus('pending')

  try {
    await sendContactData({
      email: enteredEmail,
      name: enteredName,
      message: enteredMessage
    })
    
    setRequestStatus('success')

} catch (error) {
    setRequestStatus('error')
  }
}

Now we have the different status updates and we can use them to show our notification. Render notification component which we are importing based on the status and with different data. So under the code above, we want to derive the data for the notification, and initially it will be undefined and check if status is equal to pending and set object.

let notification //initially undefined

if (requestStatus === 'pending') {
  notification = {
    status: 'pending',
    title: 'Sending message...',
    message: 'Your message is on its way'
  }
}

if (requestStatus === 'success') {
  notification = {
    status: 'success',
    title: 'Success...',
    message: 'Your message sent sucessfully'
  }
}

if (requestStatus === 'error') {
  notification = {
    status: 'error',
    title: 'Error ending message...',
    message: requestError // need to get out of error object and set separate state for that
  }
}

To get message out of the error object:

// just inside ContactForm component
const [requestError, setRequestError] = useState() //initially undefined

//in the sendMessageHandler catch block
setRequestError(error.message)

The component is fairly large now so maybe a good time to split into separate components. Down in the JSX we want to conditionally show the notification just after the closing form tag.

{notification && <Notification status={notification.status} title={notification.title} message={notification.message}/>}

//becomes

{notification && (
  <Notification
    status={notification.status}
    title={notification.title}
    message={notification.message}
  />
)}

Note: breaking the code above into multiple lines is what requires the parentheses, and this answers a lingering question of mine as to when this is needed.

So now the message can be shown and we need to implement the removal of the message automatically after a few seconds. And we can do this with the help of the useEffect hook to run a function whenever the request status changes and then reset to undefined after X seconds.

useEffect({
  if (requestStatus === 'success' || requestStatus === 'error') {
    const timer = setTimeout(() => {
    setRequestStatus(null) // resets
    setRequestError(null) // resets
    }, 3000)

   return () => clearTimeout(timer)
  }
}, [requestStatus])

Note that if the “request status” changes and we run the function again we need to reset the timer.

As a first test send and check the console for errors and ensure making it into the database and as a second test you can break the connection string and see if it you get error message on the send.

As a last step we can clear the inputs. In the “sendMessageHandler” function, at the bottom of the try block and after the line setting the request status to success, set the 3 pieces of state values to an empty string. Remember that we are binding the state values back to the inputs using the value attribute, (In addition to the onChange prop; this is what is meant by two-way binding). In summary, if we change the state the value will change.

Chapter 197. Adding “head” Data

Don’t forget to set metadata before deploying! Some should be added to all pages, (mainly the viewport meta element and the favicon)

// in _app.js
// be sure to import Head from 'next/head'

function MyApp({ Component, pageProps}) {
  return(
    <Layout>
    <Head>
      <meta name='viewport' content='width=device-width, initial-scale=1' />
    </Head>
    <Component {...pageProps} />
    </Layout>
  )
}

For page-specific metadata we want to go into all of our pages and import the Head component as well to set things like title and meta-name.

<Head>
<title>Page Title Here</title>
<meta name="description" content="This is a blurb"/>
</Head>

In the “[slug].js” file we use dynamic title and content values by getting from the props.

<Head>
<title>{props.post.title}</title>
<meta name="description" content={props.posts.excerpt} />
</Head>

Now you will see in the page’s source code if you looked up via “view > source.” Look up all the meta tags available to you for use on the internet. An example of this is an image that Facebook will show when you share your post. Twitter cards and many others as well.

<meta property=“og:image” content=“http://example.com/picture.jpg” />
<meta property=”og:image:width” content=”180″ />
<meta property=”og:image:height” content=”110″ />

Chapter 198. Adding A “_document.js” File

Allows you to define general structure, add attribute on the html element itself or add extra elements to use with React Portals.

import Document, { HTML, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render () {
    return (
      <Html lang='en'>
        <Head>
        <body>
          <Main />
          <NextScript />
        </body>
        </Head>
      </Html>
    )
  }
}

export default MyDocument

Note that when adding a “_document.js” file you will need to restart your server.

Chapter 199. Using React Portals

The notification functionality is working but gets randomly added to to the html element tree. The location where it gets added may not make sense semantically and make the page less accessible. So we have React Portals where we can specifically render a component anywhere in our component tree but added in a different place in the html Dom tree. We need to add an extra hook in the “_document.js” file

// In _dcoument.js file right under <NextScript />
<div id="notifications"></div>

import ReactDOM from ‘react-dom’ and tweak the return() to wrap in ReactDOM.createPortal method where the first argument is the JSX and the second argument is a selector where we select the DOM element where it should be ported to. We use the native browser’s API for selecting DOM elements.

// In notification.js file

import ReactDOM from 'react-dom'

return ReactDOM.createPortal((Same-JSX-Goes-Here), document.getElementById('notifications'))

Now if we go to the elements tab in the developer tools, and send a message, we can see the notification markup change.

React Portals is a nice feature, not Next.JS exclusive, but easy to use with Next.js when setting your own “_document.js” file.

Chapter 200. Module Summary

We added a lot of features including: Starting Page with Hero and Featured Posts. A page with all posts, a page for each single post. Posts stored as markdown files. Also a contact form that submits to our own API route that stores messages in a database.

Chapter 201. Module Resources