Writing a blog with Next.js!
Written on 2023-08-06
Today, I will be writing a blog with Next.js, and deploying it to Vercel!
In this post, I will be showing off how I made this blogging website, taking inspiration from the amazing website https://dimden.dev! I decided to try out Next.js, a javascript framework to create react-based projects for the web! It supports both client and server side rendering, and boasts incredibly fast build time to truly streamline the development experience. I will be going over how I made this website, and how you can make your own!
Getting Started
The first step is to set up the project. After installing Node.js and npm, I created the Next.js project following the steps on the documentation. I then installed the following dependencies:
- react-markdown - Parse markdown files. I also installed some plugins:
- remark-math - Parse math using .
- remark-gfm - Use GitHub Flavored Markdown.
- rehype-katex - Render math using .
- rehype-raw - Render raw HTML within the markdown.
- react-syntax-highlighter - Syntax highlighting for code blocks.
- lucide-react - Icons for the website.
- react-copy-to-clipboard - Copy code blocks to clipboard.
- gray-matter - Parse frontmatter from markdown files.
Now that we've installed that bucketload of dependencies, we can start writing the code!
Creating the Layout
The first thing I did was create the layout. Inside the components/
directory, I created a file called Layout.js
. This file contains the layout of the website, including the header, footer, and the main content. The header and footer are pretty simple, so I won't go over them. The main content is where the magic happens. It looks like this:
1export default function Layout({ children, home }) { 2 return ( 3 <div className={styles.container}> 4 <Head> 5 <link rel="icon" href="/favicon.ico" /> 6 <meta 7 name="description" 8 content="A personal website for me, a software developer." 9 /> 10 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 11 </Head> 12 <div className="rain front-row"></div> 13 <div className="rain back-row"></div> 14 <Navbar ishome={home} /> 15 <main> 16 {children} 17 <Analytics /> 18 </main> 19 {!home && ( 20 <div className={styles.backToHome}> 21 <Link href="/">← Back to home</Link> 22 </div> 23 )} 24 </div> 25 ); 26}
Let's break it down!
- The
Head
component is used to add metadata to the page, such as the title, description, and favicon. Here, we set the icon, create description metadata (to help with SEO), and set the viewport to be responsive for mobile devices. - The
div
elements with therain
class are used to create the rain effect in the background. I shamelessly stole it from here. Thank you for your service, Aaron Rickle! - The
Navbar
component is the header of the website. This is another component, which I will go over later. - The
main
element is where the main content of the page goes. This is where the blog posts will be rendered. All of the pages in the website will be encapsulated in thisLayout
component, and hence all page content will render instead of{children}
, inside themain
element. TheAnalytics
component is used to track page views for Vercel. I will also go over this later. - Finally, the
div
element with thebackToHome
class is used to create a link back to the home page, if the user is not already on the home page. This is used in the blog posts, to allow the user to go back to the home page.
Next, I created the Navbar
component. It looks like this:
1export default function Navbar({ ishome }) { 2 return ( 3 <nav className={style.navbar}> 4 <Link href="/" className={pixelnes.className}> 5 Cy4's Blog 6 </Link> 7 {ishome ? ( 8 <Link href="/">Back to cy4.dev</Link> 9 ) : ( 10 <Link href="/">Back to home</Link> 11 )} 12 </nav> 13 ); 14}
Here, we have a title, as well as a link back to the home page. If the user is already on the home page, the link directs the user to the main parent website.
Using a Custom Font
This website uses two custom fonts, PixelNES and Terminus. In order to import them, I placed the .ttf
s and .otf
s in the public/fonts/
directory. Then, I installed the @next/font
package, which allows me to actually import these fonts.
In Layout.js
, I imported the fonts like this:
1import localFont from "@next/font/local"; 2 3export const terminus = localFont({ 4 src: [ 5 { 6 path: "./../public/fonts/TerminusTTFWindows-4.49.2.ttf", 7 weight: "400", 8 style: "normal", 9 }, 10 { 11 path: "./../public/fonts/TerminusTTFWindows-Italic-4.49.2.ttf", 12 weight: "400", 13 style: "italic", 14 }, 15 ... 16 ], 17});
Next, I created the pages/_app.js
in order to set the global styles. It looks like this:
1import { terminus } from "../components/layout"; 2import "../styles/global.css"; 3 4export default function App({ Component, pageProps }) { 5 return ( 6 <> 7 <style jsx global>{` 8 html { 9 font-family: ${terminus.style.fontFamily}; 10 } 11 `}</style> 12 <Component {...pageProps} /> 13 </> 14 ); 15}
Here, we set the global font to be Terminus. We also import the global stylesheet, located in styles/global.css
. Using this .css
file, I can create the scanline effect visible on the website, and kindly stolen from dimden.dev. It looks like this:
1@keyframes Static { 2 0% { 3 background-position: 0 0; 4 } 5 6 100% { 7 background-position: 0 4px; 8 } 9} 10 11body:before { 12 content: ""; 13 position: fixed; 14 opacity: 0.2; 15 left: 0; 16 top: 0; 17 width: 100%; 18 height: 100%; 19 pointer-events: none; 20 z-index: 1000; 21 background-image: url("../public/images/overlay.png"); 22 background-repeat: all; 23 background-position: 0 0; 24 animation-name: Static; 25 animation-duration: 2s; 26 animation-iteration-count: infinite; 27 animation-timing-function: steps(4); 28 box-shadow: inset 0 0 10em rgb(0 0 0/40%); 29} 30 31body:after { 32 content: ""; 33 position: fixed; 34 left: 0; 35 top: 0; 36 opacity: 0.5; 37 width: 100%; 38 height: 100%; 39 pointer-events: none; 40 z-index: 1000; 41 background-image: url("../public/images/overlay2.png"); 42 background-repeat: all; 43 background-position: 0 0; 44 animation-name: Static; 45 animation-duration: 0.8s; 46 animation-iteration-count: infinite; 47 animation-timing-function: steps(4); 48}
Loading Blog Posts
Now that we have the layout set up, we can start loading the blog posts. I created a directory called posts/
, in which all of the post markdown files will be stored. Next, I created the lib/posts.js
file, which will run on the server side to load the posts. It looks like this:
1const postsDirectory = path.join(process.cwd(), "posts"); 2 3// Get all posts' metadata, sorted by date created. 4export function getSortedPostsData() { 5 const fileNames = fs.readdirSync(postsDirectory); 6 const allPostsData = fileNames.map((fileName) => { 7 const id = fileName.replace(/\.md$/, ""); 8 const fullPath = path.join(postsDirectory, fileName); 9 const fileContents = fs.readFileSync(fullPath, "utf8"); 10 const matterResult = matter(fileContents); 11 12 return { 13 id, 14 ...matterResult.data, 15 }; 16 }); 17 18 return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1)); 19}
Here, we get all of the post files, and parse the frontmatter from them. We then sort the posts by date, and return them. This is used for the homepage, to display all of the blog posts. We also have two more functions in this file, which are used to get all of the post IDs, and to get the post data for a specific post ID. They look like this:
1export function getAllPostIds() { 2 const fileNames = fs.readdirSync(postsDirectory); 3 4 return fileNames.map((fileName) => { 5 return { 6 params: { 7 id: fileName.replace(/\.md$/, ""), 8 }, 9 }; 10 }); 11} 12 13export function getPostData(id) { 14 const fullPath = path.join(postsDirectory, `${id}.md`); 15 const fileContents = fs.readFileSync(fullPath, "utf8"); 16 const matterResult = matter(fileContents); 17 18 return { 19 id, 20 content: matterResult.content, 21 ...matterResult.data, 22 }; 23}
The getAllPostIds
function is used to get all of the post IDs, which are then used to generate the static pages for each post. The getPostData
function is used to get the post data for a specific post ID, which is used to render the post content.
Creating the Homepage
Now that we have all the tools required, we can design the homepage, I won't go over much of the UI design in this stage, as it is pretty simple. I created the pages/index.js
file, which looks like this:
1export default function Home({ allPostsData }) { 2 return ( 3 <Layout home className={terminus.className}> 4 <Head> 5 <title>Cy4's Blog</title> 6 </Head> 7 <section> 8 <div className={`${style.card} ${style.card1}`}> 9 <h1 className={`${style.title} ${pixelnes.className}`}> 10 Cy4's Coding Adventures! 11 </h1> 12 <p className={style.description}> 13 A blog about my coding adventures, including Minecraft Modding, Web 14 Development, and more! 15 </p> 16 </div> 17 </section> 18 <section> 19 <div className={`${style.card} ${style.card2}`}> 20 <h2 className={`${style.title} ${pixelnes.className}`}> 21 Latest Posts! 22 </h2> 23 <ul className={style.list}> 24 {allPostsData.map(({ id, date, title }, index) => ( 25 <li 26 className={`${style.listItem}`} 27 key={id} 28 > 29 <a href={`/posts/${id}`}>{title}</a> 30 <p className={style.date}>{date}</p> 31 </li> 32 ))} 33 </ul> 34 </div> 35 </section> 36 </Layout> 37 ); 38} 39 40export async function getStaticProps() { 41 const allPostsData = getSortedPostsData(); 42 return { 43 props: { 44 allPostsData, 45 }, 46 }; 47}
Let's break it down!
- The
Layout
component is used to render the layout of the page. We set thehome
prop totrue
, to indicate that this is the home page. - The
Head
component is used to set the title of the page. - The first
section
element is used to render the title and description of the website. - The second
section
element is used to render the list of blog posts. We use theallPostsData
prop, which we get from thegetStaticProps
function, to render the list of posts. - The
getStaticProps
function is used to get the props for the page. Here, we get the list of posts, and pass it to the page.
Creating the Blog Posts
Now that we have the homepage, we can create the blog posts. I created the pages/posts/[id].js
file, which looks like this:
1export default function Post({ postData }) { 2 return ( 3 <Layout> 4 <Head> 5 <link 6 href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css" 7 rel="stylesheet" 8 key="katex" 9 /> 10 <title>{postData.title}</title> 11 </Head> 12 <section> 13 <div className={`${style.card} ${style.card1}`}> 14 <div className={style.posttitlerow}> 15 <h1 className={`${style.title} ${pixelnes.className}`}> 16 {postData.title} 17 </h1> 18 <p className={style.date}>Written on {postData.date}</p> 19 </div> 20 <p className={style.description}>{postData.description}</p> 21 </div> 22 </section> 23 <section className={style.post}> 24 <br /> 25 <ReactMarkdown 26 children={postData.content} 27 // Extra settings for passing the markdown omitted. 28 /> 29 </section> 30 </Layout> 31 ); 32} 33 34export async function getStaticPaths() { 35 const paths = getAllPostIds(); 36 return { 37 paths, 38 fallback: false, 39 }; 40} 41 42export async function getStaticProps({ params }) { 43 const postData = getPostData(params.id); 44 return { 45 props: { 46 postData, 47 }, 48 }; 49}
Let's break down this final bit of code!
- The
Layout
component is used to render the layout of the page. This time, we don't pass any props, as this is not the home page. - The
Head
component is used to set the title of the page, as well as the stylesheet for rendering. - The first
section
element is used to render the title, date, and description of the post. - The second
section
element is used to render the post content. We use thepostData
prop, which we get from thegetStaticProps
function, to render the post content. The settings and plugins for theReactMarkdown
component are omitted, as they are clearly explained in the documentation for thereact-markdown
package. The package is very easy to configure and bend to your needs, and comes with a plethora of plugins to extend its functionality. - The
getStaticPaths
function is used to get the paths for the static pages. Here, we get all of the post IDs, and pass them to the page. This way, Next.js knows which pages to generate. - The
getStaticProps
function is used to get the props for the page. Here, we get the post data, and pass it to the page.
And that's the end of the code. I omitted pretty much all of the css and UI design, as the purpose of this post is largely to show off the capabilities of the Next.js / Vercel stack.
Deploying to Vercel
This was probably the most exciting part of the project. I've seen Vercel's name pop up in web searches before, but I did not expect the simplicity, speed and pricing. Vercel is a platform for deploying your Next.js websites, but can be easily integrated with most popular javascript web frameworks. This process literally took me under 5 minutes!
First, I created a Vercel account, and linked it to my GitHub account. Then, I created a new project, and linked it to my GitHub repository. And... well that was it. In about 30 seconds, Vercel had deployed my website, and I was able to access it at https://cy4.vercel.app! Yep. It's literally that easy.
The website at Vercel is also automatically updated whenever I push to the main
branch of my GitHub repository. This means that I can easily update the website, and it will be automatically deployed to Vercel. Furthermore, the Analytics
component provided by vercel lets me track page views, and the Insights
tab in the Vercel dashboard lets me see the public performance of my website.
Conclusion
And that's it! A fully functional blog with Next.js. Being largely a beginner in web development, I was very impressed with the ease of use of Next.js, and the simplicity of Vercel. It's a far more streamlined development stack in comparison to the others I have tinkered with, and I will definitely be using it for future projects. If you want to create a simple (but scalable) website with complete customization, I highly recommend Next.js and Vercel.
Thank you for reading this post! It was largely created to test out the capabilities of the blog after the initial test post, and to get comfortable writing blog posts. I appreciate this one was rather heavy on code-blocks, but I hope you enjoyed it nonetheless! See you next time!
Footnote: with the popularization and developments in generative neural networks, I believe it is imperitive to mention that no, this post was not written by AI. However, I did use Github Copilot to assist me in writing the code for this project.