Capstone · Software route
Capstone: Build It for the Web
Learn to build and host a real site, then use it to ship the thing that's actually yours
- Grades 7 to 12
- Time 4 taught periods, then open build weeks
- Difficulty Starts guided, ends up to you
Everybody in this class does a capstone. This page is for the ones who want theirs to run in a browser: a game, an app, a tool, a site. You do not need to have built a full website before. The first four class periods teach you how, using your own idea from day one. After that, nobody writes the steps: you build it out, cut scope when you need to, and ship it at a real address.
Is this your capstone?
Your capstone can go several directions. Robotics and competition. Product and fabrication, on the laser cutter or the printers. Media and documentation. Or this one, software, where the thing you build runs in a browser.
Pick this one if you liked restyling your dashboard more than you expected to, or you already have something in your head that you wish existed and it is a game, a tool, a page or an app.
Do not pick this one because it sounds easier. It is not. Ask anybody who has tried to make Chess work.
You are not locked in until your proposal is approved, so if you are torn, talk to Mr. Krigger before you commit.
What you'll learn
- How HTML, CSS and JavaScript split the work across three files
- How to build a website from nothing, with no template to fill in
- How to move a project between computers without breaking it, and serve it from your Pi
- How to take an idea from a sentence to a working thing, which is harder than any single piece of code in it
- How to cut scope, the skill that decides whether a project ships or dies
- How to teach yourself something specific from a tutorial without ending up with code you cannot read
- How to keep a project alive over several weeks instead of one afternoon
What you need
- A Mac or Windows computer to build on. A lab machine or your own laptop
- A jump drive
- Your Raspberry Pi, still working from the dashboard assignment
- A phone or second device to test on
- An approved capstone proposal saying this is what you are doing
- No accounts. No downloads. Nothing to sign up for
The AI rule
Same as every assignment in this class, and it matters more here because the project is bigger.
Build your first working version without AI. No ChatGPT, no Claude, no Gemini, no Copilot writing your code. Watching somebody else solve a problem does not teach you to solve it. The fifteen minutes you spend staring at a broken layout before you work out why is where the learning actually happens.
Once your first working version exists and Mr. Krigger has seen it, you can use AI to push further: better CSS, animations, a feature you could not figure out. Two conditions. You have to say what you used it for, and you have to be able to explain every line of it on demand. If you cannot walk through a section of your own code, that section does not count toward your grade.
One more thing worth saying plainly: a large share of the capstone rubric is explaining your work, and another chunk is creativity and ambition. Neither of those can be generated. A project that was mostly typed by a machine scores badly before anybody even looks at whether it runs.
What counts
Anything that runs in a browser and does something.
Something for an audience. A page that runs at a booth or an event: spin-the-wheel, a trivia round, a live high score board, a costume vote, a photo wall.
Something the lab or the school actually needs. A printer queue for the lab. A tool checkout board. A RoboMaster battery tracker so nobody grabs a dead one again. A bell schedule that is readable from across the room.
Something about here. A hurricane prep checklist your family would really use. A carnival countdown. A page for a local business you like that does not have one. Tide and beach conditions. A food map of St. Croix.
What counts, continued
Games. Students in this class have built Chess, Tetris, Snake, Flappy Bird and Candy Crush. Go look at the projects page. Those were built by people sitting where you are sitting.
Sites with something behind them. A page that talks to your Pi. A scoreboard that survives a refresh. A page that pulls live data from somewhere.
Two routes, both legitimate
From scratch. You know roughly how to build it and you want to work it out yourself. Slower at the start, and you will understand every line.
Following a tutorial. You want to build Chess, so you find a Chess tutorial and follow it. This is a real way to learn and professionals do it constantly.
If you take the tutorial route, two conditions.
- Finish somewhere the tutorial did not. Add a feature, change how it works, restyle it completely, make it do something the video never mentioned. A capstone that ends exactly where a YouTube video ends is a transcription, not a project.
- You can explain what you typed. Mr. Krigger will point at parts of your code and ask what they do, in the Week 4 walkthrough.
Tell Mr. Krigger which tutorial you are following before you start, so he can tell you if it is teaching you something from 2013.
Week 1
Step 1: Sketch it
Close the laptop. On paper, write:
- One sentence: what is this and who is it for?
- What are the three or four main chunks of the page, top to bottom?
- What is the one thing on it that moves, changes, or responds when you click?
Show Mr. Krigger your sketch before you type anything. This takes five minutes and saves you two periods.
Step 2: Name your vertical slice and your fallback
Name your vertical slice: one thin piece of the finished thing, working end to end. Not the easy part, the whole path, made narrow: something on screen, something you can do to it, something that happens back.
| Project | The slice | Not the slice |
|---|---|---|
| Chess | A board that draws, and one pawn you can move one square | All the pieces with legal move rules |
| A study timer | A countdown that starts, runs and stops | Presets, history, sound, settings |
| A printer queue | A list you can add one job to | Login, editing, deleting, dates |
| A trivia game | One question, one answer, one right or wrong | Categories, scores, a leaderboard |
Then write down your fallback: the version of your project you would be satisfied with if everything took twice as long as you think it will. You are writing this now, while you are calm, because the day you need it you will not want to.
Step 3: Name it after what it is
Make your folder on the Desktop, and name it after your project. Not mysite, not project, not website.
| If you are building | Call the folder |
|---|---|
| Chess | chess |
| A tide and conditions page | tide-check |
| A printer queue | print-queue |
Lowercase, hyphens instead of spaces. Inside it, make three empty files and one folder:
chess/
├── index.html ← the structure. What is on the page
├── styles.css ← the look. Colors, fonts, spacing
├── script.js ← the behavior. What happens when you click
└── assets/ ← images and anything else you use
Two rules about filenames, and they matter more than they look. All lowercase, no spaces: beach-photo.jpg, never Beach Photo.jpg. Your Pi runs Linux, and Linux thinks Photo.jpg and photo.jpg are two different files. And everything you use lives inside your project folder: a file pulled straight from your Desktop will be found on this computer and nowhere else.
Step 4: Open it in VS Code
File → Open Folder, pick your project folder, and the whole thing appears in the sidebar.
Turn on two things:
- Word wrap: Alt+Z on Windows, Option+Z on Mac. Long lines stop running off the screen.
- Auto save: File → Auto Save. Now you never refresh the browser and wonder why nothing changed.
Step 5: Write the skeleton
Put this in index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Change This Title</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>Change this to what your project is</h1>
<p>One sentence saying what it does.</p>
</header>
<main>
<section>
<h2>Section one</h2>
<p>Replace all of this.</p>
</section>
</main>
<footer>
<p>Built by YOUR NAME, served from a Raspberry Pi.</p>
</footer>
<script src="script.js"></script>
</body>
</html>
Keep the <head> exactly as it is. Those five lines set the character encoding, make the page scale correctly on phones, and connect your CSS file.
Put this in styles.css to prove the connection works:
body {
background: #111;
color: #eee;
font-family: system-ui, sans-serif;
}
Double-click index.html. It opens in your browser straight off the disk, no server needed yet.
index.html.src pointing at the Desktop: works on this computer, breaks on the Pi.Step 6: Fill in your sections
Build out the chunks from your sketch. The tags that do most of the work:
| Tag | What it does |
|---|---|
<h1> to <h6> | Headings, h1 is biggest. One h1 per page |
<p> | A paragraph |
<img> | An image. The alt text is what a screen reader says |
<a> | A link |
<ul> with <li> | A bullet list |
<div> | A box you can style. The class is the name you use in CSS |
<button> | Something to click |
<section> | A chunk of the page |
Day 2
Step 1: Pick your colors and font on purpose
Generate a palette at coolors.co and pick a font at fonts.google.com. Then put your colors at the top of styles.css with names, so you can change your whole site in one place:
:root {
--bg: #0f1020;
--text: #f2f2f2;
--accent: #ff5d73;
}
body {
margin: 0;
padding: 24px;
background: var(--bg);
color: var(--text);
font-family: system-ui, sans-serif;
line-height: 1.6;
}
h1 { color: var(--accent); }
--bg is a variable. var(--bg) means "whatever that is set to." Change it once at the top and every place that uses it changes with it.
Step 2: Space things out
Beginner sites almost always look cramped for the same three reasons. Fix all three:
body { line-height: 1.6; } /* space between lines of text */
section { margin-bottom: 48px; } /* space between chunks */
main {
max-width: 800px; /* stop text running the full screen width */
margin: 0 auto; /* and center it */
}
That last one does more for how a page reads than any color choice. Text running 1400 pixels wide is exhausting and nobody can say why. max-width stops it, and margin: 0 auto centers it.
Step 3: Make it work on a phone
Squeeze your browser window as narrow as it goes. Whatever breaks, fix now. The usual culprit:
img { max-width: 100%; height: auto; } /* images that stay inside the screen */
If you have a row of boxes, this one line handles every screen size:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
"As many columns as fit, each at least 220px wide." Three columns on a laptop, one on a phone, and you did not write a rule for either.
Day 3
Step 1: Wire up a button
In index.html:
<button id="go">Click me</button>
<p id="output">Nothing yet.</p>
In script.js:
const button = document.getElementById("go");
const output = document.getElementById("output");
button.addEventListener("click", function () {
output.textContent = "You clicked it.";
});
Three lines, three ideas. getElementById finds a thing on the page by its id. addEventListener("click", ...) says "when this gets clicked, run this." .textContent = ... changes what it says. Almost everything interactive on the web is some version of those three.
Step 2: Type into the console
The console is not just for when something breaks. It is how you find out what broke and where. Open it now: F12 (or right-click the page → Inspect → Console tab).
Type this directly into the console and press Enter:
document.getElementById("go")
It prints the button element back to you. The console is a live line into your page. You can ask it questions any time, not only when something crashes.
Now break something on purpose. In script.js, misspell the id: change "go" to "gox", and click the button. The console shows something like:
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
at script.js:4
Step 3: Read the error
Read it in this order:
script.js:4— which file, which line. Click it and the browser jumps you straight there.TypeError— the kind of mistake. This one means "I tried to use something that is not the type I expected."- Cannot read properties of null —
getElementByIdcame back empty-handed. No element had that id, so it handed backnull, andnullhas no.addEventListenerto call.
Wrong id → null → crash on the next line. That chain is the single most common bug in this unit. Once you can read it, you fix it in ten seconds instead of guessing for ten minutes.
Step 4: What the other errors mean
| You'll see | It usually means |
|---|---|
ReferenceError: x is not defined | You used a variable before creating it, or typo'd its name |
Uncaught SyntaxError: ... | A typo in the code itself: a missing } or ), usually just above the line it names |
Cannot read properties of undefined | Same idea as null, one step removed: you are reaching into something that does not exist yet |
| Yellow text, not red | A warning. The page still runs, but something is off |
| Plain black text, no file:line | Your own console.log(...) output, not an error |
Fix your id back to "go" before you move on.
Step 5: Build your actual slice
Now build the interactive piece of your actual slice, the one you named on Day 1. Some shapes and what to search for:
| Idea | The trick to search for |
|---|---|
| A piece that moves on click or keypress | javascript move element on click |
| Random pick (quote, fact, dare, name) | javascript random array item |
| Countdown to a date | javascript countdown timer |
| Score or tally that goes up | javascript increment variable on click |
| Filter a list of cards | javascript filter list buttons |
| Quiz with a score at the end | javascript quiz score |
Break your own slice before it goes further: click everything twice fast, click things out of order, make the window tiny, open it on another computer, hand it to the person next to you and say nothing. Fix what they find.
Day 4
Step 1: Copy it over
Jump drive into your Pi, drag the whole folder into your YOURNAME folder (the one you made in the dashboard lesson), or from the Terminal:
cp -r /media/$USER/YOUR-DRIVE-NAME/chess ~/YOURNAME/chess
Check it arrived:
ls ~/YOURNAME/chess
ls lists index.html, styles.css, script.js and assets. If the folder is missing or empty, the drag did not finish. Do it again and wait for it.Step 2: Write the server
nano ~/YOURNAME/site_server.py
from flask import Flask, send_from_directory
import os
SITE = os.path.expanduser("~/YOURNAME/chess")
app = Flask(__name__)
@app.route("/")
def home():
return send_from_directory(SITE, "index.html")
@app.route("/<path:filename>")
def other_files(filename):
return send_from_directory(SITE, filename)
app.run(host="0.0.0.0", port=8000)
On the SITE line, change YOURNAME to your folder name and chess to your project's folder name. Everything else is the same for everybody. Save: Ctrl+O, Enter. Exit: Ctrl+X.
site_server.py with File, Open, click the green Run button, and use the red Stop button to stop it. Set Thonny's Interpreter to your pihealth toolbox first. The steps are on Day 1 of the dashboard lesson.Steps 3 and 4: Run it, then hand somebody the address
source ~/YOURNAME/pihealth/bin/activate
python3 ~/YOURNAME/site_server.py
Check http://localhost:8000 on the Pi first, then get your Pi's address:
hostname -I
Give someone http://YOUR-PI-IP:8000 and watch them load it on their computer. A phone works if you are testing at home.
Check on that other computer, not on the Pi:
- Every image shows up
- Every link goes somewhere
- Your slice still works
- Nothing runs off the side of the screen
Why images break on the Pi and nowhere else
Your Mac or Windows computer is case-insensitive. It treats Sunset.JPG and sunset.jpg as the same file, so a typo in your src never shows up.
Linux is case-sensitive. To your Pi, those are two unrelated files, and one of them does not exist. Your site worked for three days and broke the minute it arrived, and you did nothing wrong. Every web developer gets bitten by this exactly once. Now you have had your once.
The fix and the habit: all lowercase, hyphens instead of spaces.
Weeks 2 and 3
Build it out
No steps here. It is your project and nobody knows what it needs except you. What follows is how to work, not what to work on.
Add one thing at a time, and keep it running. Working, change one thing, working, change one thing. If you change five things and it breaks you have five suspects. Keep a copy of the last version that worked, on your jump drive, every single period.
Read the console every time something is wrong. F12, Console tab. It tells you what went wrong and roughly where. Guessing is slower than reading.
Build it out, continued
Get somebody to use it every week. Not at the end. Hand them the laptop and say nothing, and write down where they get stuck. You cannot see your own project the way a stranger does, and by week three you will not even remember trying.
Check phone width every period. Drag the window narrow before you leave. A layout that collapses at 390px is cheap to fix today and expensive to fix the week of the showcase.
Keep a short log. Two lines a period: what you did, what broke. When you write the explanation for the capstone, this log is where that writing comes from.
If you took the tutorial route: by now you should be past where the tutorial left off. If you are still typing what the video says with nothing of your own added, stop and add something before you go further.
Week 4
Step 1: Check it the way a stranger would
- Every image loads. A missing one is almost always a capital letter in a filename
- Every link goes somewhere
- It works on a phone
- A person who has never seen it knows what to do within a few seconds
- The console is clean
Step 2: Write the explanation
The capstone asks for a written document. Cover:
- What it does. Plain words, no jargon you would have to explain
- How it works. The main pieces and how they fit together
- Which skills you used, and where. Name the file and roughly where in it
- What went wrong and how you worked it out. This section is the most interesting one and students always write the least here. A problem you solved is worth more than a feature that went smoothly
- What you would add with another month
- If you followed a tutorial: which one, a link, and a paragraph on what you built that it did not
Step 3: The walkthrough
Three minutes with your code open. Mr. Krigger points, you explain. Be ready to open any file and explain any line.
- Point at a block of CSS: "what happens if I delete this?"
- Point at a function: "what calls this?"
- "Why is this file in this folder?"
- "What breaks if the Pi restarts?"
- For anything AI touched: "change this, now, while I watch."
You cannot revise for this. It is a conversation about a thing you built, so if you built it you will be fine.
Base and Stretch
Base · every capstone on this route
Your slice grew into your project. It runs, does what you pitched or what your fallback pitched, is hosted on your Pi, and loads on a device that is not the Pi. Your own colors and structure, not a template with the words changed. No horizontal scrolling at phone width. You wrote the explanation and can walk through your own code.
Stretch · required for high school, bonus for middle school
Something the browser cannot do on its own. Pick one: your own API route, state that persists after a reload, real data pulled from somewhere else, or your Pi's own hardware.
The Stretch, step by step
The most common pick is your own API route. Add this above the last line of site_server.py:
import random
ITEMS = ["Put your own list here.", "At least three items.", "They can be anything."]
@app.route("/api/random")
def random_item():
return {"item": random.choice(ITEMS)}
Then in script.js:
async function loadItem() {
const response = await fetch("/api/random");
const data = await response.json();
document.getElementById("output").textContent = data.item;
}
http://YOUR-PI-IP:8000/api/random in a browser first. If the JSON is not there, fix that before you touch the page.A random list is the worked example, so make it something your site actually needs: a score that survives a refresh, a visit counter, live data from the Pi. Choosing state that persists (localStorage or your server), real data from a public API (pick it with Mr. Krigger, not all of them are appropriate or free), or your Pi's own hardware works just as well. Pick whichever one your project actually needs.
Taking your screenshots
On the Pi. Press Print Screen. If nothing happens, open Menu → Accessories and look for Screenshot. Rename it yourname-chess-pi.png and drag it into your YOURNAME folder so it travels with your project.
On a lab computer. Load your site from the Pi's address. Mac is Cmd+Shift+4, then drag a box. Windows is Windows+Shift+S, then drag a box and paste it into any app to save. Name it yourname-chess-on-lab-computer.png.
Pack your project into one file
A project has many files. Pack them into one zip so you only send one thing. Run this in the Terminal. Change yourname and chess to yours.
cd ~/YOURNAME && python3 -m zipfile -c yourname-chess.zip chess site_server.py
Check it is there, along with your Pi screenshot:
ls ~/YOURNAME
yourname-chess.zip and yourname-chess-pi.png are both in the list.Packed again after you change anything? Run the same command again. It replaces the old zip.
Getting it to Schoology: pick a route
The zip and your Pi screenshot are on the Pi. Your lab-computer screenshot is on the lab computer. Try Route 1 first.
| Route | What happens | Logins |
|---|---|---|
| 1. Schoology on the Pi | Log in to Schoology on the Pi and upload from there. | Two: the Pi, then the lab computer |
| 2. Email yourself | Email the two files to yourself from the Pi, then open that email on a lab computer. | Email on the Pi, then Schoology on the lab computer |
| 3. Download from the Pi | Your Pi shares your folder for a minute. You save the two files on a lab computer. | One: Schoology on the lab computer |
Last resort: copy the zip to a jump drive and submit from a lab computer. Name your files so they are readable: yourname-chess.zip.
Route 1: Schoology on the Pi
- Open Chromium on the Pi and press Ctrl+Shift+N. That opens a Private window (Chromium calls it Incognito). It forgets your login when you close it.
- Log in to Schoology. If Chromium offers to save the password, click Never. Do not tick any box that says remember me.
- Open the assignment, click Submit Assignment, then Upload.
- Pick
yourname-chess.zipandyourname-chess-pi.pngfrom Home → YOURNAME. - Check both names are in the list, then submit.
Then log in on a lab computer and upload your lab-computer screenshot there.
Route 2: Email yourself
- On the Pi, press Ctrl+Shift+N in Chromium for a Private window.
- Log in to your email. If Chromium offers to save the password, click Never.
- Write a new message to yourself. Click the paperclip and attach
yourname-chess.zipandyourname-chess-pi.pngfrom yourYOURNAMEfolder. - Send it. Wait until your email says the message was sent.
- Log out of your email, then close the Private window. Do this before you do anything else.
- On a lab computer, open the email, download both files, and upload them to Schoology with your lab-computer screenshot.
.js files. If yours does, use Route 1 or Route 3.Route 3: Download from the Pi
Your Pi can hand out your files the same way it hands out your site. Nothing to log in to on the Pi.
On the Pi, in the Terminal:
cd ~/YOURNAME && python3 -m http.server 8001
It prints a line like this, then sits there listening:
Serving HTTP on 0.0.0.0 port 8001 (http://0.0.0.0:8001/) ...
On the lab computer, go to this address with your Pi's IP:
http://YOUR-PI-IP:8001
Right-click yourname-chess.zip and choose Save link as. Do the same for yourname-chess-pi.png.
Upload the files and your lab-computer screenshot to Schoology from that same computer. One login covers everything.
What you hand in
- Your working code — the whole project folder, every file it needs to run, packed in the zip
site_server.py— the script that hosted it, also in the zip- The address it was served at, like
http://10.0.4.71:8000 - A screenshot of it running on a device that is not the Pi, address bar visible
What you hand in, continued
- A screenshot of your project folder on the Pi
- Your written explanation, the sections above
- If you followed a tutorial: which one, a link, and what you built that it did not
- The filled-in submission template, posted in Schoology under this capstone
Words worth knowing
| Term | What it means |
|---|---|
| Vertical slice | One thin piece of the finished thing, working end to end |
| Fallback | The smaller version of your project you agree to build if the real one runs long |
| Static file | A file the server hands over unchanged: an HTML page, a stylesheet, an image |
| Port | The door number on a computer. Your dashboard uses 5000, this project uses 8000 |
| Route | A line saying which request a function answers |
| Relative path | A path starting from where the file is, like assets/photo.jpg. Works anywhere the folder goes |
| Case sensitive | Linux treats Photo.jpg and photo.jpg as different files. Mac and Windows do not |
| CSS variable | A named color or size defined once in :root and reused with var(--name) |
| Event listener | Code that waits for something to happen, like a click, and then runs |
| Console | The browser panel that shows your JavaScript errors. F12 |
| Responsive | A page that works at any screen width without a separate phone version |
Where to look things up
| W3Schools | Start here. Try It Yourself box on almost every page |
| MDN Web Docs | The real explanation, when W3Schools is not enough |
| freeCodeCamp | Free, self paced, from zero |
| Coolors | Color palettes |
| Google Fonts | Free fonts |
| Unsplash | Free photos you are allowed to use |
| Student projects | What your classmates built |
When it goes wrong
My CSS is not doing anything.
styles.css in the same folder as index.html? Is the <link> line in the <head> spelled exactly right? Did the file save as styles.css and not styles.css.txt?My browser shows the code as text instead of the page.
.html file. Look at the tab name in VS Code. If it says index.html.txt, rename it in the sidebar.My images worked on the laptop and not on the Pi.
src matches the actual file exactly, letter for letter. Rename everything to lowercase.Address already in use.
pkill -f YOURNAME/site_server.py and start again.My JavaScript does nothing.
getElementById could not find that id, so the name in your HTML and the name in your JS do not match. Also check <script src="script.js"></script> is the last thing before </body>.My email refuses the zip.
.js files. Use Route 1 or Route 3 in the Turn in tab instead.The page loads but it is the old version.
Nothing loads from the other computer.
:8000 after the IP?When it goes wrong, continued
I cannot find my jump drive on the Pi.
ls /media/$USER/ lists what is plugged in. If it is empty, unplug and replug, then wait a few seconds.