Build a Personal Website with Astro
Build and deploy a 3-page personal website using Astro and Tailwind CSS.
Introduction
30 Second Summary
Everyone says you need a personal website, but staring at a blank screen is paralyzing. A modern framework gives you the structure to focus on content instead of boilerplate.
In this project, you will build and deploy a 3-page personal website using Astro and Tailwind CSS. You'll learn component-based architecture, utility-first styling, and static site deployment.
What You'll Build
A clean, minimal personal website with a Home page, an Experience page, and a Projects page, all styled with Tailwind CSS utility classes and deployed live to Vercel.
By the end of this project, you'll have:
- A reusable Layout component with navigation shared across all three pages.
- Three styled pages (Home, Experience, Projects) using Astro's file-based routing.
- A live deployment on Vercel connected to your GitHub repo that auto-deploys on every push.
- Secret Mission: A dark mode toggle that persists the user's theme preference using localStorage.
Are there any prerequisites?
You should have a basic understanding of HTML tags (like div, p, h1). You also need a GitHub account.
No prior experience with Astro, Tailwind CSS, or deployment is needed. This project walks you through everything from scratch.
Before We Start
Before We Start...
Set Up Your Astro Project
Every website needs a foundation. In this project, that foundation is Astro, a web framework that turns files into pages automatically and ships zero JavaScript to the browser by default.
In this step, you will scaffold a new Astro project, confirm it runs locally, and add Tailwind CSS for styling. By the end, you will have a working dev environment ready for building pages.
In this step, get ready to:
- Scaffold a new Astro project using the create-astro wizard.
- Start the dev server and preview your empty project in the browser.
- Add Tailwind CSS 4 for utility-first styling.
Scaffold a new Astro project
Astro provides an interactive setup wizard that creates your project folder, installs dependencies, and initializes git in one command.
- In VS Code, open the integrated terminal by pressing Ctrl+`.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Astro project by running this command:
npm create astro@latest
What will this command do?
This launches an interactive setup wizard in your terminal. It will ask you a series of questions to configure your new project. Follow the prompts below to answer each one.
- If asked to install create-astro, enter y to confirm.
- When asked where to create your project, type ./my-website and press Enter.
- When shown a list of templates, use the arrow keys to select the empty template, then press Enter.
- When asked to install dependencies, enter y.
- When asked to initialize a git repository, enter y.
- Confirm the project was created by running:
ls my-website
You should see files like astro.config.mjs, package.json, and a src folder in the output.
Command not found?
Make sure Node.js is installed and available in your terminal. Run node --version to confirm it returns a version number.
If you see a permissions error on macOS, try prefixing the command with sudo.
Help me troubleshoot this error
Open the project and start the dev server
Now that your project exists on disk, you need to open it in VS Code and start the development server to preview your site live in the browser.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the my-website folder, then click Open.
- Install the Astro language extension by clicking the Extensions icon in the left sidebar (or pressing Cmd+Shift+X on macOS / Ctrl+Shift+X on Windows).
- Search for Astro and click Install on the official Astro extension. This adds syntax highlighting for .astro files.
- Open a new terminal in VS Code by pressing Ctrl+`.
- Start the development server by running:
npm run dev
What does this command do?
This starts Astro's local development server. It watches your files for changes and automatically refreshes your browser preview whenever you save.
- Open your browser and go to http://localhost:4321.
You should see the Astro empty project starter page in your browser.
Don't see anything at localhost:4321?
Check your terminal for error messages. If port 4321 is already in use, Astro will display a different port number in the terminal output. Use that URL instead.
Make sure you ran npm run dev from inside the my-website folder. The terminal should show the path ending in my-website.
Help me figure out why my site isn't loading
Add Tailwind CSS to your project
Tailwind CSS lets you style elements directly in your HTML using utility classes like text-xl and flex. Instead of writing custom CSS files, you compose styles right where you use them.
- Stop the dev server by pressing Ctrl+C in the terminal.
- Add Tailwind CSS to your project by running this command:
npx astro add tailwind
What does this command do?
Astro's add command automates integration setup. In this case, it installs the @tailwindcss/vite plugin, creates src/styles/global.css with the Tailwind import, and updates astro.config.mjs to use the plugin.
This replaces the older @astrojs/tailwind integration, which is now deprecated. The Vite plugin is the current recommended approach.
- When the command asks to confirm the changes, enter y for each prompt to proceed.
- Verify the configuration was created by opening astro.config.mjs in the VS Code Explorer sidebar.
- Also open src/styles/global.css to confirm it contains the Tailwind import.
Your astro.config.mjs should show the @tailwindcss/vite plugin configured under vite.plugins. Your global.css should contain @import "tailwindcss".
Files don't look right?
If astro.config.mjs was not updated or src/styles/global.css was not created, try running npx astro add tailwind again and make sure to confirm each prompt with y.
Help me fix my Tailwind CSS setup
- Restart the dev server by running:
npm run dev
The terminal should show Astro running again at http://localhost:4321. Tailwind CSS is now available for use in your project.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved and your dev server is running before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point:
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
@import "tailwindcss";
Your environment is ready. You have a working Astro project with Tailwind CSS configured. Next up, you will create a reusable Layout component that every page on your site will share.
Create Your Layout Component
Every page on your website shares common elements like navigation links and a footer. Without a reusable wrapper, you'd copy-paste the same HTML into every single page file.
Astro solves this with layout components. You'll create one Layout file that provides the shared HTML shell, and each page will slot its unique content into it.
In this step, get ready to:
- Create the Layout file with its HTML shell and Tailwind import.
- Add a responsive navigation bar with links to all three pages.
- Add a content slot and footer.
Create the Layout file and HTML shell
Astro components have two sections. A frontmatter script section (between --- fences) holds your JavaScript logic. The HTML template below it defines what renders on the page.
- In the Explorer sidebar in VS Code, right-click on the src folder.
- Click New Folder and name it layouts.
- Right-click the new layouts folder and click New File.
- Name the file Layout.astro and press Enter.
- Paste the following code into Layout.astro:
---
import '../styles/global.css';
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body class="bg-white text-gray-900 min-h-screen">
</body>
</html>
What does this code do?
The frontmatter imports your global CSS file so Tailwind CSS is available on every page that uses this layout.
The interface Props block defines what data this component accepts. Here, each page must pass a title string. Astro.props gives you access to that value inside the template.
The HTML template provides the full page skeleton. The {title} expression in the <title> tag dynamically inserts whatever title the page passes in.
- Save Layout.astro.
- Confirm the file exists by checking the Explorer sidebar. You should see Layout.astro nested inside src/layouts/.
Don't see a layouts folder?
Make sure you created the folder inside src/, not at the project root. The path should be src/layouts/Layout.astro.
help me find the right folder structure for my Astro layout
Add a navigation bar
Your navigation bar gives visitors a way to move between your three pages. You'll style it with Tailwind utility classes to position the links on the right side of the page.
- In Layout.astro, find the opening <body> tag.
- Add the following navigation code between the opening <body> tag and the closing </body> tag:
<header>
<nav class="max-w-3xl mx-auto px-6 py-4 flex items-center justify-end gap-6">
<a href="/" class="text-gray-700 hover:text-gray-900 transition-colors">Home</a>
<a href="/experience" class="text-gray-700 hover:text-gray-900 transition-colors">Work</a>
<a href="/projects" class="text-gray-700 hover:text-gray-900 transition-colors">Projects</a>
</nav>
</header>
What do these Tailwind classes do?
- max-w-3xl mx-auto centers the nav and caps its width so content doesn't stretch edge-to-edge on wide screens.
- flex items-center justify-end gap-6 arranges the links in a horizontal row, pushed to the right side, with even spacing between them.
- hover:text-gray-900 transition-colors darkens each link on hover with a smooth color transition.
- Save Layout.astro.
- You won't see a change in the browser yet because no page imports this layout. The next step connects it.
Seeing a syntax error in your terminal?
Make sure the <header> block sits between the opening <body> tag and the closing </body> tag. Check that every opening tag has a matching closing tag.
help me fix a syntax error in my Layout.astro navigation code
Add the content slot and footer
The <slot /> element is where Astro injects each page's unique content. Think of it as a placeholder that says "put the page content here."
- In Layout.astro, find the closing </header> tag you just added.
- Add the following code below </header> (still inside <body>, before </body>):
<main class="max-w-3xl mx-auto px-6 py-12">
<slot />
</main>
<footer class="mt-16">
<div class="max-w-3xl mx-auto px-6 py-6 text-center text-gray-400 text-sm">
© {new Date().getFullYear()} Built with Astro + Tailwind CSS.
</div>
</footer>
What does this code do?
- <slot /> is the injection point. When a page wraps its content in this Layout, that content appears here.
- max-w-3xl mx-auto px-6 py-12 constrains the main content to a readable width, centers it horizontally, and adds comfortable padding.
- new Date().getFullYear() dynamically inserts the current year so your copyright line never goes stale.
- Save Layout.astro.
- The layout is now complete. In the next step, you'll connect your home page to this layout and see the nav, content, and footer appear in the browser.
Dev server showing an error?
Double check that the <main> and <footer> blocks are inside <body> but outside <header>. Each section should be a sibling, not nested inside one another.
help me fix the structure of my Layout.astro file
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete Layout.astro file. Make sure yours matches:
---
import '../styles/global.css';
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body class="bg-white text-gray-900 min-h-screen">
<header>
<nav class="max-w-3xl mx-auto px-6 py-4 flex items-center justify-end gap-6">
<a href="/" class="text-gray-700 hover:text-gray-900 transition-colors">Home</a>
<a href="/experience" class="text-gray-700 hover:text-gray-900 transition-colors">Work</a>
<a href="/projects" class="text-gray-700 hover:text-gray-900 transition-colors">Projects</a>
</nav>
</header>
<main class="max-w-3xl mx-auto px-6 py-12">
<slot />
</main>
<footer class="mt-16">
<div class="max-w-3xl mx-auto px-6 py-6 text-center text-gray-400 text-sm">
© {new Date().getFullYear()} Built with Astro + Tailwind CSS.
</div>
</footer>
</body>
</html>
Your reusable layout is ready to go. Next up, you'll build the Home page and plug it into this layout to see your navigation and footer come alive in the browser.
Build the Home Page
Your Layout component is ready and wrapping every page with shared navigation and a footer. Now it's time to build the first page visitors will see.
In this step, you'll create a hero section for your home page. It features a profile photo alongside your name, tagline, and a short bio, arranged in a responsive two-column layout using Tailwind CSS flexbox utilities.
In this step, get ready to:
- Import the Layout component and set up a responsive hero section.
- Add a profile photo with circular styling.
- Write your personal statement with Tailwind typography classes.
Replace the home page with a hero section
The default index.astro file still has Astro's empty scaffold content. You'll replace it with a proper home page that imports your Layout and displays a hero section.
- In VS Code, open src/pages/index.astro from the Explorer sidebar.
- Select all the existing content and delete it.
- Replace it with the following code:
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Home">
<section class="flex flex-col md:flex-row items-center gap-8">
<img
src="/profile.jpg"
alt="Profile photo"
class="rounded-full w-48 h-48 object-cover"
/>
<div>
<h1 class="text-4xl font-bold text-gray-900">Hi, I'm [Your Name]</h1>
<p class="text-xl text-gray-600 mt-2">Software Developer & Builder</p>
<p class="text-gray-700 mt-4 leading-relaxed">
I'm passionate about building things that live on the internet. Whether it's
websites, applications, or anything in between, I love creating solutions
that make a difference. Currently exploring web development and documenting
my journey along the way.
</p>
</div>
</section>
</Layout>
What does this code do?
The frontmatter section (between the --- fences) imports your Layout component. Everything below the frontmatter is the HTML template that gets injected into the Layout's <slot /> element.
- flex flex-col md:flex-row stacks the photo and text vertically on mobile, then places them side-by-side on medium screens and up.
- items-center gap-8 vertically centers both columns and adds spacing between them.
- rounded-full w-48 h-48 object-cover makes the image a perfect circle and crops it to fill a 192px square.
- Save index.astro.
- Switch to your browser at http://localhost:4321. You should see your navigation bar at the top, a broken image icon on the left (the photo file doesn't exist yet), and your name and bio text on the right.
Seeing a blank page or an error?
- Make sure the import path is exactly '../layouts/Layout.astro' with the correct relative path from src/pages/ up to src/layouts/.
- Check that your frontmatter section has both opening and closing --- fences. Missing one causes a parse error.
- Confirm the dev server is still running in your terminal. If it stopped, run npm run dev again.
Why isn't my home page rendering?
Add your profile photo
Astro's public/ folder holds static assets like images that are served directly without processing. Any file you place here is accessible at the root URL path.
- Find a photo you'd like to use as your profile picture. A square image works best since the styling crops it into a circle.
- Rename the file to profile.jpg.
- In VS Code's Explorer sidebar, locate the public/ folder at the root of your project.
- Drag and drop profile.jpg into the public/ folder.
Why the public/ folder?
Files in public/ are copied directly to your final build without any transformation. A file at public/profile.jpg becomes available at /profile.jpg in your browser. This is perfect for images, favicons, and other static files that don't need processing.
- Refresh your browser at http://localhost:4321. You should now see your profile photo displayed as a circle on the left side of the hero section.
Still seeing a broken image?
- Verify the filename is exactly profile.jpg (lowercase, no spaces). The src attribute in the code must match the filename exactly.
- Make sure the image is directly inside public/ and not inside a subfolder.
- If your image is a .png or .webp file, update the src attribute in index.astro to match your file extension (e.g. /profile.png).
Help me fix my broken profile image in Astro.
Customize your personal statement
The hero section already has placeholder text for your name, tagline, and bio. Now is a good time to make it yours.
- In index.astro, find the <h1> tag and replace [Your Name] with your actual name.
- Update the tagline in the first <p> tag to describe what you do.
- Rewrite the bio paragraph to reflect your own background and interests.
- Save index.astro and check your browser to confirm the updated text appears.
What do the typography classes do?
- text-4xl font-bold text-gray-900 on the <h1> makes your name large, bold, and near-black.
- text-xl text-gray-600 mt-2 on the tagline creates a slightly smaller, lighter line with a small top margin.
- text-gray-700 mt-4 leading-relaxed on the bio paragraph sets readable body text with generous line spacing.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.astro and that your profile photo is in the public/ folder.
ⓧ I'd like to double check the full code
Here's the complete src/pages/index.astro file:
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Home">
<section class="flex flex-col md:flex-row items-center gap-8">
<img
src="/profile.jpg"
alt="Profile photo"
class="rounded-full w-48 h-48 object-cover"
/>
<div>
<h1 class="text-4xl font-bold text-gray-900">Hi, I'm [Your Name]</h1>
<p class="text-xl text-gray-600 mt-2">Software Developer & Builder</p>
<p class="text-gray-700 mt-4 leading-relaxed">
I'm passionate about building things that live on the internet. Whether it's
websites, applications, or anything in between, I love creating solutions
that make a difference. Currently exploring web development and documenting
my journey along the way.
</p>
</div>
</section>
</Layout>
Your home page is live with a clean hero section. Next up, you'll build a work experience page that dynamically renders data from an array using Astro's frontmatter.
Build the Experience Page
Your Home page is looking great with a hero section and profile photo. Now it's time to show off your professional background.
In this step, you'll build an Experience page that displays your work history in a clean, readable format. You'll also learn how Astro's frontmatter lets you write JavaScript that feeds directly into your HTML template.
In this step, get ready to:
- Define work experience data as a JavaScript array in Astro's frontmatter.
- Render that data dynamically using JavaScript's .map() method.
Create the experience page and define your data
Instead of hardcoding each job entry as raw HTML, you'll define your experience data as a JavaScript array. This makes it easy to add, remove, or reorder entries without touching the template structure.
- In the VS Code Explorer sidebar, right-click on the src/pages/ folder.
- Click New File.
- Name it experience.astro.
- Paste the following code into your new file:
---
import Layout from '../layouts/Layout.astro';
// Define work experience as structured data
const experiences = [
{
company: "Tech Company",
role: "Software Developer",
bullets: [
"Built and maintained web applications serving thousands of users daily.",
"Implemented new features using modern JavaScript frameworks and REST APIs.",
"Collaborated with cross-functional teams to deliver products on schedule."
]
},
{
company: "Startup Inc",
role: "Junior Developer",
bullets: [
"Developed front-end features using React and TypeScript.",
"Collaborated with the design team to implement responsive user interfaces.",
"Wrote unit tests and participated in code reviews to maintain code quality."
]
},
{
company: "Dev Agency",
role: "Software Engineering Intern",
bullets: [
"Learned the fundamentals of full-stack web development.",
"Built internal tools that automated repetitive manual processes.",
"Assisted senior developers with client projects and bug fixes."
]
}
];
---
<Layout title="Work">
<h1 class="text-4xl font-bold text-gray-900 mb-2">Work</h1>
</Layout>
What does this code do?
The code between the --- fences is Astro's frontmatter. It runs at build time, not in the browser. You can write any JavaScript here and use the results in your HTML template below.
The experiences array holds structured data for each job. Each object has a company name, a role title, and an array of bullets describing accomplishments.
- Save experience.astro.
- In your browser, navigate to http://localhost:4321/experience. You should see the page heading "Work" displayed at the top.
How does file-based routing work?
Because you created a file called experience.astro inside src/pages/, Astro automatically makes it available at the /experience route. No router configuration needed.
Seeing a 404 page?
Make sure your file is named exactly experience.astro (lowercase) and is inside the src/pages/ folder. If it's in a different folder, the route won't work.
Also check that your dev server is still running in the terminal.
Why isn't my experience page showing up at /experience?
Render your experience data with .map()
Now that your data is defined, you'll use JavaScript's .map() method to loop over the array and render each entry as HTML. This keeps your template clean and your data easy to update.
- In experience.astro, find the empty space between the <h1> tag and the closing </Layout> tag.
- Add the following section to render your work experience:
<section class="mt-10">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Work Experience</h2>
<div class="space-y-10">
{experiences.map((exp) => (
<div>
<h3 class="text-xl font-bold text-gray-900">{exp.company}</h3>
<p class="font-semibold text-gray-700 mt-1">{exp.role}</p>
<ul class="list-disc list-inside mt-3 space-y-2 text-gray-700 leading-relaxed">
{exp.bullets.map((bullet) => (
<li>{bullet}</li>
))}
</ul>
</div>
))}
</div>
</section>
What does this code do?
- experiences.map((exp) => ...) loops over each object in your array and outputs HTML for each one.
- The outer .map() renders one block per company. The inner exp.bullets.map() renders each accomplishment as a list item.
- space-y-10 adds generous vertical spacing between each company block, keeping the page readable.
- Save experience.astro.
- Refresh your browser at http://localhost:4321/experience. You should see three work experience entries, each with a bold company name, a role title, and bullet-point accomplishments.
Page is blank or shows an error?
Check for missing curly braces around the .map() expressions. Every {experiences.map(...)} needs matching opening and closing braces.
Also make sure the section code is placed between the <h1> line and the </Layout> closing tag. Code outside the Layout won't render.
Help me debug my experience page rendering with .map() in Astro.
Customize with your real experience
Replace the placeholder company names, roles, and bullet points with your own work history. The structure stays the same. Just update the string values in the experiences array.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete experience.astro file for comparison:
---
import Layout from '../layouts/Layout.astro';
const experiences = [
{
company: "Tech Company",
role: "Software Developer",
bullets: [
"Built and maintained web applications serving thousands of users daily.",
"Implemented new features using modern JavaScript frameworks and REST APIs.",
"Collaborated with cross-functional teams to deliver products on schedule."
]
},
{
company: "Startup Inc",
role: "Junior Developer",
bullets: [
"Developed front-end features using React and TypeScript.",
"Collaborated with the design team to implement responsive user interfaces.",
"Wrote unit tests and participated in code reviews to maintain code quality."
]
},
{
company: "Dev Agency",
role: "Software Engineering Intern",
bullets: [
"Learned the fundamentals of full-stack web development.",
"Built internal tools that automated repetitive manual processes.",
"Assisted senior developers with client projects and bug fixes."
]
}
];
---
<Layout title="Work">
<h1 class="text-4xl font-bold text-gray-900 mb-2">Work</h1>
<section class="mt-10">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Work Experience</h2>
<div class="space-y-10">
{experiences.map((exp) => (
<div>
<h3 class="text-xl font-bold text-gray-900">{exp.company}</h3>
<p class="font-semibold text-gray-700 mt-1">{exp.role}</p>
<ul class="list-disc list-inside mt-3 space-y-2 text-gray-700 leading-relaxed">
{exp.bullets.map((bullet) => (
<li>{bullet}</li>
))}
</ul>
</div>
))}
</div>
</section>
</Layout>
Your Experience page is live and looking professional. Next up, you'll build a Projects page to showcase the things you've built.
Build the Projects Page
Your Experience page displays professional history in a clean, readable format. The final content page is your Projects page.
This page showcases the things you've built. You'll define project data in frontmatter (the same pattern you used for the Experience page) and render each project as a simple text block with a clickable link.
In this step, get ready to:
- Define your projects data as an array of objects in frontmatter.
- Render each project with a title, link, and description.
Create the projects page and define your data
Just like the Experience page, you'll define your project data as a JavaScript array in Astro's frontmatter. Each project object holds a name, a link URL, and a description.
- In VS Code's Explorer sidebar, right-click the src/pages folder.
- Click New File and name it projects.astro.
- Add the following frontmatter to define your projects data:
---
import Layout from '../layouts/Layout.astro';
// Define your projects as an array of objects
const projects = [
{
name: "Reliable Data Transport Protocol Implementation",
link: "https://github.com/username/project-1",
description: "Developed a reliable data transport protocol for computer networks. Designed and implemented mechanisms to ensure data packets are delivered accurately and in order, including handling packet loss, duplication, and reordering."
},
{
name: "Traceroute Tool",
link: "https://github.com/username/project-2",
description: "Developed a traceroute tool to analyze network paths and diagnose network issues. The project involved sending probe packets with incrementing time-to-live values and recording the routers they traversed."
},
{
name: "Personal Website",
link: "https://github.com/username/my-website",
description: "A 3-page portfolio site built with Astro and Tailwind CSS. Includes a home page, work experience, and project showcase."
}
];
---
What does this code do?
The frontmatter imports your Layout component and defines a projects array. Each object in the array has three properties:
- name is the project title displayed as a heading.
- link is the URL to the project (this will be optional in the template).
- description is a short paragraph explaining what the project does.
You won't see a change in the browser yet. The next chunk adds the template that renders your data.
Render projects as a stacked list
The template maps over your data array and renders each project as a simple text block. No cards, no grid layout. Just clean, readable text with generous spacing between entries.
- In projects.astro, add this template directly below the closing --- of your frontmatter:
<Layout title="Projects">
<h1 class="text-4xl font-bold text-gray-900 mb-10">Projects</h1>
<!-- Loop over the projects array and render each one -->
<div class="space-y-10">
{projects.map((project) => (
<div>
<h2 class="text-xl font-bold text-gray-900">{project.name}</h2>
{project.link && (
<p class="mt-1">
<span class="text-gray-600">link: </span>
<a href={project.link} class="text-blue-600 hover:underline">{project.link}</a>
</p>
)}
<p class="text-gray-700 mt-3 leading-relaxed">{project.description}</p>
</div>
))}
</div>
</Layout>
What does this template do?
The template wraps everything in your Layout component with the title "Projects". The page heading uses mb-10 for generous spacing before the list begins.
The .map() method loops over each project object. For each one, it renders the name as a bold heading, a conditional link line (only shown if project.link exists), and the description as a paragraph below.
- Save projects.astro.
- In your browser, navigate to http://localhost:4321/projects. You should see three projects listed vertically with bold titles, blue clickable links, and description text below each one.
- Click the Home and Work links in the navigation to confirm all three pages are working and connected through your shared Layout.
Page not showing up at /projects?
Make sure your file is named exactly projects.astro and is located inside src/pages/. Astro's file-based routing turns each file in this folder into a URL path.
If you see an error in the terminal, check that the opening --- and closing --- fences are both present. A missing fence will cause a syntax error.
Help me debug my projects page
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete src/pages/projects.astro file for comparison:
---
import Layout from '../layouts/Layout.astro';
const projects = [
{
name: "Reliable Data Transport Protocol Implementation",
link: "https://github.com/username/project-1",
description: "Developed a reliable data transport protocol for computer networks. Designed and implemented mechanisms to ensure data packets are delivered accurately and in order, including handling packet loss, duplication, and reordering."
},
{
name: "Traceroute Tool",
link: "https://github.com/username/project-2",
description: "Developed a traceroute tool to analyze network paths and diagnose network issues. The project involved sending probe packets with incrementing time-to-live values and recording the routers they traversed."
},
{
name: "Personal Website",
link: "https://github.com/username/my-website",
description: "A 3-page portfolio site built with Astro and Tailwind CSS. Includes a home page, work experience, and project showcase."
}
];
---
<Layout title="Projects">
<h1 class="text-4xl font-bold text-gray-900 mb-10">Projects</h1>
<div class="space-y-10">
{projects.map((project) => (
<div>
<h2 class="text-xl font-bold text-gray-900">{project.name}</h2>
{project.link && (
<p class="mt-1">
<span class="text-gray-600">link: </span>
<a href={project.link} class="text-blue-600 hover:underline">{project.link}</a>
</p>
)}
<p class="text-gray-700 mt-3 leading-relaxed">{project.description}</p>
</div>
))}
</div>
</Layout>
Why keep it simple?
You might be tempted to add fancy cards or a grid layout. The stacked-text approach keeps the focus on content and is easier to scan. You can always add visual flair later once the structure is solid.
All three pages of your website are now built and connected through the shared navigation. Next up, you'll push this project to GitHub and deploy it live on the internet with Vercel.
Deploy to Vercel
You've built a complete three-page personal website. Right now it only lives on your computer.
In this step, you'll push your code to GitHub and deploy it to Vercel. Once deployed, anyone with the URL can visit your site.
In this step, get ready to:
- Push your project to a GitHub repository.
- Deploy your site to Vercel with automatic builds.
- Visit your live website at a public URL.
Push your project to GitHub
Your project already has Git initialized from the Astro setup wizard. You need to commit your latest changes, create a GitHub repository, and push your code up.
- Stage and commit all your project files by running these commands in the VS Code terminal:
git add .
git commit -m "Add personal website pages"
What do these commands do?
- git add . stages every file in the project so Git knows to include them in the next snapshot.
- git commit -m "..." saves that snapshot with a descriptive message.
- Open GitHub in your browser and navigate to github.com/new to create a new repository.
- Enter my-website as the repository name.
- Leave the repository set to Public.
- Do NOT check any boxes for README, .gitignore, or license. Your project already has these files locally.
- Click Create repository.
GitHub shows a page with setup instructions. You only need the commands for pushing an existing repository.
- Connect your local project to GitHub and push by running these commands:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/my-website.git
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin tells Git where to push your code (your GitHub repo URL).
- git branch -M main ensures your local branch is named main to match GitHub's default.
- git push -u origin main uploads all your committed files to GitHub.
- Refresh your GitHub repository page in your browser.
- You should see all your project files listed, including src/, public/, and astro.config.mjs.
Push failed or asking for credentials?
- If Git asks for a username and password, GitHub no longer accepts passwords for HTTPS. You need a Personal Access Token. Go to GitHub Settings, then Developer settings, then Personal access tokens to generate one.
- If you see "remote origin already exists", run git remote set-url origin followed by your URL instead.
Help me push my Astro project to GitHub
Deploy to Vercel
Vercel connects directly to your GitHub repo. Every time you push new code, Vercel automatically rebuilds and redeploys your site.
- Open vercel.com in your browser.
- Click Sign Up (or Log In if you already have an account).
- Choose Continue with GitHub to connect your GitHub account.
- Authorize Vercel to access your GitHub repositories when prompted.
What is Vercel?
Vercel is a cloud platform that hosts websites and web applications. Its Hobby plan is free for personal projects. It auto-detects your framework and handles all the build configuration for you.
- From the Vercel dashboard, click Add New... and select Project.
- Find your my-website repository in the list and click Import.
- Vercel auto-detects Astro as the framework and fills in the build settings for you. Leave everything as-is.
- Click Deploy.
Vercel builds your site and deploys it. This takes about a minute. When it finishes, you see a congratulations screen with a preview of your live site.
- Click the preview image or the generated URL to visit your live website.
- Your site is now live at a URL like my-website-abc123.vercel.app.
- Test that all three pages work by clicking the navigation links for Home, Work, and Projects.
Deployment failed?
- Check the build logs on the Vercel deployment page. The most common issue is a missing dependency or a typo in an import path.
- Make sure your astro.config.mjs matches the code from earlier in this project. A broken config prevents the build from starting.
- If you see a Node version error, Vercel uses Node 20 by default. Your project works on Node 22+. In your Vercel project settings under General, set the Node.js version to 22.x and redeploy.
Help me fix my Vercel deployment error
What is continuous deployment?
Now that Vercel is connected to your GitHub repo, every future push to the main branch automatically triggers a new build and deployment. You never need to manually deploy again.
(Optional) Add a custom domain
If you own a domain name, you can point it at your Vercel deployment. This step is optional.
- In the Vercel project dashboard, click Settings in the top navigation.
- Select Domains from the left sidebar.
- Enter your domain name and click Add.
- Follow the DNS configuration instructions Vercel provides. Vercel gives you free HTTPS automatically.
No domain? No problem
The .vercel.app URL works perfectly well for sharing your site. You can always add a custom domain later.
Secret mission
Add Dark Mode Toggle
Your website looks great in light mode, but what about visitors who prefer a darker aesthetic? In this secret mission, you'll add a dark mode toggle that remembers the user's preference across page refreshes using localStorage.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and deploys to Vercel's free Hobby plan, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local my-website project folder on your Desktop.
- GitHub repository containing your project code.
- Vercel project and deployment (free Hobby plan).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep your site live.
- Your site stays live at your Vercel URL at no cost.
- Your GitHub repo remains available for future edits.
- Your local my-website folder stays on your Desktop.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep your files and deployment intact.
- If your dev server is still running in the terminal, press Ctrl+C to stop it.
- Your Vercel deployment stays live at no cost. You can pause it later from the Vercel dashboard if needed.
- Keep your local my-website folder and GitHub repo as they are.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete your local project folder by running the appropriate command for your system:
macOS
cd ~/Desktop
rm -rf my-website
Windows
cd ~\Desktop
Remove-Item -Recurse -Force my-website
- Delete your GitHub repository:
- Go to your my-website repository on GitHub.
- Click Settings.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository, then click I have read and understand these effects.
- Type the repository name to confirm, then click Delete this repository.
- Delete your Vercel project:
- Go to your Vercel dashboard and select the my-website project.
- Open Settings in the sidebar.
- Scroll to the bottom of the General page and click Delete in the Delete Project section.
- Confirm by entering the project name, then click Continue.
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a personal website from scratch using modern web tooling.
You've learned how to:
- Built a 3-page website using Astro's component system and file-based routing to create a reusable Layout with shared navigation across all pages.
- Styled everything with Tailwind CSS utility classes and used JavaScript in Astro's frontmatter to dynamically render experience and project data.
- Deployed your site live to the internet with Vercel + GitHub integration, giving you automatic redeployments on every push.
- Added a dark mode toggle that persists the user's theme preference using localStorage.
Ready to quiz yourself?