Your Responsive Personal Site
The idea
Every earlier lesson this week taught you one tool at a time — a landmark element here, a flex layout there. This project is where they all go into the same file at once: a small personal site with a real structure, a layout that survives a phone screen, a form someone can actually fill out, and a public address anyone can visit. Nothing here is new — you're combining things you've already used.
Work through the examples below on a throwaway page first (this lesson builds a one-page bakery site as its running example), then apply the same moves to your own site in the project parts.
Semantic structure: rooms with real labels
Picture a house where every room is labeled: a sign over the porch, a directory just inside the front door, clearly marked rooms you actually visit, and a mailbox out back for anything administrative. A visitor — or someone reading the house by touch, in the dark — can find their way around without ever stepping into a room and guessing what it's for. A page built entirely from unlabeled <div>s is a house with every door left blank.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Corner Bakery</title>
</head>
<body>
<header>
<h1>Corner Bakery</h1>
<nav>
<a href="#menu">Menu</a>
<a href="#hours">Hours</a>
</nav>
</header>
<main>
<section id="menu">
<h2>Today's menu</h2>
<p>Sourdough, cinnamon rolls, and a rotating soup.</p>
</section>
<section id="hours">
<h2>Hours</h2>
<p>Tuesday through Sunday, 7am to 2pm.</p>
</section>
</main>
<footer>
<p>© 2026 Corner Bakery.</p>
</footer>
</body>
</html>
<header> is the porch sign — the page's own title and its entry points. <nav> is the directory just inside the door: a set of links to the page's own sections. <main> is the house's actual rooms — the content a visitor came for, and a page should have exactly one. Each <section> is one labeled room inside it, with its own heading (<h2>) so a screen reader can announce "Today's menu" before reading what's in it. <footer> is the mailbox: copyright, contact details, anything administrative that isn't the point of the visit. Open this file in a browser and nothing looks different from a page built out of <div>s — the difference only shows up in the Elements panel's accessibility tree, or to someone using a screen reader, where each region now has a name instead of being an anonymous box.
A layout that bends instead of breaks
You've already built the pieces: Flexbox's serving tray kept a row of items lined up, Grid's chessboard arranged cards into columns, and a mobile-first media query let you write the narrow-screen layout first and only add what changes for a wider one. This project is where those three habits run together on one real page instead of three separate examples.
nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
main {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
@media (min-width: 40rem) {
nav {
flex-direction: row;
gap: 1.5rem;
}
main {
grid-template-columns: repeat(2, 1fr);
}
}
By default — on a phone-width screen — the nav links stack in a column and the bakery's sections sit in a single column, one below the other. Once the screen reaches 40rem (640px at the default root size), the media query switches the nav to a row and the main content to two columns side by side. Nothing about the narrow layout is a special case bolted on afterward; it's just the plain rule that's always there, with the query adding only what changes for more room. Resize the browser window across that 40rem line and watch the nav and the grid both flip at once, from the same breakpoint.
A contact form everyone can use
Think of filling out a form at a government office window: every blank has its own printed label fixed right next to it, so you know what goes there without guessing from a faint gray hint that vanishes the moment you start typing. A sighted mouse user can often get away with a vague placeholder; someone using a screen reader or switching fields with Tab cannot — the label is the only thing announced.
<form>
<div>
<label for="bakery-email">Email for weekly specials</label>
<input id="bakery-email" name="email" type="email" required>
</div>
<div>
<label for="bakery-note">Anything you're allergic to?</label>
<textarea id="bakery-note" name="note"></textarea>
</div>
<button type="submit">Sign up</button>
</form>
input:focus-visible,
textarea:focus-visible,
button:focus-visible {
outline: 2px solid #7a4a00;
outline-offset: 2px;
}
<label for="bakery-email"> is wired to <input id="bakery-email"> by that matching id — click the label and focus jumps to the field, and a screen reader reads "Email for weekly specials" the instant the field gets focus. type="email" tells the browser (and anyone's built-in validation) what shape of answer is expected, and required marks a field that can't be left empty, which browsers already flag with built-in validation from an earlier lesson. The :focus-visible rule gives every one of these a visible outline when reached by keyboard, so Tabbing through the form never loses track of where you are. None of this changes how the form looks to a mouse user glancing at it — it only matters the moment someone isn't using a mouse.
Shipping a change the team way: branches and pull requests
You already know Git as a save-game system — commits are named checkpoints, and a branch is a parallel save slot you can experiment on without touching the one everyone else is playing. A pull request is the step between finishing work on a branch and folding it back into that shared save: it's a formal "here's what I changed, please look before it becomes everyone's problem."
git checkout -b add-hours-section
git add index.html
git commit -m "Add hours section to bakery page"
git push -u origin add-hours-section
git checkout -b add-hours-section opens a new branch — a fresh save slot copied from wherever you were standing — and switches onto it in one step. git add and git commit save a checkpoint on that branch alone, exactly as before. git push -u origin add-hours-section sends that branch to the remote (the cloud backup copy) and remembers the link between your local branch and that remote one, so a plain git push works next time without repeating the branch name. From here, opening a pull request happens on GitHub itself: visit the repository, and GitHub offers a "Compare & pull request" button for any branch you just pushed that isn't the default branch. Fill in a short description of what changed, open the pull request, and — working solo — you can merge it yourself once you're happy with the diff shown on that page. The point isn't ceremony: a pull request is a single page showing exactly what changed, which is worth having even when you're the only one reviewing it.
Putting the site on the internet
A finished draft sitting only on your own computer is a newsletter that never left your desk — real until someone else can read it. Deploying is putting a copy on a public stand with a fixed address, so anyone can pick it up anytime, even while your own computer is off.
GitHub Pages serves files straight out of a GitHub repository. In the repository's Settings tab, under Pages, choose the branch to publish (commonly main) and the folder (/ for the repository root, if index.html sits there). Save, and GitHub builds a URL shaped like https://your-username.github.io/your-repo-name/ within a minute or two. Push a new commit to that branch later, and the live site updates automatically — there's no separate "upload" step to remember.
Netlify instead watches a repository you connect from its dashboard: pick "Add new site" → "Import an existing project," authorize GitHub, and choose the repo. Since this site has no build step, leave the build command blank and set the publish directory to wherever index.html lives (the repository root, for this project). Netlify assigns a random subdomain like https://cheerful-site-1234.netlify.app immediately, and — same as GitHub Pages — every push to the connected branch triggers a fresh deploy automatically.
Either way, the moment the first deploy finishes, open the URL in a private/incognito browser window — one where you're not logged into anything — to see the page exactly as a stranger would, with none of your own browser history or extensions in the way.
Common mistakes
- Reaching for a
<div>out of habit where a landmark element already says what the region is for. - Writing the wide-screen layout first and bolting a narrow-screen fallback on afterward, instead of starting narrow and adding only what changes.
- A placeholder standing in for a label — it disappears the moment someone starts typing and is never announced by a screen reader the same way a real
<label>is. - Committing straight to the default branch out of convenience, skipping the pull request that would've shown the whole diff in one place.
- Deploying from the wrong folder or branch, so the live URL serves a 404 or an old version instead of what was just pushed.
Try it
- Build the bakery page's
<header>/<nav>/<main>/<footer>skeleton above, then open the browser's Elements panel and check the accessibility tree — each landmark should show up by name. - Add the nav/grid CSS above, then resize the browser window slowly across the
40rembreakpoint and watch both the nav and the main content switch layout at the same point. - Build the signup form above, then Tab through it with your mouse untouched — confirm you can always see which field has focus, and that the label text is read before each field if you turn on a screen reader.
- Make a small edit on a new branch, push it, and open a pull request on GitHub — read your own diff on that page before merging it.
- Deploy any throwaway one-page site to GitHub Pages or Netlify, then open the live URL in a private browser window to confirm it's really public.