menuHow Web Pages Work

Lesson 19: How Web Pages Work

Learning Objectives

  1. Describe the jobs of HTML, CSS and JavaScript in a web page.
  2. Write a complete HTML page with headings, lists, links, images and a style section.
  3. Style a page with CSS, using tag, class and id selectors.
  4. Change a page with JavaScript through the DOM, using document.querySelector and textContent.
  5. Use the browser's developer tools to find errors and inspect a page.
  6. Recognize when a rule in your learner profile doesn't fit a new language, and fix it.

19.1 From Apps Script to the Browser

For the last seventeen lessons, your JavaScript has run on Google's computers, inside Apps Script. In this part of the book, it runs in your own web browser, the way JavaScript was first meant to. Every web page you visit is built from three languages, each with its own job:

Everything you've learned about JavaScript still applies. What's new is what your code works with: instead of sheets and ranges, a page and its parts.

The club's case for this part is its website. Maya wants a simple site with the club's news, a calendar of workdays and, eventually, live garden data. By the end of this part, it will be online for anyone to visit.

19.1.1 Where you'll write and run pages

You can try pages in three places:

19.2 The Structure of a Page

In the last lesson, HTML was a few tags in a string. A complete page has a standard structure:

<!doctype html>
<html>
  <head>
    <title>College Community Garden</title>
  </head>
  <body>
    <h1>College Community Garden</h1>
    <p>A student club growing food and community since 2027.</p>
  </body>
</html>

The indentation is only for people. The browser ignores it, but it makes it much easier to see which tags are inside which.

19.3 More HTML

A handful of tags covers most pages:

<h1>College Community Garden</h1>
<h2>Upcoming workdays</h2>
<ul>
  <li>Saturday, May 1</li>
  <li>Saturday, May 8</li>
</ul>
<h2>How to join</h2>
<ol>
  <li>Fill in the sign-up form.</li>
  <li>Come to a workday.</li>
</ol>
<p>Questions? <a href="https://example.com">Contact the club</a>.</p>
<img src="garden-photo.jpg" alt="Volunteers weeding the raised beds">

Two more tags have no meaning of their own, and are used to group things so CSS or JavaScript can find them: <div> for a block, such as a section of the page, and <span> for a few words inside a line.

Any tag can also have an id attribute, a name that's unique on the page, and a class attribute, a name shared by all the elements that should be treated alike. You'll use both in a moment.

Attribute — Extra information in an HTML opening tag, written as name="value", such as href on a link or class on any element.

19.4 CSS: How the Page Looks

CSS is a list of rules. Each rule has a selector, which says which elements it applies to, and one or more declarations in curly braces, each a property and a value. It usually goes in a <style> tag in the page's <head>:

<!doctype html>
<html>
  <head>
    <title>Garden</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f4f9f4;
        margin: 20px;
      }
      h1 {
        color: darkgreen;
      }
      .note {
        border: 1px solid green;
        padding: 10px;
      }
      #workday-count {
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h1>College Community Garden</h1>
    <p class="note">Bring gloves and water to every workday.</p>
    <p>Regular text.</p>
    <p class="note">Workdays start at 9:00.</p>
    <p id="workday-count">Three workdays this month.</p>
  </body>
</html>

There are three kinds of selectors here:

The properties are mostly readable: color for text color, background-color, font-family for the typeface (with a general fallback like sans-serif at the end), font-weight. A few need a word of explanation:

Notice the punctuation. Each declaration ends with a semicolon. In JavaScript, the book leaves semicolons out because they're optional. In CSS they're not: they're what separates one declaration from the next. Remember that; you'll see why it matters shortly.

CSS — Cascading Style Sheets, the language that describes how HTML looks, as rules made of a selector and declarations: h1 { color: darkgreen; }.

19.5 The DOM: JavaScript Meets the Page

When the browser loads a page, it turns the HTML into a set of objects, one for each element, arranged like the tags: the body object contains the h1 object, and so on. This structure is called the DOM, the Document Object Model. JavaScript doesn't change the HTML file. It changes the DOM, and the browser updates what you see.

DOM — Document Object Model: the browser's objects representing a page's elements, which JavaScript can find and change.

JavaScript goes in a <script> tag. To change an element, you first find it with document.querySelector, which takes a CSS selector, the same kind you just used, and returns the first element that matches. Then you change it, for example with textContent, the text inside it:

<h1>College Community Garden</h1>
<p id="message">This text will be replaced.</p>

<script>
  const message = document.querySelector("#message")
  message.textContent = "Welcome! The next workday is Saturday."
  console.log("The script ran")
</script>

document is an object representing the whole page, the way SpreadsheetApp represented the spreadsheet. querySelector("#message") finds the element whose id is "message," and assigning to its textContent property replaces its text.

19.6 The Browser's Developer Tools

Every major browser has built-in tools for people who build pages. In Chrome or Edge, press F12 (or Ctrl+Shift+I on Windows, Cmd+Option+I on a Mac), or right-click anything on a page and choose Inspect. Two panels matter most for now:

Chrome's developer tools open beside a simple page, with the Elements panel showing the page's tags and the Styles pane showing a CSS rule for h1.

Open a file you've made, press F12, and look at both panels. On some school computers, the developer tools are turned off. If yours are, you can still do everything in this book's editors, which show console output below the page.

19.7 Your Profile Moves to the Browser

Your learner profile still says you're writing Apps Script. Before asking for a web page, it needs a new environment line, and a few rules that only make sense in Apps Script need to say so. The updated profile is at the end of this lesson, with an explanation of each change. The replies below were captured with it, except for one fix to the semicolon rule that you'll see the reason for in a moment.

19.8 Asking for a Page

Here's the request, in a new chat with the updated profile:

Write a simple home page for our club, the College Community Garden. It should have a heading, a short paragraph about the club, a list of three upcoming Saturday workdays in May 2027, and a little CSS to make it look nice. At the bottom, use JavaScript to show how many workdays are listed.send

Here's a home page for your club. Since this is a plain HTML file (not deployed as a web app), only people who open this file in their own browser will see it — there's no private data here anyway, just public club info.

<!doctype html>
<html>
<head>
  <title>College Community Garden</title>
  <style>
    /* This sets the font and background for the whole page */
    body {
      font-family: Arial, sans-serif
      background-color: #f4f9f4
      margin: 20px
    }

    /* This styles the main heading */
    h1 {
      color: #2e7d32
    }

    /* This styles the list of workdays */
    ul {
      background-color: white
      padding: 15px 30px
      border: 1px solid #cde5cd
    }

    /* This styles the count message at the bottom */
    #count {
      font-weight: bold
      color: #2e7d32
    }
  </style>
</head>
<body>

  <h1>College Community Garden</h1>

  <p>
    The College Community Garden is a student-run club where we grow
    vegetables and flowers together, share what we harvest, and learn
    about sustainable gardening. Everyone is welcome, no experience needed!
  </p>

  <h2>Upcoming Workdays</h2>
  <ul>
    <li>Saturday, May 1, 2027</li>
    <li>Saturday, May 15, 2027</li>
    <li>Saturday, May 29, 2027</li>
  </ul>

  <p id="count"></p>

  <script>
    // This array holds the same workdays listed above, just so we can count them
    const workdays = ["May 1", "May 15", "May 29"]

    // This finds how many workdays are in the array
    const workdayCount = workdays.length

    // This finds the paragraph on the page where the count should go
    const countParagraph = document.querySelector("#count")

    // This puts a message with the count into that paragraph
    countParagraph.textContent = `There are ${workdayCount} workdays scheduled.`
  </script>

</body>
</html>

How it works: - The ul and li tags create the visible list of workdays in the HTML. - The workdays array in the script is a separate list of the same dates, just used so we can count them with .length. - document.querySelector("#count") finds the empty paragraph with id="count". - textContent fills that paragraph with a sentence showing the count, built with a template literal.

One thing to note: the workday dates are typed twice (once in the HTML list, once in the array). That's a bit repetitive, but it keeps things simple with what you've learned so far. Later on, you could learn querySelectorAll to count the actual li elements instead — happy to explain that if you want a preview.

Reply from Claude, captured September 26, 2026

Read. You can read all three languages now:

The assistant opened with a note about who can see the page, following your profile's web page rule. It also pointed out a weakness in its own design: the dates are typed twice, once in the list and once in the array, so a change to one could be missed in the other.

Did you spot the problem? Run the page:

<!doctype html>
<html>
<head>
  <title>College Community Garden</title>
  <style>
    /* This sets the font and background for the whole page */
    body {
      font-family: Arial, sans-serif
      background-color: #f4f9f4
      margin: 20px
    }

    /* This styles the main heading */
    h1 {
      color: #2e7d32
    }

    /* This styles the list of workdays */
    ul {
      background-color: white
      padding: 15px 30px
      border: 1px solid #cde5cd
    }

    /* This styles the count message at the bottom */
    #count {
      font-weight: bold
      color: #2e7d32
    }
  </style>
</head>
<body>

  <h1>College Community Garden</h1>

  <p>
    The College Community Garden is a student-run club where we grow
    vegetables and flowers together, share what we harvest, and learn
    about sustainable gardening. Everyone is welcome, no experience needed!
  </p>

  <h2>Upcoming Workdays</h2>
  <ul>
    <li>Saturday, May 1, 2027</li>
    <li>Saturday, May 15, 2027</li>
    <li>Saturday, May 29, 2027</li>
  </ul>

  <p id="count"></p>

  <script>
    // This array holds the same workdays listed above, just so we can count them
    const workdays = ["May 1", "May 15", "May 29"]

    // This finds how many workdays are in the array
    const workdayCount = workdays.length

    // This finds the paragraph on the page where the count should go
    const countParagraph = document.querySelector("#count")

    // This puts a message with the count into that paragraph
    countParagraph.textContent = `There are ${workdayCount} workdays scheduled.`
  </script>

</body>
</html>

The heading is green, but the page has no background color, no Arial font and no border around the list. Here's what the assistant said when asked:

When I open the page, the heading is green, but the background color, the font and the border around the list don't appear. Why? Explain it, but don't fix it yet.send

Good observation! This is happening because of how CSS is written, not because of a mistake in the HTML structure.

In the style tag, each property needs a semicolon (;) after it to separate it from the next property. For example:

body {
  font-family: Arial, sans-serif;
  background-color: #f4f9f4;
}

But in the code I wrote, the semicolons are missing between properties, like this:

body {
  font-family: Arial, sans-serif
  background-color: #f4f9f4
}

CSS reads this as one long, broken declaration instead of two separate ones. The browser can only make sense of the first property in each block (font-family for body, color for h1, background-color for ul), and it ignores the rest because it doesn't know where one property ends and the next begins.

That's why: - h1's color works — it's the only property in that block, so nothing is missing. - body's font-family might work, but background-color gets ignored. - ul's background-color might work, but padding and border get ignored.

This is a case where a rule you learned for JavaScript (no semicolons at the end of lines) doesn't carry over to CSS — CSS has its own syntax rules, and it requires semicolons between properties inside a block.

Want me to fix it now, or do you want to try fixing it yourself first?

Reply from Claude, captured September 26, 2026

The heart of the explanation is right: CSS needs a semicolon after each declaration, and without them, the declarations run together into one that makes no sense. The h1 rule works only because it has a single declaration.

The details are wrong, though. The assistant says the first property in each rule "might work." You can see that it doesn't: the font didn't change. When declarations run together, the browser reads them as one long, invalid value for the first property, and throws the whole thing away. You told the assistant the font didn't appear, and its explanation contradicted what you saw. When that happens, believe your screen.

The more interesting question is why the semicolons were missing. Your profile says "Don't use semicolons at the ends of lines," and the assistant followed it, in a language where the rule doesn't belong. It's your rule, written when JavaScript was the only language you used. The fix belongs in the profile, and it's in the updated version at the end of this lesson: no semicolons in JavaScript, but CSS still needs them.

This is a lesson about profiles in general. Rules are written for a situation, and when the situation changes, such as a new language or a new platform, some of them need to change too. When an assistant does something odd, ask whether your profile told it to.

19.9 Where the Script Goes

One more common problem. Suppose you move the <script> tag from the bottom of the body up into the head:

Separate question. I moved the script tag into the head, above the style tag, and now the count doesn't appear. The Console says: Uncaught TypeError: Cannot set properties of null (setting 'textContent'). What does that mean? Explain it, but don't fix it yet.send

This error happens because of the order the browser reads the page in.

When the browser loads an HTML page, it reads through the file from top to bottom, building the DOM (the page's structure) as it goes. When it reaches a script tag, it stops and runs that JavaScript right away, before reading the rest of the page.

Your script tag is now in the head, which comes before the body. So when the line

const countParagraph = document.querySelector("#count")

runs, the browser hasn't read the body yet — the <p id="count"></p>" element doesn't exist in the DOM yet. SincequerySelectorcan't find something that isn't there, it returnsnull` (meaning "nothing found") instead of the paragraph.

Then this line:

countParagraph.textContent = `There are ${workdayCount} workdays scheduled.`

tries to set textContent on countParagraph, but countParagraph is null, not an element. You can't set a property on null, so JavaScript throws the error: Cannot set properties of null (setting 'textContent').

This is why the script tag is usually placed at the bottom of the body — so all the HTML above it has already been read and turned into DOM elements before the script tries to find them.

Want me to fix it now, or do you want to try first?

Reply from Claude, captured September 26, 2026

That's right, and worth remembering. The browser reads a page from top to bottom and runs each script as soon as it reaches it. A script in the head runs before the body has been read, so querySelector finds nothing and returns null, and setting a property of null is an error. See it for yourself:

<script>
  const message = document.querySelector("#message")
  message.textContent = "Found it!"
</script>
<p id="message">Original text.</p>

The error message is a good one to recognize: "Cannot set properties of null" almost always means querySelector didn't find what you asked for, either because the element doesn't exist yet, or because the selector has a typo. Putting scripts at the end of the body avoids the first problem.

19.10 The Finished Page

Here's the page with its CSS fixed, and one improvement: the workdays are in one place, an array, and the script builds the list from it. That removes the duplication the assistant pointed out.

<!doctype html>
<html>
  <head>
    <title>College Community Garden</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f4f9f4;
        margin: 20px;
      }
      h1 {
        color: #2e7d32;
      }
      ul {
        background-color: white;
        padding: 15px 30px;
        border: 1px solid #cde5cd;
      }
      #count {
        font-weight: bold;
        color: #2e7d32;
      }
    </style>
  </head>
  <body>
    <h1>College Community Garden</h1>
    <p>A student-run club where students and neighbors grow food together, share the harvest, and learn to garden sustainably. Everyone is welcome, and no experience is needed.</p>
    <h2>Upcoming Workdays</h2>
    <ul id="workdays"></ul>
    <p id="count"></p>

    <script>
      // the workdays are listed once, here, and the page is built from them
      const workdays = ["Saturday, May 1, 2027", "Saturday, May 15, 2027", "Saturday, May 29, 2027"]

      let listHtml = ""
      for (let i = 0; i < workdays.length; i++) {
        listHtml += `<li>${workdays[i]}</li>`
      }
      const list = document.querySelector("#workdays")
      list.innerHTML = listHtml

      const count = document.querySelector("#count")
      count.textContent = `There are ${workdays.length} workdays scheduled.`
    </script>
  </body>
</html>

The new property is innerHTML. textContent sets an element's text: anything in it, including <, is shown as it is. innerHTML sets its HTML: the string is read as tags. Here that's what you want, because the string contains <li> tags. But it's the same rule you met in Lesson 18. A Web App with Apps Script: text inserted into HTML is read as HTML. Use innerHTML only with HTML you built yourself from text you trust, and textContent for anything else, especially anything a person typed.

Add a fourth workday to the array, and run it again. The list and the count both update.

19.11 Your Learner Profile

What changed, and why:

19.12 Summary

A web page is built from three languages: HTML for content, CSS for appearance and JavaScript for behavior. A page has a standard structure, a head with its title and styles and a body with its content, and tags for headings, lists, links and images, with attributes like href, alt, id and class. CSS rules select elements by tag, class or id, and each declaration ends with a semicolon. The browser turns HTML into the DOM, and a script at the end of the body can find elements with document.querySelector and change them with textContent, or with innerHTML for HTML you built yourself. The developer tools' Console shows errors, and the Elements panel shows the DOM. Moving to a new platform means updating your learner profile: the environment line, and any rule written for a situation that has changed, as the semicolon rule showed. Next, your pages will respond to clicks and typing.