Andrey Markin
  • home
  • services
  • projects
  • blog
  • directory
  • courses
    • Vibe Coding: Build Real Apps with AI Agents
  • resume
  • about
  • contact
  • meet

Mark Life Ltd

  1. Home
  2. Courses
  3. Vibe Coding
  4. Pages And Components
Meet

Mark Life Ltd

BG208147965

HomeContactPrivacyLLM-friendlyBlog RSSDirectory RSS

Vibe Coding: Build Real Apps with AI Agents

  1. 01Set Up Your Environment
  2. 02Find Your Way Around Your Project
  3. 03Pages and Components: Make It Yours
  4. 04Make It Look Good: Styling, Components, and Themes
  5. 05Deploying Your App to the Web
  6. 06Apps and Interfaces: Bots, Desktop, and Mobile
  7. 07Where Apps Store Data: Files, Key-Value, and Databases
  8. 08Fast, Temporary Storage with Redis
  9. 09Adding a Database
  10. 10Adding Authentication
  1. Courses
  2. Vibe Coding
  3. Pages and Components
Lesson 3 of 3

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.

Time to make the template yours. Last lesson you found the file that draws the demo — apps/web/src/app/page.tsx and its one <Demo /> line. This lesson you replace it. By the end the home page shows your content, built from components you create.

This one time, you'll do it yourself — not by prompting the agent. From here on the agent builds components for you constantly, but creating the first few files yourself is the fastest way to actually get what it's doing. Copy-paste the snippets below into the files — no need to type them out — and read the notes beside each. Once you've felt how the pieces snap together, you'll read the agent's output instantly and direct it far better. So: editor open, bun dev running, let's build.

What a component is

A component is a reusable piece of UI — think of it as a Lego brick. A button is a component. A card is a component. A whole page is just bricks snapped together.

Three things to hold onto:

  • They nest. A page holds a hero, which holds a card, which holds a button — bricks inside bricks. You'll build exactly that nesting today.
  • You write one, use it anywhere. Build a Card once, drop it on five pages.
  • Each lives in its own file. One brick, one file — easy to find, easy to change.

That's the whole mental model. Everything below is just how you snap bricks together in code.

Files, exports, and imports

A component lives in a file and exports itself — "here's my brick, anyone can use it." Another file imports it — "I'd like to use that brick here." That's the entire wiring system.

You already 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

The <Demo /> line is the brick we're about to swap out. (These are static imports — written at the top of the file, the normal kind. You don't need any other kind for a long while.)

What's that <div> stuff? What a component returns looks like HTML but lives inside your code — it's called JSX. For now, read it as "the markup this brick draws." className attaches styling; ignore the specific classes until next lesson.

Build the Card

We'll start from the inside out — the smallest brick first. Make a little call-to-action card: a line of text and a button.

In VS Code, find apps/web/src/components in the left sidebar, right-click it → 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 — it gives the card its border and padding and makes the link look like a button. Don't worry about reading it yet; styling is the whole next lesson. It's here only so your page looks like a real card instead of bare text. Focus on the structure, not the classes.

Why Link and not a plain <a>? Link is Next.js's version of a link — that's the import Link from "next/link" line up top. In the browser it still becomes an ordinary <a>, but for links to your own pages it switches between them instantly, with no full reload; for an email or outside link it just behaves like a normal link. So one Card can safely use it for both the /about link here and the mailto: button you'll add later.

Read what you just made:

  • export const Card = () => (...) defines the brick and exports it. The name is Capitalized — React requires that capital so it knows Card is a component, not plain HTML. (The file is lowercase, card.tsx; that's just convention.)
  • It returns JSX: a line of text (<p>) and a button that's really a link (<Link href=…>) — clicking it goes wherever href points. Here that's /about, a page we'll build soon.

Right now everything inside it is hardcoded — the words and the link are baked into the file. That's the simplest way to start. Hold that thought; we'll come back to it.

Build the Hero (it uses the Card)

Now the bigger brick. A hero is the intro block at the top of a page — your name and title — and ours will hold 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 the last one inside the <div>: Hero imports the Card brick and uses it — <Card /> — exactly the way a page will use <Hero />. Bricks inside bricks. (The @/components/card path uses the @/ shortcut from last lesson: "this app's src/components.")

Like the Card, everything here is hardcoded for now.

Put it on the home page

Open apps/web/src/app/page.tsx and replace the whole thing with this — 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. The wall of buttons is gone — your name, your subtitle, and a "Learn more" link are on screen. Plain and unstyled, exactly right for now.

See an error instead? Make sure bun dev is still running, then paste the error to your agent: "I wrote this and got this error — what's wrong?" Getting unstuck this way is a core skill, not cheating.

Reuse it on a second page — and hit a wall

Remember the rule from last lesson: folders in app/ become pages, the path becomes the URL. So a file at app/about/page.tsx gives you /about — the page that "Learn more" button is already pointing at. Let's build it and reuse the same Hero.

But there's a catch. On the About page you'd want the call to action to say something different — "Schedule a meeting" that opens your email, not "Learn more" that goes to /about. Yet every word in your Card and Hero is hardcoded. Drop <Hero /> on the About page and you'd get the exact same "Learn more" card. The bricks aren't truly reusable yet — they only know how to say one thing.

This is the moment props exist for.

Props: make the brick configurable

Props are inputs to a component — settings you pass in when you use it. Instead of baking the text into the Card, you let whoever uses it decide. Same brick, different contents.

Turn the hardcoded Card into a configurable one. Update card.tsx — the - lines are what you remove, the + lines are what you add:

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 brick's inputs — text, buttonLabel, href, all text (string). This is TypeScript describing what Card expects, so you can't forget one or pass the wrong kind.
  • The hardcoded sentence became {text}, and the hardcoded link became {href} and {buttonLabel}. Those curly braces mean "drop the value in here." The Card no longer decides what it says — its caller does.

Now do the same to the Hero, so it can be configured too — and so it can pass the call-to-action values down 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>);

Look at that last + line: Hero takes the ctaText, ctaLabel, and ctaHref it receives and hands them down to the Card. Hero is now a middle layer — data comes in from above, and it forwards the relevant pieces to the brick below.

Now use it twice — differently

With both bricks configurable, the home page has to actually pass 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 now the words live in the props, not buried inside the brick. And that is what unlocks the second page. 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 and click "Learn more" — it takes you to the About page, where the very same Hero and Card now say "Schedule a meeting" and open an email. One definition of each brick, reused on two pages, saying different things — because the words come in through props. That's the whole payoff: build the brick once, pour in different data each time.

How the data flows

Step back and look at what you built. The values you set on each page travel down through three layers of bricks:

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. Card turns them into a real link. Each brick handles only its own job and passes the rest along. This is the core pattern of every component-based app — and you just built it by hand.

One place to configure: a profile file

Notice a smell: your name and subtitle are now typed into two files — the home page and the About page. Change your name and you'd have to remember to edit both. The fix is the same idea as a component, but for data: keep it in one place and pull it in wherever you need it.

Make a new folder apps/web/src/lib, and inside it a file profile.ts. (lib is just a common name for "supporting code that isn't a component.") Put who you are in one object — name the const 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",
};

Use your own name for the const — this object is literally you, so andrey reads truer than a generic person. It's a plain constant: no JSX, no component, just data. It exports andrey exactly the way your components export themselves, so any file can import it. (.ts not .tsx, because there's no markup in it.)

Now feed it into your pages instead of hardcoded text. 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." And `mailto:${andrey.email}` builds the link from your email — the backticks let you drop a value into the middle of text with ${…}.

That's the win: your name, subtitle, and email now live in exactly one place. Edit profile.ts once and both pages update. You just used import/export to share data the same way you used it to share components — same tool, two jobs. As the site grows, this one file becomes the dial you turn to reconfigure it.

Bonus: be found — page titles for search and shares

One more thing that profile object is perfect for: SEO — the title and description search engines and chat apps show for your page. In the Next.js App Router, a page sets these by exporting a metadata object. Add it to the top of your home page, 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 now reads your name instead of the default. That title is what shows in the tab, in Google results, and on the search engine's listing; description is the gray line of text underneath. Both come straight from the same andrey object — write yourself once, reuse everywhere, even for search engines.

Give the About page its own metadata too, e.g. title: "About — " + andrey.name. Each page can export its own. (Metadata is a type from Next.js that describes the allowed fields — your editor will autocomplete them.)

What's next

You built a Hero and a Card from scratch, nested one inside the other, passed data down through both across two pages, then pulled your details into a single profile.ts and even wired it into your page titles. That's components, props, import/export, single-source config, and a first taste of SEO — all by hand. But it's bare. Next lesson: Tailwind for styling, and you'll meet shadcn/ui's ready-made Card — a polished version of the brick you just hand-built — and swap yours for it. Doing it by hand first is exactly what makes that click.

Previous