Project: A Multi Page Business Website
Six pages sharing a layout, with consistent navigation, breadcrumbs, a sitemap and metadata that differs correctly on every page.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
The brief
Build a six page site for a small business. The challenge is not any single page - it is keeping structure, navigation and metadata consistent across all of them while making each page genuinely distinct.
The pages
/ home
/services what the business does
/services/repairs one service in detail
/about the business and its people
/contact address, map, form
/404 not foundThe shared layout
Everything except the main content is identical on every page. In a static site that means partials; on a server it means a template.
<!DOCTYPE html>
<html lang="en-IN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- these four differ on every page -->
<title>{{ page_title }} - Riverside Repairs</title>
<meta name="description" content="{{ page_description }}">
<link rel="canonical" href="https://example.com{{ page_path }}">
<meta property="og:title" content="{{ page_title }}">
<meta property="og:description" content="{{ page_description }}">
<meta property="og:url" content="https://example.com{{ page_path }}">
<meta property="og:image" content="https://example.com/images/og-default.jpg">
<meta property="og:type" content="website">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/styles/site.css">
<script src="/scripts/site.js" defer></script>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header class="site-header">
<a href="/" class="logo">
<img src="/logo.svg" alt="Riverside Repairs home" width="160" height="40">
</a>
<button type="button" class="menu-toggle"
aria-expanded="false" aria-controls="site-menu">
<span class="hamburger" aria-hidden="true"></span>
Menu
</button>
<nav id="site-menu" aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
{{ breadcrumb }}
<main id="main-content" tabindex="-1">
{{ content }}
</main>
<footer class="site-footer">
<nav aria-label="Footer">
<ul>
<li><a href="/privacy">Privacy</a></li>
<li><a href="/sitemap">Sitemap</a></li>
</ul>
</nav>
<address>
Riverside Repairs, 14 Sadar Bazaar Road, Pune 411001<br>
<a href="tel:+912026001234">+91 20 2600 1234</a>
</address>
<p><small>Copyright 2026 Riverside Repairs</small></p>
</footer>
</body>
</html>Marking the current page
<li><a href="/services" aria-current="page">Services</a></li>.site-header nav a[aria-current="page"] {
font-weight: 600;
border-bottom: 2px solid currentColor;
}One attribute, announced as current page and styled without an extra class. Whatever generates the pages should set it automatically from the current path.
The mobile menu
const toggle = document.querySelector(".menu-toggle");
const menu = document.getElementById("site-menu");
toggle.addEventListener("click", () => {
const open = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!open));
menu.hidden = open;
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && toggle.getAttribute("aria-expanded") === "true") {
toggle.setAttribute("aria-expanded", "false");
menu.hidden = true;
toggle.focus();
}
});/* the menu is always visible above the breakpoint */
@media (min-width: 48rem) {
.menu-toggle { display: none; }
#site-menu[hidden] { display: block; }
}That last rule matters. The hidden attribute is what removes the links from the tab order on mobile; on desktop the CSS overrides it so the menu shows regardless of the attribute state.
Metadata per page
| Page | Title | h1 |
|---|---|---|
| / | Riverside Repairs - Appliance Repair in Pune | Appliance repair in Pune |
| /services | Services - Riverside Repairs | What we repair |
| /services/repairs | Kettle and Small Appliance Repair - Riverside Repairs | Kettle and small appliance repair |
| /about | About Us - Riverside Repairs | About Riverside Repairs |
| /contact | Contact - Riverside Repairs | Contact us |
Note that the title and the h1 are related but not identical. The title carries the site name for a search result; the h1 does not need to, because the header already shows it.
The contact page
<h1>Contact us</h1>
<section>
<h2>Where we are</h2>
<address>
14 Sadar Bazaar Road<br>
Pune 411001<br>
Maharashtra
</address>
<iframe src="https://www.openstreetmap.org/export/embed.html?bbox=73.85,18.51,73.87,18.53"
title="Map showing the workshop on Sadar Bazaar Road"
width="100%" height="360" loading="lazy" style="border:0"></iframe>
<p><a href="https://www.openstreetmap.org/?mlat=18.52&mlon=73.86">Open a larger map</a></p>
</section>
<section>
<h2>Opening hours</h2>
<table>
<caption>Workshop opening hours</caption>
<tbody>
<tr><th scope="row">Monday to Friday</th><td>09:00 to 18:00</td></tr>
<tr><th scope="row">Saturday</th><td>09:00 to 13:00</td></tr>
<tr><th scope="row">Sunday</th><td>Closed</td></tr>
</tbody>
</table>
</section>The address is written in text as well as shown on a map. A map in a frame is close to unusable with a keyboard and impossible to read aloud.
LocalBusiness structured data
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Riverside Repairs",
"url": "https://example.com",
"telephone": "+91-20-2600-1234",
"address": {
"@type": "PostalAddress",
"streetAddress": "14 Sadar Bazaar Road",
"addressLocality": "Pune",
"postalCode": "411001",
"addressCountry": "IN"
},
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "09:00",
"closes": "18:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday",
"opens": "09:00",
"closes": "13:00"
}
]
}
</script>The 404 page
<main id="main-content" tabindex="-1">
<h1>That page is not here</h1>
<p>It may have moved, or the address may have a typo in it.</p>
<h2>Try one of these</h2>
<ul>
<li><a href="/services">What we repair</a></li>
<li><a href="/contact">Contact us</a></li>
</ul>
<search>
<form action="/search" method="get">
<label for="q">Search the site</label>
<input type="search" id="q" name="q">
<button type="submit">Search</button>
</form>
</search>
</main>The page must return a real 404 status, not a 200. A soft 404 - an error page returning success - leaves search engines indexing pages that do not exist.
robots.txt and sitemap
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc><lastmod>2026-08-11</lastmod></url>
<url><loc>https://example.com/services</loc></url>
<url><loc>https://example.com/services/repairs</loc></url>
<url><loc>https://example.com/about</loc></url>
<url><loc>https://example.com/contact</loc></url>
</urlset>Checking your work
- Validate every page.
- Confirm every title, description and canonical is unique and correct.
- Tab through each page; the skip link should be first and the menu operable.
- Open the mobile menu with a keyboard and close it with Escape.
- Request a nonexistent URL and confirm the status is 404, not 200.
- Check every internal link with a link checker.
- Test the structured data.
- Confirm no page is more than two clicks from the home page.
Extensions
- Add a news section with an index and article pages.
- Add a language version with reciprocal
hreflang. - Add a print stylesheet for the contact page.
- Add a service worker for basic offline support.
Practice
- Build all six pages from one shared layout.
- Generate the sitemap from the same page list the navigation uses.
- Set
aria-currentautomatically from the current path. - Deploy and verify the production
robots.txtand 404 status.