Passion Taste 5–7 hours Code Club · 01 of 03

A Real Page for One Real Person

No drag-and-drop. No sandbox. Real HTML, CSS, and JavaScript in plain text files. By the end, a public URL holds something only you would have built — for one person whose name you know.

A web page is three text files, sitting on a server somewhere. HTML is the structure. CSS is the look. JavaScript is the behavior. That's the whole thing. Most "code for kids" platforms hide this from you behind block-based editors. We're not going to.

The reason is leverage. The HTML/CSS/JS you write here will run, unchanged, on any computer in the world for the next 15 years. That's a much better deal than learning a platform-specific block language whose servers might not exist in 2030.

The rule about Claude

You can use Claude the entire time. You can paste in Claude's HTML wholesale. The rule is: the part that's only you must be visibly only you. The choice of what to build, who it's for, and what to keep vs. throw away — those are yours.

Step by step

  1. Pick a person and a useful, weird thing.

    Not "a website about my dog" (too vague). Pick someone, and something useful for them. "A page for my brother that decides what he should read next based on the last 5 books he liked." Or "A page for my piano teacher that randomly picks one of her sight-reading drills with a 60-second timer."

    The constraint is: this page should not exist on the internet yet, because only you would have built it.

  2. Set up the file structure (real, not toy).

    Make a folder on your computer. Inside, the canonical four-file shape:

    my-page/ ├── index.html # the page itself ├── style.css # the look ├── script.js # the behavior └── README.md # 5 lines: what is this, who's it for

    Open them in any text editor (VS Code is the standard — it's free). Double-click index.html — it opens in your browser. You're shipping in a browser already.

  3. Make it not look like 1996.

    The default browser styles are ugly. CSS is the cure. Don't try to learn all of CSS — learn five things and you can build a clean page: color, font, spacing, max-width, hover. That's enough to look intentional. The rest is restraint.

  4. Make it do something with JavaScript.

    JavaScript runs in the browser, when the user does something. The pattern is always the same: pick an element, listen for an event, do something, update the page. Once you have this loop, you have all of front-end.

  5. Persist user state with localStorage.

    Real pages remember their users. Your friend lands on your page, picks a vibe, leaves, comes back tomorrow — they should not have to pick the vibe again. localStorage gives you a tiny key-value store right in the browser. 5 lines of code.

  6. Make it work on a phone.

    Half of all web traffic is mobile. The page that looks great on your laptop will look like trash on a 380px screen unless you handled it. The fix is one CSS media query and one viewport meta tag. Test on your actual phone, not the desktop devtools.

  7. Deploy to a real URL.

    Use Netlify Drop or GitHub Pages. Both are free. Both take 2–5 minutes. Send the link to your named person. Watch them use it. Note what surprises you. That's where v1.1 starts.

A complete worked example, every file

"What should Owen read next?" — every file you'd commit. Tabs are independent files in the same folder. All four together = the whole site, ~180 lines, runs anywhere.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>What should Owen read next?</title>
  <meta name="description" content="A book picker for one specific 13-year-old.">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <header>
      <h1>Hey Owen.</h1>
      <p class="lead">Pick a vibe. I'll suggest your next book.</p>
    </header>

    <div class="vibes">
      <button class="vibe" data-vibe="weird-scifi">weird sci-fi</button>
      <button class="vibe" data-vibe="real-history">real history</button>
      <button class="vibe" data-vibe="graphic">graphic novels</button>
      <button class="vibe" data-vibe="surprise">surprise me</button>
    </div>

    <article class="result" id="result" aria-live="polite">
      <p class="placeholder">Pick a vibe to begin.</p>
    </article>

    <footer>
      <p>Built for Owen by his sister.<br>
        <button id="reset" class="link">reset memory</button></p>
    </footer>
  </main>
  <script src="script.js"></script>
</body>
</html>
style.css · ~60 lines, considered restraint
/* color & font — five tools cover everything */
:root {
  --ink: #1F2230;
  --ink-soft: rgba(31, 34, 48, 0.6);
  --paper: #FAF6EC;
  --paper-warm: #FFFCF4;
  --accent: #D9533F;
  --accent-soft: rgba(217, 83, 63, 0.1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: 'Georgia', 'Cambria', serif;
  background: var(--paper);
  color: var(--ink);
  line-height: 1.65;
  padding: 30px 20px;
}
main {
  max-width: 600px;
  margin: 0 auto;
}
header { text-align: center; margin-bottom: 40px; }
h1 {
  font-size: clamp(2rem, 6vw, 2.8rem);
  margin-bottom: 8px;
  letter-spacing: -0.02em;
}
.lead { color: var(--ink-soft); font-size: 1.05rem; }

/* vibes — the buttons */
.vibes {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  margin-bottom: 30px;
}
.vibe {
  font: inherit;
  padding: 14px 18px;
  background: var(--paper-warm);
  color: var(--ink);
  border: 1px solid rgba(31,34,48,.12);
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.15s;
}
.vibe:hover { border-color: var(--accent); background: var(--accent-soft); }
.vibe:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.vibe.active {
  background: var(--accent);
  color: var(--paper-warm);
  border-color: var(--accent);
}

/* result card */
.result {
  background: var(--paper-warm);
  border: 1px solid rgba(31,34,48,.12);
  border-radius: 14px;
  padding: 26px 28px;
  min-height: 140px;
}
.result h2 {
  font-size: 1.4rem;
  margin-bottom: 8px;
  color: var(--accent);
}
.result .author { color: var(--ink-soft); margin-bottom: 12px; }
.result .why { line-height: 1.7; }
.placeholder { color: var(--ink-soft); font-style: italic; }

footer { margin-top: 30px; text-align: center; color: var(--ink-soft); font-size: 0.85rem; }
.link {
  background: none; border: none; padding: 0;
  color: var(--accent); cursor: pointer;
  font: inherit; text-decoration: underline;
}

/* mobile — single column under 480px */
@media (max-width: 480px) {
  .vibes { grid-template-columns: 1fr; }
  body { padding: 20px 14px; }
}
script.js · ~70 lines — the behavior
// ---------- Owen's books — only you would have this list ----------
const BOOKS = {
  'weird-scifi': [
    { title: "Annihilation",      author: "Jeff VanderMeer",  why: "starts ordinary, ends with you not knowing which end is up — you'll like it" },
    { title: "Piranesi",          author: "Susanna Clarke",   why: "narrator slowly figures out something is wrong. very you." },
    { title: "Roadside Picnic",   author: "Strugatsky bros.",  why: "the original 'mysterious zone' book. shorter than you think." },
  ],
  'real-history': [
    { title: "The Wager",         author: "David Grann",      why: "a real shipwreck story that reads like a thriller" },
    { title: "Killers of the Flower Moon", author: "Grann",  why: "same author, ranks even higher with people who've read both" },
    { title: "Endurance",         author: "Alfred Lansing",   why: "Shackleton in Antarctica. cold and incredible." },
  ],
  'graphic': [
    { title: "Saga, vol. 1",       author: "Brian K. Vaughan", why: "the art does work the prose can't. start here." },
    { title: "Persepolis",         author: "Marjane Satrapi",  why: "memoir, black and white, will stay with you" },
  ],
};

// ---------- localStorage — remember the last pick ----------
const STORE_KEY = 'owen-last-vibe';
const lastVibe = localStorage.getItem(STORE_KEY);

// ---------- DOM refs ----------
const result = document.getElementById('result');
const resetBtn = document.getElementById('reset');
const vibeBtns = document.querySelectorAll('.vibe');

// ---------- core logic ----------
function pick(vibe){
  let pool;
  if (vibe === 'surprise') {
    const all = Object.values(BOOKS).flat();
    pool = all;
  } else {
    pool = BOOKS[vibe] || [];
  }
  if (pool.length === 0) {
    result.innerHTML = '<p class="placeholder">No books in that vibe yet — try another.</p>';
    return;
  }
  const book = pool[Math.floor(Math.random() * pool.length)];
  result.innerHTML = `
    <h2>${book.title}</h2>
    <p class="author">by ${book.author}</p>
    <p class="why">${book.why}</p>
  `;
}

// ---------- wire up clicks ----------
vibeBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    vibeBtns.forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    const vibe = btn.dataset.vibe;
    pick(vibe);
    localStorage.setItem(STORE_KEY, vibe);
  });
});

resetBtn.addEventListener('click', () => {
  localStorage.removeItem(STORE_KEY);
  vibeBtns.forEach(b => b.classList.remove('active'));
  result.innerHTML = '<p class="placeholder">Pick a vibe to begin.</p>';
});

// ---------- restore last session ----------
if (lastVibe) {
  const btn = document.querySelector(`.vibe[data-vibe="${lastVibe}"]`);
  if (btn) {
    btn.classList.add('active');
    pick(lastVibe);
  }
}
README.md
## what should Owen read next?

a single-page book picker for my brother Owen, age 13.
not for "young readers in general." for him.

## what's in the four files

  index.html  — page structure
  style.css   — look (Georgia serif, warm cream, four vibes)
  script.js   — pick logic + localStorage memory of last vibe
  README.md   — this

## what it deliberately doesn't do

- does not have an account / login / signup
- does not save your reading history beyond the last vibe
- does not have an "all books" list — that's a different page
- does not work on Windows IE (target: any phone made after 2018)

## who built it

his sister.
deploy.sh · push to GitHub Pages in three lines
#!/usr/bin/env bash
set -e

# 1. make sure we're at the project root
test -f index.html || { echo "no index.html — wrong folder?"; exit 1; }

# 2. commit any changes
git add . && git commit -m "deploy: $(date +%Y-%m-%d)" || true

# 3. push to the gh-pages branch (auto-publishes if Pages is enabled)
git push origin main

echo "✓ pushed. site will be live in ~30s at"
echo "  https://YOUR-USERNAME.github.io/YOUR-REPO/"

Live demo 1: a full HTML/CSS/JS playground

Type into any of the three boxes. The preview updates as you type. This is exactly what your editor + browser will look like in real life. Try editing the book list. Try changing the colors. Break it on purpose, watch what happens.

Live sandbox · the Owen page, editable

index.html
style.css
script.js

Live demo 2: does it work on a phone?

Click the device buttons to resize the preview. The page above should look intentional at every width. If it doesn't, you skipped step 6. (The viewport buttons resize the preview by setting an iframe width. This is exactly what Chrome's devtools do.)

Viewport tester

↑ Reads a small page. Click the buttons to resize.

Live demo 3: localStorage, demystified

Most "save the user's choice" features are 3 lines of code. The hardest part is knowing it exists. The widget below lets you write a key, read it back, and clear it — exactly the API your real page would use. Open this page in a new tab; the data persists.

localStorage explorer


  

Refresh the page — the value sticks. Open in another tab — same value. localStorage.setItem(key, value). That's the whole API.

What makes this hard

Three things will break for you. Step 2: the file paths. Your index.html can't find style.css if they're not in the same folder. Look at the URL bar; if you see file:///, you're loading directly from disk. Good. Step 4: JavaScript errors are silent unless you open the developer console (Cmd-Option-J on Mac, F12 on Windows). When something doesn't work, open the console first. It will tell you the exact line.

Step 6: the mobile media query. The default is to assume desktop, which means your page will be unusable on the phone you actually built it for someone to use. Set the viewport meta tag and write at least one @media (max-width: 480px) rule.

The real hard part, though, is restraint. Once you can build, you'll want to add features. Resist. The page is good when one person uses it twice without prompting from you. Until then, it's not done — but adding features won't get you there. Talking to that one person will.

Self-check before you ship

  • Four files in a folder. They open in any browser, including Safari and Firefox.
  • The page does one thing well, with at least one detail nobody but you would have included.
  • Looks intentional — not corporate, not 1996, just considered.
  • Works on a phone. Tested on the actual phone, not just resized devtools.
  • Persists at least one piece of state with localStorage.
  • Deployed to a public URL I can text to my named person.
  • Named person has used it. I noticed at least one thing they did that I didn't expect.

Push further · for the harder end of 15+

A page that works is the floor. Here's where you start sounding like a real engineer.

  1. Add an offline mode with a Service Worker. Right now your page breaks on the subway. Add ~20 lines of Service Worker that caches the four files and serves them offline. The user opens the page on the train, no signal, it still works. This is the same tech every modern PWA uses. Once you've done it once, you understand 80% of how Twitter / Gmail / every "installable web app" works.
  2. Add full keyboard navigation + skip-to-content. Try driving your page with only the keyboard. Tab to each button. Hit Enter. Notice what's clunky. Add a "skip to content" link, proper focus rings, ARIA-labels for buttons that are just icons. Test with a screen reader. This is what professional accessibility means — and most professional websites don't do it. Yours will.
  3. Build a tiny analytics layer that doesn't track anyone. You want to know whether Owen actually clicks "weird sci-fi" more than "history." But you don't want third-party trackers, cookies, or anything Owen would object to. Write a 30-line counter that logs anonymous click counts to localStorage and shows you a tiny dashboard at ?dashboard=1 in the URL. The constraint: nothing leaves Owen's device. Real engineering, real ethics, in your hands at age 16.