Introduction
This guide shows you how to build an example static website using the Next.js framework which uses the React JavaScript library under the hood.
You will set up a new Next.js project from scratch and then build with it to produce a simple website showing a list of development-related websites.
Once the website is built you will convert it to a static website, the built files will end up being HTML, CSS, JavaScript, and nothing else.
Next.js DevSites application
Requirements
Development tools and software are required to start the Next.js website on your PC / Laptop.
Here is a list of things needed to follow this guide.
- Node / Node Package Manager
- Code Editor: Sublime Text
- Code Editor Syntax: Vue Syntax Highlight
- Web Browser: Google Chrome
- Terminal: Git Bash
Feel free to use alternative applications to the ones listed above, as long as you can use them to build the website in this guide.
Creating the project
Open the terminal application, change to a directory to add the Next.js project, and run the following command to start the app setup.
npx create-next-app@latest
You will be provided with a series of questions, you can use the same answers as shown below(Answers are in bold).
✔ What is your project named? … devsites
Name of the folder containing all files and folders associated with the app.
✔ Would you like to use TypeScript? … No / Yes
TypeScript is considered the standard and should be used for all Next.js apps.
✔ Would you like to use ESLint? … No / Yes
Add ESLint to ensure code errors get fixed.
✔ Would you like to use Tailwind CSS? … No / Yes
For this app, standard CSS will be used although Tailwind CSS for apps is recommended.
✔ Would you like to use src/
directory? … No / Yes
The source code will be inside the src folder of the devsites project folder.
✔ Would you like to use App Router? (recommended) … No / Yes
This app will consist of multiple pages so an App Router is required.
✔ Would you like to customize the default import alias (@/*)? … No / Yes
The default @ alias is fine to use when importing components.
After answering all the questions the project will be built, and after the app has been built go into the folder.
cd devsites
Start up the app by running the dev command.
npm run dev
The URL for the app will be output, by default the URL used is http://localhost:3000. Go to the URL and the website loaded will be similar to the following.
Removing the default page code
The content for the default website should be removed before adding in the devsites-specific code.
Open up your code editor to start coding the app.
Go to the following page.tsx file inside of the project.
src / app / page.tsx
Remove most of the content inside the main tag and add in the below code.
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<div>Hello World!</div>
</main>
);
}
After saving the file you should see the change apply instantly when you check the website.
Now go to the CSS file referenced on the page.tsx file.
src / app / page.module.css
Remove all the CSS inside of this file and leave the .main class CSS.
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
Bug Fix
While working on this app I experienced the following bug when using Chrome developer tools.
If you get the same error go to the layout.tsx file and add the suppressHydrationWarning parameter with a value of true.
return (
<html lang="en">
<body suppressHydrationWarning={true} className={inter.className}>{children}</body>
</html>
);
Building devsites app
After removing most of the default code you can begin coding the devsites app.
The config file needs to be updated so that the app is set up to export files to be used for a static website.
next.config.mjs
Go to the next config file and add the following values for the nextConfig JSON.
Setting output to export will allow the build command to build static website files.
Setting the images to unoptimized allows images to be built for static sites.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: {
unoptimized: true
}
};
export default nextConfig;
Go to the devsites folder and create a new file named .env.local.
.env.local
Inside this file add the clientside environment variable with the URL for the API used to get the devsites data.
NEXT_PUBLIC_DEVPUSH_API=https://api.devpushprojects.com
Now to test that the environment variable can be read go back to the page.tsx file.
src / app / page.tsx
Add the following code which will output the environment variable added in.
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<div>{ process.env.NEXT_PUBLIC_DEVPUSH_API }</div>
</main>
);
}
If you see the environment variable output on the website, this means it's been set up correctly.
Types
As TypeScript is set up for this app it makes sense to create types to represent the API data that will be retrieved.
Create a types folder and then an index.ts file inside of it.
src / types / index.ts
Add the types to represent the site and category data.
export interface Site {
id: number
site_category_id: number
name: string
url: string
image_url: string
}
export interface SiteCategory {
id: number
name: string
}
Services
A services file will contain the API code to call and return the API data.
Create a services folder and then a devsites.ts file.
src / services / devsites.ts
Add the following functions to the file.
getSites - Call the API to get the developer websites
getSiteByCategoryId - Call the API to get devsites filtered by the category selected.
getSiteCategories - Call the API to get all the categories.
getData - Reusable function that calls passed in URL and returns the data.
import type { Site, SiteCategory } from "@/app/types"
const apiUrl: string = process.env.NEXT_PUBLIC_DEVPUSH_API ?? ''
const getData = async (url: string) => {
const response = await fetch(url)
const json = await response.json()
return json['data']
}
export const getSites = async (): Promise<Site[]> => {
return getData(`${apiUrl}/devsites`)
}
export const getSiteByCategoryId = async (categoryId: Number): Promise<Site[]> => {
return getData(`${apiUrl}/devsites/categorized/${categoryId}`)
}
export const getSiteCategories = async (): Promise<SiteCategory[]> => {
return getData(`${apiUrl}/devsites/categories`)
}
Outputting API data
Now that the types and service functions have been added the next step is to call the API to output data.
So let's do a test to make sure we can get the data.
src / app / page.tsx
Go back to the page.tsx file and add the following code which will call the getSites function, loop through the returned data, add the data to a variable, and output the variable to the view.
import { Site } from "@/types";
import styles from "./page.module.css"
import { getSites } from "@/services/devsites"
export default async function Home() {
const devsites: Array<Site> = await getSites()
let elems = []
for (const site of devsites) {
elems.push(<div>{ site.name }</div>)
}
return (
<main>
<div>{ process.env.NEXT_PUBLIC_DEVPUSH_API }</div>
{ elems }
</main>
);
}
Go to the website and you should see a list of names, this means the API call worked and this is the data from the API.
Components
The next step is to code the view to present and filter all the devsite API data.
Create a components folder inside the devsites folder and then create a SiteList.tsx file.
src / app / components / SiteList.tsx
This file will be used to get the Site data and show them as a list of thumbnail blocks with the name.
This code will also get the filtered sites if the category ID is passed into the component.
import Link from 'next/link'
import Image from 'next/image'
import { Site } from '@/app/types'
import { getSiteByCategoryId, getSites } from '@/app/services/devsites'
type Props = {
categoryId: Number|undefined
}
const SiteList = async ({ categoryId }: Props) => {
let sites: Array<Site> = [];
if (categoryId) {
sites = await getSiteByCategoryId(categoryId)
} else {
sites = await getSites()
}
const siteElements = [];
for (const site of sites) {
siteElements.push(
<Link href={ site.url } className="site">
<Image src={ site.image_url } alt={ site.name } width={ 250 } height={ 150 } />
<div className="site-name">{ site.name }</div>
</Link>
)
}
return (
<div className="sites">
{ siteElements }
</div>
)
}
export default SiteList
Next, create a CategoryList.tsx.
src / app / components / CategoryList.tsx
The following code will get all the categories and present a list of each category as a clickable link.
The passed-in category ID is used to highlight the matching category.
import Link from 'next/link'
import { getSiteCategories } from '@/app/services/devsites'
import { SiteCategory } from '@/app/types'
type Props = {
categoryId: Number
}
const CategoryList = async ({ categoryId }: Props) => {
let categories: Array<SiteCategory> = await getSiteCategories()
const categoryElements = []
for (const category of categories) {
categoryElements.push(
<Link
className={ 'category' + (category.id == categoryId ? ' category-active' : '') }
href={ `/category/${category.id}` }>
{ category.name }
</Link>
)
}
return (
<div className="categories">
<h2>Categories</h2>
<div className="category-links">
<Link href="/" className={ 'category' + (categoryId == 0 ? ' category-active' : '') }>All</Link>
{ categoryElements }
</div>
</div>
)
}
export default CategoryList
Lastly, create the SitePanel.tsx component file.
src / app / components / SitePanel.tsx
This component includes the SiteList and CategoryList components so both can be added to a page as a single component.
The passed-in category ID will get passed into the SiteList and CategoryList components.
import SiteList from '@/app/components/SiteList'
import CategoryList from '@/app/components/CategoryList'
type Props = {
categoryId?: Number
}
const SitePanel = ({ categoryId }: Props) => {
return (
<>
<SiteList categoryId={ categoryId ?? 0 } />
<CategoryList categoryId={ categoryId ?? 0 } />
</>
)
}
export default SitePanel
Home Page
Go back to the page.tsx once again.
src / app / page.tsx
This time import and output the SitePanel component.
import SitePanel from "./components/SitePanel"
export default async function Home() {
return (
<SitePanel />
)
}
Go to the layout file to update the layout.
src / app / layout.tsx
The main tag is moved from the page.tsx to the layout file and the header tag is added with a name and description of the website.
import type { Metadata } from "next";
import { Inter } from "next/font/google"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body suppressHydrationWarning={true} className={inter.className}>
<header>
<h1>DevSites</h1>
<div>Technologies and services related to PHP development</div>
</header>
<main>
{ children }
</main>
</body>
</html>
)
}
After adding all the above code the website layout is complete, go to the browser and check the website which should look like the following.
The links won't work as the page for the category link URLs doesn't exist yet.
Category Sites Page
The category links need fixing so start by creating a category folder, inside the category folder create a folder named [categoryId] (Yes, include the square brackets). Then inside of this new folder create a page.tsx file.
src / category / [categoryId] / page.tsx
This page component will reuse the SitePanel component and pass in the category ID from the URL.
The generateStaticParams returns a list of params representing all the page URLs that should be generated.
import SitePanel from "@/app/components/SitePanel"
import { getSiteCategories } from "@/app/services/devsites"
import { SiteCategory } from "@/app/types"
export async function generateStaticParams() {
const categories: SiteCategory[] = await getSiteCategories()
return categories.map((category) => ({
categoryId: category.id.toString()
}))
}
export default async function CategoryPage({ params }: { params: { categoryId: string } }) {
return (
<SitePanel categoryId={ Number(params.categoryId) } />
)
}
Go back to the website and click on one of the categories and the sites should filter based on the selected category.
Building the static website
After completing the site code it's ready for building the static website files.
Go to the terminal and run the following command which will generate all the static website files inside a new out folder.
npm run build
After running the command you will see the out folder, if you open the folders you will see all the static files that would make up the website.
Conclusion
After going through this guide you have set up a Next.js project, built a simple app with the Next.js framework, and generated static website files to host the website.
When hosting the static website it will look the same as the development version except that all the pages and data are already generated. So there are no API calls on the static website and the TypeScript is already compiled into native JavaScript.
If you want to host a static website I would recommend a frontend web hosting service such as Netlify and Vercel, these are the services I'm currently using.