Pages and Components: Make It Yours
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
Cardonce, 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:
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 itThat <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."classNameattaches 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:
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
Linkand not a plain<a>?Linkis 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 oneCardcovers the/aboutlink and themailto: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:
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 />:
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. 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 devis 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:
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:
CardPropslists the inputs:text,buttonLabel,href, all text (string). That's TypeScript describing whatCardexpects, 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:
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:
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:
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 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:
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 rendersPage 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:
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:
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:
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:
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. (Metadatais 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.