๐ Lesson 18: Building Content Pages
Your homepage is live โ navigation, hero, cards, featured recipe, footer. Now it's time to build the pages those links actually point to. In this lesson, you'll create the About, Recipe, and Contact pages, each with its own layout patterns. You'll reuse the nav and footer from Lesson 17 (copy-paste is correct here), learn new CSS techniques like profile layouts and styled forms, and see how a consistent style guide makes multi-page development surprisingly fast.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Reuse shared components (nav, footer) across multiple pages
- Build a profile/about page with an image-and-text layout
- Create a recipe page with a card grid and a detailed recipe layout
- Build and style an accessible contact form
- Use consistent classes and custom properties across all pages
- Mark the current page in the navigation with the
activeclass
Estimated Time: 60โ90 minutes
Prerequisites: Lesson 17 (Building the Homepage) โ you need a working index.html with nav, footer, and a css/style.css with all the styles built so far.
๐ In This Lesson
Reusing Shared Components
Every page on your site shares two things: the navigation bar and the footer. In a production site, you'd use a template engine or a framework to avoid duplicating this code. But for a static HTML site, the correct approach is straightforward: copy the nav and footer from your homepage into each new page.
The Multi-Page Workflow
(Homepage)"] -->|"Copy nav + footer"| B["about.html"] A -->|"Copy nav + footer"| C["recipe.html"] A -->|"Copy nav + footer"| D["contact.html"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#1e293b style C fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#1e293b style D fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#1e293b
For each new page:
- Copy the entire
index.htmlfile - Rename it (
about.html,recipe.html,contact.html) - Keep the
<head>, nav, footer, and<script>tags - Replace the
<title>and the content between the nav and footer - Move the
activeclass to the correct nav link
๐ก The Duplication Trade-off
Yes, copying the nav into four files means updating four files if you add a new page. This is the trade-off of static HTML. For a small 4โ6 page site, it's perfectly manageable. When it becomes painful, that's your signal to learn a tool like a static site generator (e.g., Eleventy) or a framework (e.g., React). But don't reach for those tools until plain HTML becomes limiting โ you'll understand the tools better when you know what problem they solve.
About Page โ HTML
The About page introduces you to visitors. It typically includes a photo, a short bio, your story, and some personal details. The layout pattern is a profile section (image beside text) followed by prose sections.
Create about.html (or replace the boilerplate). Use the same <head>, nav, and footer from your homepage, but change the <title> and the content:
<!-- about.html โ content between nav and footer -->
<main>
<section class="page-header">
<div class="container">
<h1>About Me</h1>
<p class="page-subtitle">The person behind the recipes.</p>
</div>
</section>
<section class="profile">
<div class="container">
<div class="profile-content">
<div class="profile-image">
<img src="images/about-photo.jpg"
alt="A smiling person in a kitchen holding a wooden spoon"
width="400" height="400">
</div>
<div class="profile-text">
<h2>Hi, I'm Alex!</h2>
<p>I'm a home cook who believes that great food doesn't
need to be complicated. What started as a quarantine
hobby turned into a genuine passion โ and this site
is where I share what I've learned along the way.</p>
<p>My cooking philosophy is simple: use fresh ingredients,
don't skip the garlic, and always taste as you go.
Every recipe on this site has been tested at least
three times in my own kitchen before making it here.</p>
</div>
</div>
</div>
</section>
<section class="about-story">
<div class="container">
<h2>My Cooking Journey</h2>
<p>I grew up watching my grandmother cook โ no recipes,
no measuring cups, just instinct and decades of practice.
I always admired that but never thought I could learn it
myself. Then the world changed, I had time, and I started
with scrambled eggs. Really bad scrambled eggs.</p>
<p>Three years and a lot of burnt garlic later, I can
confidently say I make a mean pasta, a respectable curry,
and the best banana bread my friends have ever tasted
(their words, not mine). This site is my way of documenting
what I've learned and sharing it with anyone who wants
to start their own kitchen journey.</p>
</div>
</section>
<section class="about-interests">
<div class="container">
<h2>When I'm Not Cooking</h2>
<div class="interests-grid">
<div class="interest-item">
<span class="interest-icon">๐</span>
<h3>Reading</h3>
<p>Mostly cookbooks, but also sci-fi and history.</p>
</div>
<div class="interest-item">
<span class="interest-icon">๐ฑ</span>
<h3>Gardening</h3>
<p>Growing herbs on my balcony โ basil, mint, rosemary.</p>
</div>
<div class="interest-item">
<span class="interest-icon">๐ต</span>
<h3>Music</h3>
<p>Cooking with a good playlist is non-negotiable.</p>
</div>
<div class="interest-item">
<span class="interest-icon">โ๏ธ</span>
<h3>Travel</h3>
<p>Finding new flavors in every city I visit.</p>
</div>
</div>
</div>
</section>
</main>
Structure Breakdown
- Page header โ a simple banner with the page title and subtitle. This replaces the hero section from the homepage.
- Profile section โ image and text side by side, using the same Flexbox pattern as the featured recipe in Lesson 17.
- Story section โ plain prose paragraphs. Not everything needs a fancy layout โ sometimes text is just text.
- Interests grid โ small cards showing hobbies. Reuses the card grid pattern from the homepage feature cards.
About Page โ CSS
Add these styles to your css/style.css. Notice how many existing classes we reuse โ the style guide pays off:
/* ===========================
Page Header (shared across content pages)
=========================== */
.page-header {
background-color: var(--color-bg-alt);
padding: var(--space-2xl) 0;
text-align: center;
}
.page-subtitle {
color: var(--color-text-light);
font-size: 1.125rem;
margin-top: var(--space-sm);
}
/* ===========================
Profile Section (About Page)
=========================== */
.profile {
padding: var(--space-2xl) 0;
}
.profile-content {
display: flex;
gap: var(--space-2xl);
align-items: center;
flex-wrap: wrap;
}
.profile-image {
flex: 0 1 300px;
}
.profile-image img {
width: 100%;
height: auto;
border-radius: 50%;
object-fit: cover;
aspect-ratio: 1 / 1;
}
.profile-text {
flex: 1 1 400px;
}
.profile-text h2 {
margin-bottom: var(--space-md);
}
.profile-text p {
line-height: 1.7;
margin-bottom: var(--space-md);
}
/* ===========================
Story Section
=========================== */
.about-story {
background-color: var(--color-bg-alt);
padding: var(--space-2xl) 0;
}
.about-story h2 {
margin-bottom: var(--space-lg);
}
.about-story p {
line-height: 1.7;
margin-bottom: var(--space-md);
max-width: 720px;
}
/* ===========================
Interests Grid
=========================== */
.about-interests {
padding: var(--space-2xl) 0;
}
.about-interests h2 {
text-align: center;
margin-bottom: var(--space-xl);
}
.interests-grid {
display: flex;
gap: var(--space-lg);
flex-wrap: wrap;
justify-content: center;
}
.interest-item {
flex: 1 1 200px;
max-width: 250px;
text-align: center;
padding: var(--space-lg);
}
.interest-icon {
font-size: 2rem;
display: block;
margin-bottom: var(--space-sm);
}
.interest-item h3 {
margin-bottom: var(--space-xs);
font-size: 1.1rem;
}
.interest-item p {
color: var(--color-text-light);
font-size: 0.9rem;
line-height: 1.5;
}
Key Decisions
border-radius: 50%on the profile image creates a circle crop โ a common design pattern for profile photos. Theaspect-ratio: 1 / 1ensures the image stays square regardless of the source image's dimensions.flex: 0 1 300pxon the image means: don't grow (keep it at 300px max), but shrink if needed, starting at 300px. The text side getsflex: 1 1 400pxโ it takes up the remaining space.max-width: 720pxon story paragraphs keeps lines at a comfortable reading length (roughly 65โ80 characters per line).
๐ก Optimal Line Length for Reading
Research on typography suggests that 45โ75 characters per line is the most comfortable reading width. At a 16px base font, that's roughly 600โ750px. This is why we constrain paragraph widths โ full-width text on a wide monitor is exhausting to read.
Recipe Page โ HTML
The recipe page has two jobs: show a grid of recipe cards (so visitors can browse) and present a detailed recipe with ingredients, instructions, and tips. We'll build both on the same page.
<!-- recipe.html โ content between nav and footer -->
<main>
<section class="page-header">
<div class="container">
<h1>Recipes</h1>
<p class="page-subtitle">Tried-and-true dishes from my kitchen to yours.</p>
</div>
</section>
<!-- Recipe Card Grid -->
<section class="recipe-grid-section">
<div class="container">
<div class="recipe-card-grid">
<article class="recipe-card">
<img src="images/recipe-pasta.jpg"
alt="Creamy garlic pasta in a bowl"
width="400" height="300">
<div class="recipe-card-body">
<h2>Creamy Garlic Pasta</h2>
<p class="recipe-meta">โฑ 25 min ยท ๐ฝ Serves 4 ยท Easy</p>
<p>One-pan pasta that's creamy without the cream.</p>
<a href="#recipe-detail" class="btn btn-outline">View Recipe</a>
</div>
</article>
<article class="recipe-card">
<img src="images/recipe-salad.jpg"
alt="Fresh Mediterranean salad with feta and olives"
width="400" height="300">
<div class="recipe-card-body">
<h2>Mediterranean Salad</h2>
<p class="recipe-meta">โฑ 15 min ยท ๐ฝ Serves 2 ยท Easy</p>
<p>Bright, crunchy, and ready in fifteen minutes.</p>
<a href="#" class="btn btn-outline">View Recipe</a>
</div>
</article>
<article class="recipe-card">
<img src="images/recipe-soup.jpg"
alt="A bowl of tomato basil soup with crusty bread"
width="400" height="300">
<div class="recipe-card-body">
<h2>Tomato Basil Soup</h2>
<p class="recipe-meta">โฑ 40 min ยท ๐ฝ Serves 6 ยท Medium</p>
<p>Comfort in a bowl โ perfect for rainy days.</p>
<a href="#" class="btn btn-outline">View Recipe</a>
</div>
</article>
</div>
</div>
</section>
<!-- Detailed Recipe -->
<section id="recipe-detail" class="recipe-detail">
<div class="container">
<article>
<h2>Creamy Garlic Pasta</h2>
<p class="recipe-meta">โฑ 25 min ยท ๐ฝ Serves 4 ยท Easy ยท Vegetarian</p>
<div class="recipe-layout">
<div class="recipe-image">
<img src="images/recipe-pasta.jpg"
alt="Creamy garlic pasta close-up"
width="600" height="400">
</div>
<div class="recipe-info">
<div class="ingredients">
<h3>Ingredients</h3>
<ul>
<li>350g spaghetti or linguine</li>
<li>4 cloves garlic, minced</li>
<li>2 tbsp olive oil</li>
<li>2 tbsp butter</li>
<li>1 cup reserved pasta water</li>
<li>ยพ cup grated Parmesan cheese</li>
<li>Salt and black pepper to taste</li>
<li>Fresh parsley for garnish</li>
</ul>
</div>
<div class="instructions">
<h3>Instructions</h3>
<ol>
<li>Bring a large pot of salted water to a boil. Cook pasta
according to package directions until al dente. <strong>Reserve
1 cup of pasta water</strong> before draining.</li>
<li>While pasta cooks, heat olive oil and butter in a large
skillet over medium heat. Add minced garlic and cook for
60โ90 seconds until fragrant โ don't let it brown.</li>
<li>Add drained pasta to the skillet. Toss to coat in the
garlic butter.</li>
<li>Remove from heat. Add Parmesan and ยฝ cup of the reserved
pasta water. Toss vigorously โ the starch in the water
creates the creamy sauce.</li>
<li>Add more pasta water a splash at a time until the sauce
is silky. Season with salt and pepper.</li>
<li>Serve immediately, topped with extra Parmesan and fresh
parsley.</li>
</ol>
</div>
</div>
</div>
<div class="recipe-tips">
<h3>๐ก Tips & Notes</h3>
<ul>
<li><strong>Don't skip the pasta water.</strong> The starch is
what makes the sauce creamy without cream. Reserve it
before draining!</li>
<li><strong>Low heat for the garlic.</strong> Burnt garlic is
bitter. If it starts turning dark, remove the pan from
heat and add the pasta immediately.</li>
<li><strong>Toss off the heat.</strong> Adding Parmesan over
high heat makes it clump. Remove from the burner first,
then toss.</li>
</ul>
</div>
</article>
</div>
</section>
</main>
Two Layout Patterns in One Page
This page demonstrates two distinct patterns:
(Flexbox row, wraps)"] A1["Card 1
img + body"] A2["Card 2
img + body"] A3["Card 3
img + body"] A --- A1 A --- A2 A --- A3 end subgraph "Pattern 2: Detailed Recipe" B["recipe-layout
(Flexbox row, wraps)"] B1["recipe-image
(left)"] B2["recipe-info
(right: ingredients + instructions)"] B --- B1 B --- B2 end style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style A1 fill:#f0fdf4,stroke:#22c55e,stroke-width:1px,color:#1e293b style A2 fill:#f0fdf4,stroke:#22c55e,stroke-width:1px,color:#1e293b style A3 fill:#f0fdf4,stroke:#22c55e,stroke-width:1px,color:#1e293b style B1 fill:#fdf2f8,stroke:#ec4899,stroke-width:1px,color:#1e293b style B2 fill:#fdf2f8,stroke:#ec4899,stroke-width:1px,color:#1e293b
- Recipe card grid โ similar to the homepage feature cards, but with images on top. Each card is an
<article>because it's a self-contained, linkable piece of content. - Detailed recipe โ image on the left, ingredients and instructions on the right. On mobile, everything stacks.
Recipe Page โ CSS
/* ===========================
Recipe Card Grid
=========================== */
.recipe-grid-section {
padding: var(--space-2xl) 0;
}
.recipe-card-grid {
display: flex;
gap: var(--space-xl);
flex-wrap: wrap;
justify-content: center;
}
.recipe-card {
flex: 1 1 300px;
max-width: 380px;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-lg);
overflow: hidden;
transition: box-shadow var(--transition-base),
transform var(--transition-base);
}
.recipe-card:hover {
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-3px);
}
.recipe-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.recipe-card-body {
padding: var(--space-lg);
}
.recipe-card-body h2 {
font-size: 1.25rem;
margin-bottom: var(--space-xs);
}
.recipe-meta {
color: var(--color-text-light);
font-size: 0.85rem;
margin-bottom: var(--space-sm);
}
.recipe-card-body p {
color: var(--color-text-light);
line-height: 1.5;
margin-bottom: var(--space-md);
}
/* ===========================
Detailed Recipe
=========================== */
.recipe-detail {
background-color: var(--color-bg-alt);
padding: var(--space-2xl) 0;
}
.recipe-detail h2 {
margin-bottom: var(--space-xs);
}
.recipe-layout {
display: flex;
gap: var(--space-2xl);
margin-top: var(--space-xl);
flex-wrap: wrap;
}
.recipe-image {
flex: 1 1 300px;
}
.recipe-image img {
width: 100%;
height: auto;
border-radius: var(--border-radius-lg);
object-fit: cover;
}
.recipe-info {
flex: 1 1 400px;
}
.ingredients {
margin-bottom: var(--space-xl);
}
.ingredients h3,
.instructions h3 {
margin-bottom: var(--space-md);
color: var(--color-primary);
}
.ingredients ul {
list-style: none;
padding: 0;
}
.ingredients li {
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--color-border);
line-height: 1.5;
}
.ingredients li:last-child {
border-bottom: none;
}
.instructions ol {
padding-left: var(--space-lg);
}
.instructions li {
padding: var(--space-sm) 0;
line-height: 1.7;
margin-bottom: var(--space-sm);
}
/* Recipe Tips */
.recipe-tips {
margin-top: var(--space-xl);
padding: var(--space-lg);
background-color: var(--color-bg);
border-radius: var(--border-radius-lg);
border-left: 4px solid var(--color-accent);
}
.recipe-tips h3 {
margin-bottom: var(--space-md);
}
.recipe-tips li {
margin-bottom: var(--space-sm);
line-height: 1.6;
}
Notable Techniques
overflow: hiddenon recipe cards clips the image to the card's rounded corners. Without it, the image corners poke out beyond theborder-radius.object-fit: coverwith a fixedheight: 200pxon card images ensures all cards have identical image heights, even if the source images have different aspect ratios. The image is cropped to fill the space.- Border-left accent on the tips box (
border-left: 4px solid var(--color-accent)) is a simple way to make a callout stand out without being as heavy as a full border or background change.
๐ก object-fit Cheat Sheet
coverโ fills the entire area, cropping if needed (best for cards and backgrounds)containโ fits the entire image inside the area, may leave empty spacefillโ stretches to fill (distorts the image โ usually not what you want)noneโ keeps original size, may overflow
Contact Page โ HTML
The contact page gives visitors a way to reach you. A well-built contact form demonstrates form skills from Lesson 8 and introduces new CSS patterns for styling inputs and handling validation states.
<!-- contact.html โ content between nav and footer -->
<main>
<section class="page-header">
<div class="container">
<h1>Get in Touch</h1>
<p class="page-subtitle">Questions, suggestions, or just want to say hello? I'd love to hear from you.</p>
</div>
</section>
<section class="contact-section">
<div class="container">
<div class="contact-layout">
<!-- Contact Form -->
<form class="contact-form" action="#" method="POST">
<div class="form-group">
<label for="name">Name <span class="required">*</span></label>
<input type="text" id="name" name="name"
placeholder="Your name" required>
</div>
<div class="form-group">
<label for="email">Email <span class="required">*</span></label>
<input type="email" id="email" name="email"
placeholder="your@email.com" required>
</div>
<div class="form-group">
<label for="subject">Subject</label>
<select id="subject" name="subject">
<option value="">Choose a topic...</option>
<option value="recipe">Recipe Question</option>
<option value="suggestion">Recipe Suggestion</option>
<option value="feedback">Site Feedback</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="message">Message <span class="required">*</span></label>
<textarea id="message" name="message" rows="6"
placeholder="What's on your mind?" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
<!-- Contact Info Sidebar -->
<aside class="contact-info">
<h2>Other Ways to Connect</h2>
<div class="info-card">
<h3>๐ง Email</h3>
<p><a href="mailto:hello@tastybites.com">hello@tastybites.com</a></p>
</div>
<div class="info-card">
<h3>๐ฑ Social Media</h3>
<ul class="social-links">
<li><a href="#">Instagram</a></li>
<li><a href="#">Pinterest</a></li>
<li><a href="#">YouTube</a></li>
</ul>
</div>
<div class="info-card">
<h3>โฐ Response Time</h3>
<p>I try to respond within 24โ48 hours. If it's urgent, email is fastest.</p>
</div>
</aside>
</div>
</div>
</section>
</main>
Form Accessibility Checklist
Every form element in this example follows accessibility best practices:
- Every input has a
<label>โ connected with matchingfor/idattributes. Clicking the label focuses the input. - Required fields are marked โ the red asterisk and the
requiredattribute. Visual + programmatic = accessible. type="email"โ triggers email validation and shows the correct keyboard on mobile.- Placeholder text โ provides example input. But notice it's in addition to the label, not a replacement for it. Placeholders disappear when you type, so they're not a substitute for labels.
๐ก Where Does the Form Data Go?
The action="#" means the form doesn't actually submit anywhere yet. For a static HTML site, you need a form backend service to receive submissions. Popular free options include:
- Formspree (formspree.io) โ free tier, no backend code needed
- Netlify Forms โ built in if you deploy on Netlify (which we will in Lesson 21)
- Web3Forms (web3forms.com) โ free, just add an access key
We'll connect the form to Netlify Forms in Lesson 21.
Contact Page โ CSS
/* ===========================
Contact Section
=========================== */
.contact-section {
padding: var(--space-2xl) 0;
}
.contact-layout {
display: flex;
gap: var(--space-2xl);
flex-wrap: wrap;
}
/* ===========================
Contact Form
=========================== */
.contact-form {
flex: 2 1 400px;
}
.form-group {
margin-bottom: var(--space-lg);
}
.form-group label {
display: block;
margin-bottom: var(--space-xs);
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text);
}
.required {
color: var(--color-error);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: var(--space-sm) var(--space-md);
font-size: 1rem;
font-family: inherit;
color: var(--color-text);
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
transition: border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
/* Validation states (CSS-only with :invalid and :valid) */
.form-group input:not(:placeholder-shown):invalid {
border-color: var(--color-error);
}
.form-group input:not(:placeholder-shown):valid {
border-color: var(--color-success);
}
/* ===========================
Contact Info Sidebar
=========================== */
.contact-info {
flex: 1 1 250px;
}
.contact-info h2 {
margin-bottom: var(--space-lg);
font-size: 1.25rem;
}
.info-card {
padding: var(--space-lg);
background-color: var(--color-bg-alt);
border-radius: var(--border-radius);
margin-bottom: var(--space-md);
}
.info-card h3 {
margin-bottom: var(--space-sm);
font-size: 1rem;
}
.info-card p,
.info-card a {
color: var(--color-text-light);
line-height: 1.5;
}
.info-card a:hover {
color: var(--color-primary);
}
.social-links {
list-style: none;
padding: 0;
margin: 0;
}
.social-links li {
margin-bottom: var(--space-xs);
}
.social-links a {
color: var(--color-primary);
text-decoration: none;
font-weight: 500;
}
.social-links a:hover {
text-decoration: underline;
}
The Focus Ring Technique
When a user clicks or tabs into an input field, the browser shows a focus indicator. The default one is often ugly or hard to see. Our approach:
- Remove the default outline with
outline: none - Replace it with a
border-colorchange and a softbox-shadowglow
/* The glow effect */
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
The box-shadow with zero offset and blur but a spread of 3px creates a ring around the input. Using rgba() with 15% opacity makes it subtle โ a soft blue halo that says "you're here" without shouting.
CSS-Only Validation Feedback
The selector input:not(:placeholder-shown):invalid targets inputs that are both invalid and have user-typed content. This prevents red borders on empty required fields before the user has done anything โ a much better experience than showing errors immediately.
(placeholder shown)"] -->|"No border change"| B["Default gray border"] A -->|"User types"| C["Placeholder hidden"] C -->|":invalid"| D["Red border
(error)"] C -->|":valid"| E["Green border
(success)"] style A fill:#f1f5f9,stroke:#64748b,stroke-width:1px,color:#1e293b style B fill:#f1f5f9,stroke:#64748b,stroke-width:1px,color:#1e293b style D fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style E fill:#f0fdf4,stroke:#22c55e,stroke-width:2px,color:#1e293b
Highlighting the Active Page
Each page should show visitors where they are by highlighting the current page in the nav. In Lesson 17, the homepage nav had class="nav-link active" on the Home link. On each new page, move that active class to the correct link.
<!-- index.html -->
<li><a href="index.html" class="nav-link active">Home</a></li>
<li><a href="about.html" class="nav-link">About</a></li>
<!-- about.html -->
<li><a href="index.html" class="nav-link">Home</a></li>
<li><a href="about.html" class="nav-link active">About</a></li>
<!-- recipe.html -->
<li><a href="index.html" class="nav-link">Home</a></li>
<li><a href="recipe.html" class="nav-link active">Recipes</a></li>
<!-- contact.html -->
<li><a href="index.html" class="nav-link">Home</a></li>
<li><a href="contact.html" class="nav-link active">Contact</a></li>
The CSS from Lesson 17 already handles the styling:
.nav-link.active {
color: var(--color-primary);
font-weight: 600;
}
This is a small detail that makes a big difference in usability. Without it, visitors have to read the page title or URL to know where they are.
๐ก Accessibility Bonus: aria-current
For even better accessibility, add aria-current="page" to the active link. Screen readers will announce "Home, current page" instead of just "Home":
<a href="index.html" class="nav-link active"
aria-current="page">Home</a>
Hands-on Exercise
๐๏ธ Exercise: Build All Three Content Pages
Objective: Create complete About, Recipe, and Contact pages that share nav/footer with the homepage.
Requirements Checklist
- All three pages have the same nav and footer as the homepage
- The
activeclass is on the correct nav link for each page - Each
<title>is unique (e.g., "About - Tasty Bites") - About page โ profile image + text, story section, interests grid
- Recipe page โ recipe card grid + one detailed recipe
- Contact page โ styled form with labels, required fields, and focus states
- All pages responsive โ test at 1200px, 768px, and 375px
- All links work โ click every nav link on every page to verify navigation
Stretch Goals (Optional)
- Add a timeline to the About page (vertical line with milestones)
- Add a recipe filter bar above the cards (just the UI โ buttons for categories)
- Add character count text below the message textarea (CSS-only using
counter(), or simple JS) - Add a "Back to top" link at the bottom of each page
๐ก Hint โ Back to Top Link
<!-- At the top of your page (inside <body>) -->
<div id="top"></div>
<!-- Before the footer -->
<div class="back-to-top">
<a href="#top">โ Back to Top</a>
</div>
.back-to-top {
text-align: center;
padding: var(--space-xl) 0;
}
/* Smooth scrolling for the whole page */
html {
scroll-behavior: smooth;
}
โ Full Solution: Complete about.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - Tasty Bites</title>
<link rel="stylesheet" href="css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet">
</head>
<body>
<header class="site-header">
<nav class="navbar">
<div class="nav-container">
<a href="index.html" class="nav-logo">๐ด Tasty Bites</a>
<button class="nav-toggle" aria-label="Toggle navigation"
aria-expanded="false">โฐ</button>
<ul class="nav-menu">
<li><a href="index.html" class="nav-link">Home</a></li>
<li><a href="about.html" class="nav-link active" aria-current="page">About</a></li>
<li><a href="recipe.html" class="nav-link">Recipes</a></li>
<li><a href="contact.html" class="nav-link">Contact</a></li>
</ul>
</div>
</nav>
</header>
<main>
<section class="page-header">
<div class="container">
<h1>About Me</h1>
<p class="page-subtitle">The person behind the recipes.</p>
</div>
</section>
<section class="profile">
<div class="container">
<div class="profile-content">
<div class="profile-image">
<img src="images/about-photo.jpg"
alt="A smiling person in a kitchen"
width="400" height="400">
</div>
<div class="profile-text">
<h2>Hi, I'm Alex!</h2>
<p>I'm a home cook who believes that great food doesn't
need to be complicated. What started as a quarantine
hobby turned into a genuine passion โ and this site
is where I share what I've learned along the way.</p>
<p>My cooking philosophy is simple: use fresh ingredients,
don't skip the garlic, and always taste as you go.</p>
</div>
</div>
</div>
</section>
<section class="about-story">
<div class="container">
<h2>My Cooking Journey</h2>
<p>I grew up watching my grandmother cook โ no recipes,
no measuring cups, just instinct and decades of practice.
Three years and a lot of burnt garlic later, I can
confidently say I make a mean pasta and the best banana
bread my friends have ever tasted.</p>
</div>
</section>
<section class="about-interests">
<div class="container">
<h2>When I'm Not Cooking</h2>
<div class="interests-grid">
<div class="interest-item">
<span class="interest-icon">๐</span>
<h3>Reading</h3>
<p>Mostly cookbooks, but also sci-fi and history.</p>
</div>
<div class="interest-item">
<span class="interest-icon">๐ฑ</span>
<h3>Gardening</h3>
<p>Growing herbs on my balcony.</p>
</div>
<div class="interest-item">
<span class="interest-icon">๐ต</span>
<h3>Music</h3>
<p>Cooking with a good playlist is non-negotiable.</p>
</div>
<div class="interest-item">
<span class="interest-icon">โ๏ธ</span>
<h3>Travel</h3>
<p>Finding new flavors in every city.</p>
</div>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container">
<div class="footer-content">
<div class="footer-info">
<p class="footer-logo">๐ด Tasty Bites</p>
<p>Simple recipes, made with love.</p>
</div>
<nav class="footer-nav" aria-label="Footer navigation">
<h4>Pages</h4>
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="recipe.html">Recipes</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</nav>
</div>
<div class="footer-bottom">
<p>© 2026 Tasty Bites. All rights reserved.</p>
</div>
</div>
</footer>
<script src="js/script.js"></script>
</body>
</html>
๐ฏ Quick Quiz
Question 1: Why do we copy-paste the nav and footer into every page instead of using a template?
Question 2: What does object-fit: cover do on recipe card images?
Question 3: Why does input:not(:placeholder-shown):invalid work better than just input:invalid?
Question 4: What's the optimal line length for comfortable reading?
Question 5: Why do we use <article> for recipe cards?
Summary
๐ Key Takeaways
- Reuse nav and footer by copying from the homepage โ update the
activeclass and<title>for each page - The page header pattern (
.page-header) replaces the hero section on content pages โ simpler, consistent - Profile layouts use Flexbox with
border-radius: 50%for circular images andaspect-ratio: 1 / 1to keep them square - Recipe cards use
overflow: hiddento clip images to rounded corners andobject-fit: coverfor consistent image heights - Styled forms need explicit labels (not just placeholders), focus rings (
box-shadowglow), and smart validation (:not(:placeholder-shown):invalid) - Every input needs a label connected with matching
for/idattributes - Use
aria-current="page"on the active nav link for screen reader accessibility - Constrain reading width to 45โ75 characters per line for comfortable reading
- Form submissions on static sites need a backend service โ Formspree, Netlify Forms, or Web3Forms
๐ Your Project After This Lesson
my-website/
โโโ css/
โ โโโ style.css โ + page-header, profile, recipe, contact, form styles
โโโ images/
โ โโโ about-photo.jpg
โ โโโ recipe-pasta.jpg
โ โโโ recipe-salad.jpg
โ โโโ recipe-soup.jpg
โโโ js/
โ โโโ script.js โ mobile menu toggle (works on all pages)
โโโ index.html โ โ
Homepage
โโโ about.html โ โ
About page (NEW)
โโโ recipe.html โ โ
Recipe page (NEW)
โโโ contact.html โ โ
Contact page (NEW)
All four pages share:
โ
Same navigation bar (with correct active link)
โ
Same footer
โ
Same stylesheet
โ
Same mobile menu script
โ
Responsive at all widths
๐ What's Next?
Your site is functional โ four complete, responsive pages with working navigation. But there's a difference between functional and polished. In the next lesson โ Lesson 19: Adding Polish โ you'll add the finishing touches: smooth transitions, scroll animations, a dark mode toggle, custom scrollbar styling, loading states, and all the small details that make a site feel professional.