6 minute read

4. Building the Pages

Part 4 of the From CV to Site tutorial series.

Prerequisites

What this tutorial produces

A working Next.js project with the standard academic site pages, running locally at http://localhost:3000. Deployment is covered in Tutorial 6.

Two paths through this tutorial

You can follow this tutorial in one of two ways:

Option A: fork the starter. Copy the starter/ directory from this repository as your starting point. All the scaffolding below is already done; you only need to replace the placeholder content. This is the fastest path and the recommended one if this is your first Next.js project.

git clone https://github.com/HakeoungLee/from-cv-to-site.git
cp -r from-cv-to-site/starter ~/dev/my-site
cd ~/dev/my-site
pnpm install
pnpm dev

Option B: build from scratch. Use the step-by-step instructions below to understand each file. This option is mostly instructive; Tutorials 5 through 7 assume the starter’s structure and scripts, so from-scratch builders should still use the starter as the reference for parser scripts, data types, and file paths.

The rest of this tutorial walks through Option B. If you chose Option A, skim to verify the project layout matches and then move to Tutorial 5.

Site structure overview

The following page structure is recommended for a lab or personal academic site. Omit pages that do not apply.

Route Purpose
/ Home: identity, short intro, current focus
/research Research themes and publications
/projects Ongoing and past projects
/teaching Courses and teaching materials
/people Lab members (labs only)
/news Recent announcements and updates
/contact Contact information and affiliations

Creating the Next.js project

Create the project from a clean terminal in a folder where you keep code (for example, ~/dev).

cd ~/dev
pnpm create next-app@latest my-site --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd my-site

Answer Yes to TypeScript, Tailwind CSS, ESLint, App Router, and the src/ directory when prompted. These defaults match the rest of the series.

Start the development server to verify installation:

pnpm dev

Open http://localhost:3000. You should see the Next.js starter page. Stop the server with Ctrl+C.

File organization

The project uses the Next.js App Router. Key directories:

my-site/
├── src/
│   ├── app/
│   │   ├── layout.tsx        # root layout: header, footer, fonts
│   │   ├── page.tsx          # home page
│   │   ├── research/
│   │   │   └── page.tsx
│   │   ├── projects/
│   │   │   └── page.tsx
│   │   ├── teaching/
│   │   │   └── page.tsx
│   │   ├── people/
│   │   │   └── page.tsx
│   │   ├── news/
│   │   │   └── page.tsx
│   │   └── contact/
│   │       └── page.tsx
│   ├── components/           # reusable UI components
│   ├── data/                 # generated data files (Tutorial 5)
│   └── lib/                  # utilities
├── public/                   # static assets (images, PDFs)
├── tailwind.config.ts
└── next.config.ts

Create the page folders now:

cd src/app
mkdir research projects teaching people news contact

Creating pages with Claude Code

Start Claude Code from the project root. Paste your design brief from Tutorial 2 at the start of the session. Request one page at a time.

Example request for the home page:

Create src/app/page.tsx as the home page. It should use the fonts and colors defined in the design brief above. Include: a short headline, a two-sentence introduction, and a “current focus” section. Use semantic HTML and Tailwind utility classes. No client-side JavaScript unless required.

Review the generated file before accepting. Adjust copy and Tailwind classes as needed. Repeat for each route.

Building the home page

A minimal home page skeleton:

// src/app/page.tsx
export default function Home() {
  return (
    <main className="mx-auto max-w-3xl px-6 py-16">
      <h1 className="text-4xl font-serif mb-4">
        Your Lab or Name
      </h1>
      <p className="text-lg text-neutral-700 mb-12">
        One or two sentences describing your research.
      </p>
      <section>
        <h2 className="text-sm uppercase tracking-wider text-neutral-500 mb-2">
          Currently
        </h2>
        <p className="text-base">
          What you are working on right now.
        </p>
      </section>
    </main>
  );
}

Replace placeholder text with your own content.

Building the research page

The research page typically contains research themes and a publications list. Publications are generated in Tutorial 5; use a placeholder array for now.

// src/app/research/page.tsx
export default function Research() {
  return (
    <main className="mx-auto max-w-3xl px-6 py-16">
      <h1 className="text-3xl font-serif mb-8">Research</h1>
      <section className="mb-12">
        <h2 className="text-xl font-serif mb-4">Themes</h2>
        <ul className="space-y-3 text-neutral-700">
          <li><strong>Theme 1.</strong> Short description.</li>
          <li><strong>Theme 2.</strong> Short description.</li>
        </ul>
      </section>
      <section>
        <h2 className="text-xl font-serif mb-4">Publications</h2>
        <p className="text-neutral-500">Generated in Tutorial 5.</p>
      </section>
    </main>
  );
}

Adding the remaining pages

Use the same pattern for projects, teaching, people, news, and contact. Each page file exports a default React component that returns the page’s content.

For the contact page, include your institutional email, office location, and links to external profiles (Google Scholar, ORCID, etc.).

Root layout

The root layout at src/app/layout.tsx defines the header, footer, and fonts that appear on every page.

// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";

export const metadata: Metadata = {
  title: "Your Name",
  description: "Your Lab or research description",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="antialiased">
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

Create Header and Footer components in src/components/.

A minimal header with site navigation:

// src/components/Header.tsx
import Link from "next/link";

const NAV = [
  { href: "/research", label: "Research" },
  { href: "/projects", label: "Projects" },
  { href: "/people", label: "People" },
  { href: "/teaching", label: "Teaching" },
  { href: "/news", label: "News" },
  { href: "/contact", label: "Contact" },
];

export default function Header() {
  return (
    <header className="border-b border-neutral-200">
      <nav className="mx-auto max-w-3xl px-6 py-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
        <Link href="/" className="prose-serif text-lg font-semibold">Your Name</Link>
        <ul className="flex flex-wrap gap-x-5 gap-y-1 text-sm">
          {NAV.map((item) => (
            <li key={item.href}>
              <Link href={item.href}>{item.label}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </header>
  );
}

The flex flex-col sm:flex-row pattern stacks navigation items vertically on narrow screens and shifts to a horizontal row at the sm breakpoint (640 pixels). Remove /people from NAV if the site is for a solo researcher.

// src/components/Footer.tsx
export default function Footer() {
  return (
    <footer className="border-t border-neutral-200 mt-16">
      <div className="mx-auto max-w-3xl px-6 py-8 text-sm text-neutral-500">
        <p>Your Name, Your Institution.</p>
      </div>
    </footer>
  );
}

Typography setup

Next.js provides built-in font loading via next/font. Configure in the root layout:

import { Inter, Source_Serif_4 } from "next/font/google";

const sans = Inter({ subsets: ["latin"], variable: "--font-sans" });
const serif = Source_Serif_4({ subsets: ["latin"], variable: "--font-serif" });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${sans.variable} ${serif.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Reference var(--font-serif) and var(--font-sans) in your Tailwind configuration or inline classes.

Swapping fonts

To use different fonts, replace the imports from next/font/google. Any font available on Google Fonts works:

// Editorial, serif-led combination
import { Lora, Work_Sans } from "next/font/google";

// Mono-led combination
import { JetBrains_Mono, IBM_Plex_Sans } from "next/font/google";

Update the variable names and className on <html> accordingly, and revise the CSS variables in src/app/globals.css to match.

Adding images

Images belong in public/. Next.js serves anything there at the site root.

public/
├── figures/
│   └── model-diagram.png
├── people/
│   ├── you.jpg
│   └── student.jpg
└── favicon.ico

Use the Image component from next/image for photos and figures above the fold. It handles responsive sizing, lazy loading, and modern formats (WebP, AVIF) automatically:

import Image from "next/image";

<Image
  src="/figures/model-diagram.png"
  alt="Diagram of the proposed model"
  width={800}
  height={600}
  className="rounded-md"
/>

For small decorative images or icons where those optimizations are unnecessary, a plain <img> is acceptable. Compress images before committing them with Squoosh or sharp; aim for under 200 KB per image.

Responsive considerations

The examples use max-w-3xl to constrain line length for readability. Tailwind’s default breakpoints (sm, md, lg) are sufficient for most academic sites. Test at three widths:

  • Mobile: 375px (iPhone)
  • Tablet: 768px (iPad portrait)
  • Desktop: 1440px and above

Use Chrome DevTools’ device toolbar (Cmd+Option+I, then device icon) to verify each breakpoint.

Local preview

Run the dev server and navigate through each page:

pnpm dev

Visit http://localhost:3000, /research, /projects, /teaching, /people, /news, and /contact. Resolve any broken links or type errors before proceeding.

The starter renders as follows on first run (before replacing placeholder content):

Starter home page

Research and projects pages are populated from data generated in Tutorial 5:

Starter research page with sample publications

Committing progress

Initialize a Git repository and commit your progress:

git add .
git commit -m "Scaffold pages and layout"

Next tutorial

Tutorial 5: CV Automation


Questions or feedback: open an issue on the repository or email hannahlee@virginia.edu.

Back to README