# Pages and Components: Make It Yours

Course: Vibe Coding: Build Real Apps with AI Agents — Lesson 3 of 4

Replace the demo home page with your own. Learn what a component is, how files import and export them, and build your first pieces by hand — a Hero and a Card — wired together and passed real data through props.

Last lesson you found `apps/web/src/app/page.tsx` and its one `<Demo />` line. Now you
replace it with **your** content, built from components **you** create.

**Do this one by hand**, not by prompting the agent. It writes components for you from
here on, and writing a few yourself is the fastest way to read and direct its output.
Editor open, `bun dev` running.

## What a component is

A **component** is a reusable piece of UI, a **Lego brick**. A button is one, a card is
one, a page is bricks snapped together.

- **They nest.** A page holds a hero, which holds a card, which holds a button.
- **Write one, use it anywhere.** Build a `Card` once, drop it on five pages.
- **One brick, one file.** Easy to find, easy to change.

## Files, exports, and imports

A component lives in a file and **exports** itself. Another file **imports** it. That's the
whole wiring system, and you saw both halves last lesson in `page.tsx`:

```tsx
import { Demo } from "@workspace/ui/components/demo";
//     ↑ import the Demo brick from the shared UI package

const Page = () => (
  <div className="flex min-h-svh items-center justify-center">
    <Demo />
    {/* ↑ snap it into the page */}
  </div>
);

export default Page;
//     ↑ export this page so Next.js can show it
```

That `<Demo />` line is the brick you swap out. These are **static imports**, the normal
kind.

> **What's that `<div>` stuff?** Markup inside your code, called **JSX**. Read it as "what
> this brick draws." `className` attaches styling; ignore the classes until next lesson.

## Build the Card

Smallest brick first: a **call-to-action card**, a line of text and a button. In VS Code,
right-click `apps/web/src/components` → **New File** → name it `card.tsx`. Paste this in:

```tsx
import Link from "next/link";

export const Card = () => (
  <div className="flex flex-col items-start gap-3 rounded-xl border p-6">
    <p>Curious who's behind this?</p>
    <Link className="rounded-md bg-foreground px-4 py-2 text-background" href="/about">
      Learn more
    </Link>
  </div>
);
```

> **What's all the `className="…"`?** That's **Tailwind**, giving the card its border and
> padding and making the link look like a button. The classes are here so the card looks
> real; styling is the next lesson.

> **Why `Link` and not a plain `<a>`?** `Link` is Next.js's version of a link. It becomes
> an ordinary `<a>` in the browser, but links to **your own pages** switch instantly, with
> no full reload. Email and outside links behave normally, so one `Card` covers the
> `/about` link and the `mailto:` button you add later.

- **`export const Card = () => (...)`** defines the brick and exports it. **Capitalized**,
  because React tells a component from plain HTML by that capital. The file stays
  lowercase, `card.tsx`, by convention.
- It returns **JSX**: a line of text (`<p>`) and a button that's really a **link**
  (`<Link href=…>`) to `/about`, a page you build soon.

Everything inside is **hardcoded**, baked into the file.

## Build the Hero (it uses the Card)

A **hero** is the intro block at the top of a page, your name and title. Ours holds the
`Card` inside it. Create `apps/web/src/components/hero.tsx`:

```tsx
import { Card } from "@/components/card";

export const Hero = () => (
  <div className="flex flex-col items-center gap-6 text-center">
    <h1 className="font-bold text-4xl tracking-tight">Andrey Markin</h1>
    <p className="text-lg text-muted-foreground">Building things on the internet with AI agents.</p>
    <Card />
  </div>
);
```

Put your own name and subtitle in. The line that matters is `<Card />`: `Hero` **uses the
`Card` brick** the way a page will use `<Hero />`. `@/components/card` is the `@/` shortcut
from last lesson, "this app's `src/components`."

## Put it on the home page

Open `apps/web/src/app/page.tsx` and replace the whole file, your `Hero` in place of
`<Demo />`:

```tsx
import { Hero } from "@/components/hero";

const Page = () => (
  <div className="flex min-h-svh flex-col items-center justify-center">
    <Hero />
  </div>
);

export default Page;
```

Save, then open [https://web.localhost](https://web.localhost). The wall of buttons is
gone: your name, your subtitle, and a "Learn more" link, plain and unstyled.

> **See an error instead?** Check that `bun dev` is still running, then paste the error to
> your agent: *"I wrote this and got this error, what's wrong?"*

## Reuse it on a second page, and hit a wall

**Folders in `app/` become pages, and the path becomes the URL**, so `app/about/page.tsx`
gives you `/about`, where "Learn more" already points. Build it and reuse the same `Hero`.

On the About page you want the button to say *"Schedule a meeting"* and open your email.
But every word in your `Card` and `Hero` is **hardcoded**, so `<Hero />` there gives you
the same "Learn more" card.

## Props: make the brick configurable

**Props** are inputs you pass to a component when you use it. Instead of baking text into
the `Card`, whoever uses it decides.

Update `card.tsx`. The `-` lines come out, the `+` lines go in:

```diff
 import Link from "next/link";

+interface CardProps {
+  text: string;
+  buttonLabel: string;
+  href: string;
+}
+
-export const Card = () => (
+export const Card = ({ text, buttonLabel, href }: CardProps) => (
   <div className="flex flex-col items-start gap-3 rounded-xl border p-6">
-    <p>Curious who's behind this?</p>
-    <Link className="rounded-md bg-foreground px-4 py-2 text-background" href="/about">
-      Learn more
+    <p>{text}</p>
+    <Link className="rounded-md bg-foreground px-4 py-2 text-background" href={href}>
+      {buttonLabel}
     </Link>
   </div>
 );
```

What changed:

- **`CardProps`** lists the inputs: `text`, `buttonLabel`, `href`, all text (`string`).
  That's **TypeScript** describing what `Card` expects, so you can't forget one or pass the
  wrong kind.
- The hardcoded words became **`{text}`**, **`{href}`** and **`{buttonLabel}`**. Curly
  braces mean "drop the value in here."

Do the same to the `Hero`, so it takes props and passes the call-to-action values into the
`Card`:

```diff
 import { Card } from "@/components/card";

+interface HeroProps {
+  name: string;
+  subtitle: string;
+  ctaText: string;
+  ctaLabel: string;
+  ctaHref: string;
+}
+
-export const Hero = () => (
+export const Hero = ({ name, subtitle, ctaText, ctaLabel, ctaHref }: HeroProps) => (
   <div className="flex flex-col items-center gap-6 text-center">
-    <h1 className="font-bold text-4xl tracking-tight">Andrey Markin</h1>
-    <p className="text-lg text-muted-foreground">Building things on the internet with AI agents.</p>
+    <h1 className="font-bold text-4xl tracking-tight">{name}</h1>
+    <p className="text-lg text-muted-foreground">{subtitle}</p>
-    <Card />
+    <Card text={ctaText} buttonLabel={ctaLabel} href={ctaHref} />
   </div>
 );
```

That last `+` line: `Hero` hands the values it receives **down** to the `Card`.

## Now use it twice, differently

Now the home page passes the values in. Update `apps/web/src/app/page.tsx`:

```tsx
import { Hero } from "@/components/hero";

const Page = () => (
  <div className="flex min-h-svh flex-col items-center justify-center">
    <Hero
      name="Andrey Markin"
      subtitle="Building things on the internet with AI agents."
      ctaText="Curious who's behind this?"
      ctaLabel="Learn more"
      ctaHref="/about"
    />
  </div>
);

export default Page;
```

Same page as before, but the words live in the **props** now. Create
`apps/web/src/app/about/page.tsx` with the **same `Hero`, different values**:

```tsx
import { Hero } from "@/components/hero";

const AboutPage = () => (
  <div className="flex min-h-svh flex-col items-center justify-center">
    <Hero
      name="About me"
      subtitle="A few words about what I build and why."
      ctaText="Like what you see?"
      ctaLabel="Schedule a meeting"
      ctaHref="mailto:you@example.com"
    />
  </div>
);

export default AboutPage;
```

Open [https://web.localhost](https://web.localhost) and click **"Learn more"**. The About
page shows the same `Hero` and `Card`, now saying "Schedule a meeting" and opening an
email.

## How the data flows

The values you set on each page travel **down** through three layers:

```text
page.tsx                     you set the values here
  └─ <Hero name … ctaText="Curious who's behind this?" ctaLabel="Learn more" ctaHref="/about" />
        └─ <Card text="Curious who's behind this?" buttonLabel="Learn more" href="/about" />
              └─ <Link href="/about">Learn more</Link>     what finally renders
```

`Page` knows the content, `Hero` forwards the call-to-action parts to `Card`, and `Card`
turns them into a real link. Each brick does its own job and passes the rest along.

## One place to configure: a profile file

Your name and subtitle are now in **two** files, so changing your name means editing both.
Same fix as a component, but for **data**: one place, imported where you need it.

Make a folder `apps/web/src/lib` and a file `profile.ts` inside it. (`lib` is the common
name for supporting code that isn't a component.) Put **who you are** in one object, named
after yourself:

```ts
export const andrey = {
  name: "Andrey Markin",
  subtitle: "Building things on the internet with AI agents.",
  description:
    "Andrey Markin builds web apps and bots with AI coding agents — and teaches others to do the same.",
  email: "you@example.com",
};
```

A plain **constant**: no JSX, just data. It **exports** `andrey` the way your components
export themselves, so any file can **import** it. (`.ts` not `.tsx`: no markup.)

Now feed it into your pages. The home page becomes:

```tsx
import { Hero } from "@/components/hero";
import { andrey } from "@/lib/profile";

const Page = () => (
  <div className="flex min-h-svh flex-col items-center justify-center">
    <Hero
      name={andrey.name}
      subtitle={andrey.subtitle}
      ctaText="Curious who's behind this?"
      ctaLabel="Learn more"
      ctaHref="/about"
    />
  </div>
);

export default Page;
```

And the About page pulls from the **same** object, including your email for the button:

```tsx
import { Hero } from "@/components/hero";
import { andrey } from "@/lib/profile";

const AboutPage = () => (
  <div className="flex min-h-svh flex-col items-center justify-center">
    <Hero
      name={andrey.name}
      subtitle={andrey.subtitle}
      ctaText="Like what you see?"
      ctaLabel="Schedule a meeting"
      ctaHref={`mailto:${andrey.email}`}
    />
  </div>
);

export default AboutPage;
```

`andrey.name` reads "the `name` key out of the `andrey` object." The backticks in
`` `mailto:${andrey.email}` `` drop a value into the middle of text with `${…}`.

Edit `profile.ts` once and both pages update. Same import/export tool, sharing *data*
instead of *components*.

## Bonus: page titles for search and shares

That profile object works for **SEO** too: the title and description search engines and
chat apps show. In the App Router a page sets these by **exporting a `metadata` object**.
Add it to the top of `apps/web/src/app/page.tsx`:

```diff
+import type { Metadata } from "next";
 import { Hero } from "@/components/hero";
 import { andrey } from "@/lib/profile";

+export const metadata: Metadata = {
+  title: andrey.name,
+  description: andrey.description,
+};
+
 const Page = () => (
   <div className="flex min-h-svh flex-col items-center justify-center">
     <Hero
       name={andrey.name}
       subtitle={andrey.subtitle}
       ctaText="Curious who's behind this?"
       ctaLabel="Learn more"
       ctaHref="/about"
     />
   </div>
 );

 export default Page;
```

Save and look at your **browser tab**: it reads your name now. That `title` also shows in
Google results; `description` is the gray line underneath.

> Give the About page its own metadata too, e.g. `title: "About — " + andrey.name`. Each
> page exports its own. (`Metadata` is a Next.js type describing the allowed fields; your
> editor autocompletes them.)

## What's next

You built a `Hero` and a `Card` by hand, nested them, passed data down across two pages,
and pulled your details into one `profile.ts` that also feeds your page titles. Next
lesson: Tailwind for styling, **and shadcn/ui's ready-made `Card`** to swap yours for.

## Links

- Lesson page: https://andrey-markin.com/courses/vibe-coding/pages-and-components
- Course: https://andrey-markin.com/courses/vibe-coding.md
- Next lesson: https://andrey-markin.com/courses/vibe-coding/styling-and-shadcn.md
