Published: Sep 7, 2023 by Isaac Johnson
My feed kept prompting me to check out a dev.to post about Next.js with React to create smooth Vercel based navbar experience in NodeJS.
I decided to start by reworking that post then filling it out a bit more into rendering a markdown-based blog rendering and functional page stubs.
Setup
Let’s create a directory and see what version of Node we are using presently.
builder@DESKTOP-QADGF36:~/Workspaces$ mkdir nodesite
builder@DESKTOP-QADGF36:~/Workspaces$ cd nodesite/
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ nvm list
-> v10.22.1
v12.22.11
v14.18.1
v17.6.0
default -> 10.22.1 (-> v10.22.1)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v17.6.0) (default)
stable -> 17.6 (-> v17.6.0) (default)
lts/* -> lts/gallium (-> N/A)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.19.1 (-> N/A)
lts/gallium -> v16.14.2 (-> N/A)
That’s a bit dated. Let’s use latest LTS (at time of writing), 16.14.2.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ nvm install 16.14.2
Downloading and installing node v16.14.2...
Downloading https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.xz...
############################################################################################################################################################################################################################ 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v16.14.2 (npm v8.5.0)
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ nvm use 16.14.2
Now using node v16.14.2 (npm v8.5.0)
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ node --version
v16.14.2
We’ll be using the newer Plug-N-Play Package Manager, pnpm. It’s a bit more performant than npm.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ npm install -g pnpm
added 1 package, and audited 2 packages in 1s
1 package is looking for funding
run `npm fund` for details
found 0 vulnerabilities
npm notice
npm notice New major version of npm available! 8.5.0 -> 10.0.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.0.0
npm notice Run npm install -g npm@10.0.0 to update!
npm notice
We can now install with pnpm create next-app@latest
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ pnpm create next-app@latest
.../share/pnpm/store/v3/tmp/dlx-12218 | +1 +
.../share/pnpm/store/v3/tmp/dlx-12218 | Progress: resolved 1, reused 0, downloaded 1, added 1, done
✔ What is your project named? … freshbrewed-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
Creating a new Next.js app in /home/builder/Workspaces/nodesite/freshbrewed-app.
Using pnpm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- tailwindcss
- postcss
- autoprefixer
- eslint
- eslint-config-next
Downloading registry.npmjs.org/next/13.4.19: 12.97 MB/12.97 MB, done
Downloading registry.npmjs.org/typescript/5.2.2: 7.23 MB/7.23 MB, done
Downloading registry.npmjs.org/@next/swc-linux-x64-musl/13.4.19: 42.84 MB/42.84 MB, done
Downloading registry.npmjs.org/@next/swc-linux-x64-gnu/13.4.19: 36.21 MB/36.21 MB, done
Packages: +325
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 333, reused 0, downloaded 325, added 325, done
dependencies:
+ @types/node 20.5.8
+ @types/react 18.2.21
+ @types/react-dom 18.2.7
+ autoprefixer 10.4.15
+ eslint 8.48.0
+ eslint-config-next 13.4.19
+ next 13.4.19
+ postcss 8.4.29
+ react 18.2.0
+ react-dom 18.2.0
+ tailwindcss 3.3.3
+ typescript 5.2.2
Done in 7.2s
Initialized a git repository.
Success! Created freshbrewed-app at /home/builder/Workspaces/nodesite/freshbrewed-app
I want to create a nice Navbar akin to this dev.to blog so I’ll add framer-motion
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ pnpm i framer-motion
Packages: +4
++++
Progress: resolved 4, reused 1, downloaded 3, added 4, done
dependencies:
+ framer-motion 10.16.2
Done in 1s
If we want to test first, we need to hop into the app directory and git it a go
builder@DESKTOP-QADGF36:~/Workspaces/nodesite$ cd freshbrewed-app/
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm dev
> freshbrewed-app@0.1.0 dev /home/builder/Workspaces/nodesite/freshbrewed-app
> next dev
- ready started server on [::]:3000, url: http://localhost:3000
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry
- event compiled client and server successfully in 185 ms (20 modules)
- wait compiling...
- event compiled client and server successfully in 133 ms (20 modules)
I was having some WSL issues when writing, so I ended up firing up Firefox into X using Windows 11 so I could properly hit localhost:3000
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm dev
> freshbrewed-app@0.1.0 dev /home/builder/Workspaces/nodesite/freshbrewed-app
> next dev
- ready started server on [::]:3000, url: http://localhost:3000
loc- event compiled client and server successfully in 294 ms (20 modules)
al- wait compiling...
- event compiled client and server successfully in 118 ms (20 modules)
- wait compiling /page (client and server)...
- event compiled client and server successfully in 3.3s (426 modules)
- wait compiling...
- event compiled successfully in 257 ms (235 modules)
- wait compiling /favicon.ico/route (client and server)...
- event compiled client and server successfully in 2.7s (472 modules)
I’ll create a Navbar.tsx component which will have my main headings/routes.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ mkdir -p src/app/components
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ vi src/app/components/Navbar.tsx
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat src/app/components/Navbar.tsx
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
const navItems = [
{
path: "/",
name: "Home",
},
{
path: "/fun",
name: "Fun",
},
{
path: "/guestbook",
name: "Guestbook",
},
{
path: "/blog",
name: "Blog",
},
];
export default function NavBar() {
let pathname = usePathname() || "/";
if (pathname.includes("/blog/")) {
pathname = "/blog";
}
const [hoveredPath, setHoveredPath] = useState(pathname);
return (
<div className="border border-stone-800/90 p-[0.4rem] rounded-lg mb-12 sticky top-4 z-[100] bg-stone-900/80 backdrop-blur-md">
<nav className="flex gap-2 relative justify-start w-full z-[100] rounded-lg">
{navItems.map((item, index) => {
const isActive = item.path === pathname;
return (
<Link
key={item.path}
className={`px-4 py-2 rounded-md text-sm lg:text-base relative no-underline duration-300 ease-in ${
isActive ? "text-zinc-100" : "text-zinc-400"
}`}
data-active={isActive}
href={item.path}
onMouseOver={() => setHoveredPath(item.path)}
onMouseLeave={() => setHoveredPath(pathname)}
>
<span>{item.name}</span>
{item.path === hoveredPath && (
<motion.div
className="absolute bottom-0 left-0 h-full bg-stone-800/80 rounded-md -z-10"
layoutId="navbar"
aria-hidden="true"
style={{
width: "100%",
}}
transition={{
type: "spring",
bounce: 0.25,
stiffness: 130,
damping: 9,
duration: 0.3,
}}
/>
)}
</Link>
);
})}
</nav>
</div>
);
}
We now need to customize the layout.tsx.
$ cat src/app/layout.tsx
import "./globals.css";
import NavBar from "@/components/navbar";
export const metadata = {
title: "Create NextJS App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gradient-to-tr overflow-x-hidden min-w-screen from-zinc-950 via-stone-900 to-neutral-950 flex min-h-screen flex-col items-center justify-between">
<main className="p-4 py-24 gap-6 w-full lg:w-[55%]">
<section className="w-full flex gap-4 justify-start mb-6 p-2">
<div>
<img src="https://avatars.githubusercontent.com/u/68690233?s=100&v=4" alt="avatar"
className="w-12 h-12 rounded-full shadow-lg grayscale hover:grayscale-0 duration-300"
/>
</div>
<div className="flex flex-col gap-2 justify-center">
<h2 className="mb-0 text-zinc-100 font-bold">Isaac</h2>
<p className="mb-0 text-zinc-400 font-semibold leading-none">
Blogger, Father and Adventurer
</p>
</div>
</section>
<NavBar />
{children}
</main>
</body>
</html>
);
}
It’s mostly to build out the body. Here is a diff of what was there:
$ diff src/app/layout.tsx.bak src/app/layout.tsx
1,3c1,2
< import './globals.css'
< import type { Metadata } from 'next'
< import { Inter } from 'next/font/google'
---
> import "./globals.css";
> import NavBar from "@/components/navbar";
5,10c4,7
< const inter = Inter({ subsets: ['latin'] })
<
< export const metadata: Metadata = {
< title: 'Create Next App',
< description: 'Generated by create next app',
< }
---
> export const metadata = {
> title: "Create NextJS App",
> description: "Generated by create next app",
> };
15c12
< children: React.ReactNode
---
> children: React.ReactNode;
19c16,34
< <body className={inter.className}>{children}</body>
---
> <body className="bg-gradient-to-tr overflow-x-hidden min-w-screen from-zinc-950 via-stone-900 to-neutral-950 flex min-h-screen flex-col items-center justify-between">
> <main className="p-4 py-24 gap-6 w-full lg:w-[55%]">
> <section className="w-full flex gap-4 justify-start mb-6 p-2">
> <div>
> <img src="https://avatars.githubusercontent.com/u/68690233?s=100&v=4" alt="avatar"
> className="w-12 h-12 rounded-full shadow-lg grayscale hover:grayscale-0 duration-300"
> />
> </div>
> <div className="flex flex-col gap-2 justify-center">
> <h2 className="mb-0 text-zinc-100 font-bold">Isaac</h2>
> <p className="mb-0 text-zinc-400 font-semibold leading-none">
> Blogger, Father and Adventurer
> </p>
> </div>
> </section>
> <NavBar />
> {children}
> </main>
> </body>
21c36
< )
---
> );
Lastly, we should scrub up the globals.css so it doesn’t mess with our styles. I’ll just sed out all but the first 4 lines.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ sed -i '1,4!d' src/app/globals.css
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
My first issue was that NavBar wasn’t found.
I should have run build first which would have shown that.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm build
> freshbrewed-app@0.1.0 build /home/builder/Workspaces/nodesite/freshbrewed-app
> next build
Failed to compile.
./src/app/layout.tsx
Module not found: Can't resolve '@/components/navbar'
https://nextjs.org/docs/messages/module-not-found
> Build failed because of webpack errors
- info Creating an optimized production build . ELIFECYCLE Command failed with exit code 1.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$
It took some experimenting to figure out why the pathing was off. I knew we rooted in /src from the setting in tsconfig
But it seems I needed to add ‘app’ to the path
Then it built without issue:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm build
> freshbrewed-app@0.1.0 build /home/builder/Workspaces/nodesite/freshbrewed-app
> next build
- info Creating an optimized production build
- info Compiled successfully
./src/app/layout.tsx
20:15 Warning: Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules
- info Linting and checking validity of types
- info Collecting page data
[ ] - info Generating static pages (0/4)(node:3273) ExperimentalWarning: buffer.Blob is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
- info Generating static pages (4/4)
- info Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.17 kB 83.6 kB
└ ○ /favicon.ico 0 B 0 B
+ First Load JS shared by all 78.5 kB
├ chunks/431-82c16d9f92a293e1.js 26.1 kB
├ chunks/e0f3dcf3-c6e4db7d5451056b.js 50.5 kB
├ chunks/main-app-72ba40e3bddb704a.js 221 B
└ chunks/webpack-a4eed088dcf0360e.js 1.67 kB
Route (pages) Size First Load JS
─ ○ /404 181 B 76.5 kB
+ First Load JS shared by all 76.3 kB
├ chunks/framework-510ec8ffd65e1d01.js 45.1 kB
├ chunks/main-615196ad320d4c32.js 29.4 kB
├ chunks/pages/_app-991576dc368ea245.js 195 B
└ chunks/webpack-a4eed088dcf0360e.js 1.67 kB
○ (Static) automatically rendered as static HTML (uses no initial props)
We can now see the nav in action:
Creating a repo
It’s at this point I have a basic framework and want to save it aside.
I’ll use gitignore.io to create a gitignore file.
I ended up appending to the one there already. It was then I realized the whole create-react pnpm actually setup git for me. I did not expect that.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .gitignore
modified: src/app/globals.css
modified: src/app/layout.tsx
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/app/components/
no changes added to commit (use "git add" and/or "git commit -a")
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git branch
* main
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git remote show origin
fatal: 'origin' does not appear to be a git repository
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git remote show
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$
I can add and commit, but without a remote, I can’t push anywhere
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git commit -m "phase 1"
[main 8320e2d] phase 1
4 files changed, 291 insertions(+), 49 deletions(-)
create mode 100644 src/app/components/Navbar.tsx
rewrite src/app/globals.css (88%)
rewrite src/app/layout.tsx (76%)
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git push
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using
git remote add <name> <url>
and then push using the remote name
git push <name>
Azure Repo
I debated between gitea, gitlab and github. Since it’s pretty simple, I decided to just make a public Github repo and push it there.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git remote add origin https://github.com/idjohnson/nodesite.git
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git push -u origin main
Enumerating objects: 29, done.
Counting objects: 100% (29/29), done.
Delta compression using up to 16 threads
Compressing objects: 100% (25/25), done.
Writing objects: 100% (29/29), 49.71 KiB | 9.94 MiB/s, done.
Total 29 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), done.
To https://github.com/idjohnson/nodesite.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
Stubbed pages
My next step was to make a few stubbed in pages.
import "../globals.css";
import NavBar from "@/app/components/Navbar";
export const metadata = {
title: "Create NextJS App",
description: "Generated by create next app",
};
// <!-- use http://caius.github.io/github_id/ -->
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-2 justify-center">
<h2 className="mb-0 text-zinc-100 font-bold">and</h2>
<p className="mb-0 text-zinc-400 font-semibold leading-none">
.... blog here .....
</p>
</div>
);
}
Which gives us a basic layout without crashing (checked in as phase 2)
Blog
Let’s pull in Markdown to render as blog entries.
I need to install some libraries such as react icons
pnpm install --save gray-matter
pnpm install --save markdown-to-jsx
pnpm install --save react-icons
Then create a lot of files. You can see the SHA here
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: package.json
modified: pnpm-lock.yaml
modified: src/app/blog/page.tsx
Untracked files:
(use "git add <file>..." to include in what will be committed)
content/
src/app/blog/[slug]/
src/app/components/common/
src/app/components/preview/
src/app/content/
src/app/libs/
src/app/providers/
src/app/types/
src/content/
Let’s see it all in action:
Summary
I started this off by following this Ashish dev.to post, with some tweaks and fixes. I liked the nav, but it was mostly broken when anything was clicked.
I then added in some stub pages and then used this Rahil Siddique repo as a basis to figure out how to work out the blogging using markdown. Though my end code is a bit different than his.
I wanted to have something that was a functional base others can use to do a general React website. This post will be followed up with a bit more, like the guestbook, before we then move on to containerizing and hosting or rendering out - the deployment parts I have yet to formulate fully.