
EE 547 - Unit 7
Fall 2025
Browser requests document from server
Server responds with HTML document:
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<p>Member since January 15, 2024</p>
<section class="activity">
<h2>Your Activity</h2>
<div class="stat">
<span class="number">47</span>
<span class="label">Total Orders</span>
</div>
<div class="stat">
<span class="number">$2,847.50</span>
<span class="label">Total Spent</span>
</div>
</section>
<div class="actions">
<a href="/orders">View Orders</a>
<a href="/settings">Settings</a>
</div>
</body>
</html>HTML describes document structure using markup tags.
Browser receives this text and renders visual display on screen.

Browser transforms markup into visual document:
User sees formatted document, not HTML tags.
Same HTTP protocol as APIs - different content type and purpose.

Tags describe document structure
<h1> tag marks heading Browser renders “Alice Chen” as large, bold text </h1> closes the heading tag
<p> tag marks paragraph Browser renders as normal text with spacing </p> closes the paragraph
<a> tag creates link href attribute specifies destination URL Browser renders “View Orders” as clickable text Click navigates to /orders
Nesting creates hierarchy:
<section> contains heading and paragraph Indentation shows structure (not required, aids readability) Browser understands parent-child relationships
Attributes configure elements:
class attribute names element for styling Multiple elements can share same class Browser applies CSS rules matching class names


HTML evolution 1995-2025:
Structure (1995-1997)
Separation (1997-2000)
<font> deprecatedFailed strictness (2000)
Pragmatic evolution (2008-2014)
HTML5 accepts messy reality
Error recovery built-in
New elements for applications:
<canvas> for graphics<video>/<audio> native<section>, <article> semanticsLiving standard (2014+)
Backward compatibility:
Tags describe meaning, not appearance
<h1>Alice Chen</h1>
Declares: “This is a top-level heading”
Does NOT specify:
Browser applies default heading styles.
<strong>important</strong>
Declares: “This text is important”
Does NOT specify:
Browser chooses emphasis method (typically bold).
<p>This is a paragraph.</p>
Declares: “This is a paragraph of text”
Does NOT specify:
Browser applies paragraph formatting.
HTML separates structure from presentation.
Same markup renders differently:
Not a programming language
HTML has no:
Variables:
Loops:
Conditionals:
Functions:
Computation:
HTML only describes structure.
Processing and logic happen elsewhere:
HTML itself is static document structure.
Browser parses HTML into tree of nodes
HTML text:
<div id="profile">
<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<section>
<h2>Activity</h2>
<p>Orders: 47</p>
</section>
</div>Browser builds DOM tree:
div (id="profile")
├── h1
│ └── "Alice Chen"
├── p
│ └── "Email: alice@example.com"
└── section
├── h2
│ └── "Activity"
└── p
└── "Orders: 47"
Each tag becomes node in tree Text becomes leaf nodes Parent-child relationships preserved from nesting
DOM allows programmatic access:
JavaScript can traverse and modify tree:
// Get element by ID
let profile = document.getElementById('profile');
// Get child elements
let heading = profile.querySelector('h1');
// Modify content
heading.textContent = 'Alice C. Chen';
// Add new element
let newPara = document.createElement('p');
newPara.textContent = 'Phone: 555-0123';
profile.appendChild(newPara);Changes to DOM immediately reflected in display.

Tree structure enables:
Navigation between elements:
Styling rules:
JavaScript manipulation:
Browser maintains this tree in memory while page displayed.
Stylesheet defines visual properties
HTML without styling:
Browser applies default styles:
<h1> renders 32px, bold, Times New Roman<p> renders 16px, normal weight, Times New RomanCSS overrides defaults:
h1 {
font-size: 48px;
color: #2c3e50;
font-family: Arial, sans-serif;
margin-bottom: 20px;
}
p {
font-size: 18px;
color: #34495e;
line-height: 1.6;
}Browser applies these rules to matching elements.
Class selectors target specific elements:
.stat-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.number {
font-size: 36px;
font-weight: bold;
color: #3498db;
}
.label {
font-size: 14px;
color: #7f8c8d;
}Elements with matching class names receive these styles.

CSS separates content from presentation:
Same HTML:
Different stylesheets produce different appearance:
Content unchanged - only presentation differs.
Cascade and inheritance:
Paragraph inherits font from body Element with class="special" overrides color
Rules cascade from general to specific.
Browser transforms HTML into pixels on user’s machine
Server sends HTML text:
Browser operations (client-side):
Parse HTML into DOM tree Apply CSS to calculate styles Compute layout (positions and sizes) Paint pixels to screen
All rendering happens on user’s computer, not server.
Server responsibility:
@app.route('/profile/<int:user_id>')
def show_profile(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
html = f"""
<h1>Welcome, {user.name}</h1>
<p>Total Orders: {user.order_count}</p>
"""
return htmlServer queries database and generates HTML text (7ms) Server sends HTML to client Server done - rendering is client’s responsibility
Client responsibility:
Browser receives HTML text Parses into DOM Applies styles Calculates layout for screen size Renders pixels
Total client rendering time: 80ms on user’s CPU
Server handles 1000 users: 7 seconds total CPU Clients handle 1000 renderings: Distributed across 1000 machines

Rendering distribution:
1000 concurrent users viewing profiles
Server work:
Client work:
Each user’s browser uses their own CPU for rendering.
Server only generates markup - does not render pixels.
HTTP serves both documents and data
API request for data:
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...Server returns structured data:
Service consumes data programmatically.
Browser request for document:
Server returns marked-up document:
<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<p>Total Orders: 47</p>
<p>Total Spent: $2,847.50</p>Browser renders document visually.
Both use HTTP - Content-Type header determines interpretation.

Flask serving both:
@app.route('/users/<int:user_id>')
def get_user(user_id):
user = db.query(
"SELECT * FROM users WHERE id = ?",
user_id
)
accept = request.headers.get('Accept', '')
if 'application/json' in accept:
return {
'user_id': user.id,
'email': user.email,
'order_count': user.orders
}
else:
return render_template(
'profile.html',
user=user
)Same database query Different response format based on Accept header
APIs and browsers share HTTP infrastructure - serve different purposes.
Code runs in two separate environments
Server-side (before response sent):
@app.route('/dashboard')
def dashboard():
# Runs on server
user_id = session.get('user_id')
# Server accesses database
user = db.query(
"SELECT * FROM users WHERE id = ?",
user_id
)
orders = db.query(
"SELECT * FROM orders WHERE user_id = ?",
user_id
)
# Server has secrets
stripe_key = os.environ['STRIPE_SECRET_KEY']
# Generate HTML
html = render_template(
'dashboard.html',
user=user,
orders=orders
)
return html
# Server execution endsBrowser receives:
Client-side (after HTML received):
<script>
// Runs in browser
document.getElementById('refresh')
.addEventListener('click', function() {
// Browser makes HTTP request
fetch('/api/orders')
.then(r => r.json())
.then(data => {
// Update page
updateOrderList(data);
});
});
</script>JavaScript executes in browser - different environment than server Python.

Server environment:
Accesses database directly Reads files from disk Has environment variables with secrets Calls other backend services Stores session data
Client environment:
Modifies DOM elements Responds to user events (clicks, typing) Stores data in browser (localStorage, cookies) Cannot access database Cannot read server files Cannot access API keys
Communication only through HTTP requests.
Client code and data visible to user
HTML and JavaScript sent to browser:
<script>
function submitOrder() {
let price = document.getElementById('price').value;
// Client-side validation
if (price < 100) {
alert('Minimum order $100');
return;
}
// Submit to server
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({price: price})
});
}
</script>User opens DevTools console and bypasses validation:
// Modify price directly
document.getElementById('price').value = 0.01;
// Call submit function
submitOrder();
// Or make request directly
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({price: 0.01})
});Order submitted at $0.01 because client validation bypassed.
Server must enforce rules:
@app.route('/api/orders', methods=['POST'])
def create_order():
price = request.json['price']
# Server validation required
if price < 100:
return {'error': 'Minimum order $100'}, 400
order = db.insert('orders', price=price)
return {'order_id': order.id}, 201Server validation cannot be bypassed - code not visible to user.

User can view all client-side code:
Browser View Source shows HTML and JavaScript DevTools shows network requests Console allows code execution Elements tab shows DOM structure
User cannot view server code:
Python code stays on server Database queries never sent to client Environment variables inaccessible Business logic hidden
Client-side validation improves user experience:
Server-side validation enforces security:
Both serve different purposes - client for usability, server for security.
Each HTTP request crosses network boundary
Local function call (same process):
def calculate_total(items):
return sum(item.price for item in items)
total = calculate_total(cart_items)
# Execution: 0.00001msFunction executes in process memory.
HTTP request to server:
fetch('/api/cart/total')
.then(response => response.json())
.then(data => displayTotal(data.total));Request components:
Total: 150-400ms depending on distance and caching
Geographic distance:
Round-trip time accumulates with each request.

Network crossing unavoidable:
Physical distance limits speed of light in fiber Signal must travel to server and back Cannot eliminate latency
Design implications:
Minimize number of requests Combine related data in single response Cache responses when appropriate Use connection reuse (HTTP/2)
Single request returning combined data:
@app.route('/api/checkout-data')
def checkout_data():
return {
'user': get_user(),
'cart': get_cart(),
'shipping': get_shipping(),
'tax': calculate_tax()
}One request with all data vs four separate requests.
Sequential requests accumulate latency
Checkout page loading data sequentially:
// Get user first
fetch('/api/user')
.then(r => r.json())
.then(user => {
displayUser(user);
// Then get cart (waits for user)
return fetch('/api/cart');
})
.then(r => r.json())
.then(cart => {
displayCart(cart);
// Then shipping (waits for cart)
return fetch('/api/shipping');
})
.then(r => r.json())
.then(shipping => {
displayShipping(shipping);
});Each request waits for previous to complete:
Total: 450ms (3 × 150ms)
Parallel requests start simultaneously:
Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/cart').then(r => r.json()),
fetch('/api/shipping').then(r => r.json())
])
.then(([user, cart, shipping]) => {
displayUser(user);
displayCart(cart);
displayShipping(shipping);
});All three requests start at time 0:
Total: 150ms
Parallel requests when data independent.

Dependencies require sequencing:
Some requests depend on previous responses:
// Must be sequential
fetch('/api/user')
.then(r => r.json())
.then(user => {
// Need user.id for cart
return fetch(`/api/cart?user_id=${user.id}`);
});Second request needs data from first.
Server-side combination:
Instead of client making multiple requests:
// Single request
fetch('/api/checkout-data')
.then(r => r.json())
.then(data => {
displayUser(data.user);
displayCart(data.cart);
displayShipping(data.shipping);
});Server endpoint combines data:
@app.route('/api/checkout-data')
def checkout_data():
user = get_user()
cart = get_cart(user.id)
shipping = get_shipping(user.zip)
return {
'user': user,
'cart': cart,
'shipping': shipping
}One client request Server makes internal queries (no network latency) Returns combined response
HTML (Markup)
<article>
<h1>Weather Report</h1>
<p>Today: Sunny, 75°F</p>
<ul>
<li>Morning: 65°F</li>
<li>Afternoon: 75°F</li>
<li>Evening: 70°F</li>
</ul>
</article>Describes WHAT:
Python (Programming)
def weather_report(temp):
if temp > 70:
status = "Warm"
else:
status = "Cool"
forecast = []
for hour in range(24):
forecast.append(
calculate_temp(hour)
)
return {
"status": status,
"temps": forecast
}Describes HOW:
HTML declares structure. CSS styles it. JavaScript adds behavior.
HTML alone cannot: validate input, calculate values, make decisions, or modify itself.
This separation is by design - each technology has a specific role.


Multiple attributes example:
<a href="https://example.com"
target="_blank"
title="Visit our site"
class="external-link">
Click here
</a>Each attribute:
Common global attributes:
id="unique-name"
class="style-name"
data-*="custom"
Boolean attributes:
Presence = true, absence = false

Default display behavior:
<!-- Block elements -->
<h1>Title</h1>
<p>First paragraph.</p>
<p>Second paragraph.</p>
<!-- Renders as: -->
Title
First paragraph.
Second paragraph.Each block starts a new line.
<!-- Inline elements -->
<p>
Text with <strong>bold</strong>
and <a href="#">link</a> inside.
</p>
<!-- Renders as: -->
Text with bold and link inside.Inline elements flow with text.
Layout implications:
Block elements:
Inline elements:
Invalid nesting:

Complete declaration:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>International Site</title>
</head>
<body>
<h1>Café München</h1>
<p>Price: 50€</p>
<p>北京 • москва • القاهرة</p>
<p>Emoji: 🎉 🚀 ❤️</p>
</body>
</html>UTF-8 Coverage:
Server should also send:
Meta tag provides fallback when:
Without charset declaration, browsers guess encoding based on:
Characters require encoding to bytes
Same character “é” (U+00E1):
UTF-8: C3 A9 (2 bytes)
UTF-16: 00 E1 (2 bytes)
ISO-8859-1: E1 (1 byte)
Code point (what) ≠ byte representation (how)
UTF-8 dominates (97% of web):
Variable length: 1-4 bytes per character
1 byte: ASCII (0-127) a → 61
2 bytes: Latin, Greek, Cyrillic (128-2047) é → C3 A9
3 bytes: CJK, most common scripts (2048-65535) 中 → E4 B8 AD
4 bytes: Emoji, rare scripts (65536+) 😀 → F0 9F 98 80
Continuation bytes start with 10 (self-synchronizing)
Backward compatible with ASCII
HTML declaration required:
Wrong encoding = mojibake:
File saved UTF-8, browser reads Latin-1:
"Café" → "Café"
File saved Latin-1, browser reads UTF-8:
Shows: �
Common failure points:
Database connections, form submissions, file storage—all must use matching encoding.

Common mojibake patterns:
| Original | Wrong Display | Cause |
|---|---|---|
| Café | Café | UTF-8 as Latin-1 |
| “smart” | “smart†| Windows-1252 as UTF-8 |
| 北京 | ?????? | Lost in conversion |
Always declare encoding explicitly:
<meta charset="UTF-8"> in first 1024 bytesHTML uses <, >, & as syntax
To display these in content, use entities.
Reserved characters:
| Character | Entity | Purpose |
|---|---|---|
& |
& |
Entity/reference start |
< |
< |
Tag start |
> |
> |
Tag end |
" |
" |
Attribute delimiter |
Common symbols:
| Symbol | Entity | Name |
|---|---|---|
| © | © |
Copyright |
| € | € |
Euro |
| — | — |
Em dash |
| (space) | |
Non-breaking space |
Numeric references (any Unicode):
Both render: á
Non-breaking space:
prevents line break between words.
Keeps “Price:” and “$50” together.

Code examples need entities:
Without entities, <script> would execute as JavaScript.
Use entities when:
)Non-Semantic (Generic)
<div class="header">
<div class="title">News</div>
</div>
<div class="nav">
<div>Home</div>
<div>About</div>
</div>
<div class="content">
<div class="post">
<div class="post-title">
Article Title
</div>
<div>Content...</div>
</div>
</div>
<div class="footer">
© 2025
</div>Problems:
Semantic elements describe their purpose, not appearance.
Screen readers announce “navigation” for <nav>, “main content” for <main>.
Search engines understand page structure from semantic tags.

Semantic elements (HTML5):
Each tag describes its content purpose:
<header> - Introductory content <nav> - Navigation links <main> - Primary page content <article> - Self-contained content <section> - Thematic grouping <aside> - Tangential content <footer> - Footer information <figure> - Illustration with caption <figcaption> - Caption for figure <details> - Collapsible content <summary> - Details heading <mark> - Highlighted text <time> - Date/time data <address> - Contact information
Before HTML5 (div soup):
<div class="header">
<div class="nav">...</div>
</div>
<div class="content">
<div class="article">...</div>
<div class="sidebar">...</div>
</div>
<div class="footer">...</div>With HTML5 semantics:
<header>
<nav>...</nav>
</header>
<main>
<article>...</article>
<aside>...</aside>
</main>
<footer>...</footer>Benefits:
Semantic = meaning, not style

Nesting creates hierarchy:
<article>
<header>
<h2>Article Title</h2>
<time>Jan 15, 2025</time>
</header>
<section>
<p>First paragraph...</p>
<p>Second paragraph...</p>
</section>
<footer>
<a href="/share">Share</a>
</footer>
</article>Tree relationships:
JavaScript access:
// Get element
let article =
document.querySelector('article');
// Navigate tree
let title =
article.querySelector('h2');
// Modify content
title.textContent = 'New Title';
// Add element
let newPara =
document.createElement('p');
article.appendChild(newPara);Browser maintains tree in memory. Changes immediately update display.

Semantic importance, not size:
<h1>Research Paper</h1>
<p>Introduction text...</p>
<h2>Methodology</h2>
<p>We conducted...</p>
<h3>Data Collection</h3>
<p>Samples were...</p>
<h3>Analysis Method</h3>
<p>Statistical...</p>
<h2>Results</h2>
<p>Our findings...</p>Screen reader navigation:
User hears: “Heading level 1: Research Paper” “Heading level 2: Methodology” “Heading level 3: Data Collection”
Can jump between headings with keyboard.
SEO impact:
Search engines:
Weight content by heading level.
Common mistakes:
<!-- WRONG: Using for size -->
<h1>Welcome</h1>
<h3>Subtitle text</h3>
<!-- RIGHT: Proper hierarchy -->
<h1>Welcome</h1>
<h2>Subtitle text</h2>CSS controls size, HTML provides structure.
Unordered list (no sequence):
<ul>
<li>Neural Networks</li>
<li>Decision Trees</li>
<li>Support Vector Machines</li>
<li>Random Forests</li>
</ul>Ordered list (sequence matters):
Nested lists (hierarchy):
<ul>
<li>Frontend
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript
<ul>
<li>React</li>
<li>Vue</li>
</ul>
</li>
</ul>
</li>
<li>Backend
<ul>
<li>Python</li>
<li>Node.js</li>
</ul>
</li>
</ul>Browser changes bullet style per level. Can mix ordered and unordered.

Relative path resolution:
Current page: https://example.com/blog/posts/article.html
contact.html → /blog/posts/contact.html/contact.html → /contact.html../index.html → /blog/index.html../../home.html → /home.html
Alt text purposes:
Accessibility
Failed loads
SEO value
Responsive images:
<img src="photo.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1000px) 50vw,
800px"
alt="Product photo">Browser chooses appropriate size:
Common formats:

| Model | Accuracy | Speed |
|---|---|---|
| ResNet-50 | 94.2% | 22ms |
| BERT | 91.8% | 45ms |
| GPT-2 | 93.5% | 67ms |
Complete structure:
Table elements:
<table> - Container <caption> - Table title <thead> - Header section <tbody> - Body section <tfoot> - Footer section <tr> - Table row <th> - Header cell <td> - Data cell
Appropriate table usage:
Inappropriate table usage:
Accessibility:
Spanning cells:
Creates merged cells for complex headers or grouped data.
IFrame embeds another HTML page:
Browser makes HTTP request to src URL and renders the result inside a rectangle on the current page.
Common uses: Maps, videos (YouTube), payment forms (Stripe), third-party widgets
Security: Same-origin policy
Origin = protocol + domain + port
Different origins cannot access each other:
Parent (example.com):
<iframe id="map" src="https://maps.google.com/embed">
</iframe>
<script>
// BLOCKED - different origin
iframe.contentDocument.getElementById('map');
// SecurityError: Blocked
</script>IFrame (maps.google.com):
Same origin can access:
Parent (example.com):
<iframe src="/widget.html"></iframe>
<script>
// ALLOWED - same origin
iframe.contentDocument.getElementById('btn');
</script>Origin comparison:
https://example.com:443 ≠ http://example.com:80 (protocol)https://example.com ≠ https://www.example.com (domain)https://example.com ≠ https://example.com:8080 (port)
PostMessage enables controlled cross-origin communication
Parent sends message:
iframe.contentWindow.postMessage(
{action: 'resize', height: 400},
'https://widget.com' // Target origin
);IFrame receives:
window.addEventListener('message', (event) => {
// MUST verify sender origin
if (event.origin !== 'https://parent.com') {
return; // Ignore untrusted messages
}
if (event.data.action === 'resize') {
// Handle message
}
});Always verify event.origin before processing messages.
Sandbox attribute restricts iframe capabilities
No sandbox (default - full permissions):
Default IFrame permissions:
Full sandbox (maximum restriction):
Sandboxed IFrame restrictions:
Selective permissions:
Enables only JavaScript and form submission.

Real-world examples:
Payment form (needs scripts, forms, origin):
<iframe src="https://stripe.com/checkout"
sandbox="allow-scripts
allow-forms
allow-same-origin">
</iframe>Third-party widget (scripts only, isolated):
User-generated content (display only):
Multiple permissions:
Space-separated list enables selected capabilities.

<input type="text"
name="username"
placeholder="Enter username"
required>
<input type="email"
name="email"
placeholder="user@example.com"
required>
<input type="password"
name="password"
placeholder="Password"
minlength="8">
<input type="number"
name="age"
min="1"
max="120"
step="1">
<input type="url"
name="website"
placeholder="https://...">
<input type="search"
name="query"
placeholder="Search...">
<input type="date"
name="birthday"
max="2025-12-31">
<input type="color"
name="theme"
value="#3498db">Input attributes:
name attribute required for submissiontype determines validation and keyboardRadio buttons (exclusive):
Radio button group:
<input type="radio"
name="plan"
value="basic"
checked>
<label>Basic</label>
<input type="radio"
name="plan"
value="pro">
<label>Pro</label>Same name = mutually exclusive. Only one value submitted.
Checkbox group:
<input type="checkbox"
name="features"
value="api"
checked>
<label>API Access</label>
<input type="checkbox"
name="features"
value="support">
<label>Support</label>Same name = array of values. Unchecked = not submitted.
Dropdown with groups:

Textarea for multi-line input:
Textarea syntax:
<textarea name="comments"
rows="5"
cols="40"
maxlength="500"
placeholder="Enter text...">
Default content here
</textarea>Label benefits:
<!-- Method 1: for/id -->
<label for="email">
Email Address:
</label>
<input type="email"
id="email"
name="email">
<!-- Method 2: Wrapping -->
<label>
<input type="checkbox"
name="subscribe">
Subscribe to newsletter
</label>Label benefits:
Fieldset groups related inputs:
<fieldset>
<legend>Shipping Address</legend>
<input name="street">
<input name="city">
<input name="zip">
</fieldset>Visual and semantic grouping.
Try submitting this form:
Note: Browser validates before submission. Try invalid inputs to see error messages.
Validation attributes:
<!-- Required field -->
<input type="email"
required>
<!-- Length constraints -->
<input type="text"
minlength="3"
maxlength="50">
<!-- Number range -->
<input type="number"
min="0"
max="100"
step="5">
<!-- Pattern matching -->
<input type="text"
pattern="[A-Z]{2,4}"
title="2-4 uppercase letters">
<!-- Custom message -->
<input type="email"
required
oninvalid="this.setCustomValidity('Please enter valid email')"
oninput="this.setCustomValidity('')">Validation happens:
CSS pseudo-classes:
input:valid {
border-color: green;
}
input:invalid {
border-color: red;
}
input:required {
border-left: 3px solid blue;
}Visual feedback for validation state.

Server must validate everything:
@app.route('/register', methods=['POST'])
def register():
email = request.form['email']
age = request.form['age']
# Server validation (required!)
if not '@' in email:
return "Invalid email", 400
if int(age) < 13 or int(age) > 120:
return "Invalid age", 400
# Only now safe to process
create_user(email, age)<!DOCTYPE html>
<html>
<head>
<title>Event Registration</title>
</head>
<body>
<h1>Tech Conference 2025</h1>
<p>Join us for three days of talks and workshops.</p>
<h2>Event Details</h2>
<p>Date: March 15-17, 2025</p>
<p>Location: Convention Center</p>
<p>Price: $299</p>
<h2>Register Now</h2>
<p>Fill out the form below:</p>
<form>
<p>Name: <input type="text" name="name"></p>
<p>Email: <input type="email" name="email"></p>
<p><button type="submit">Register</button></p>
</form>
</body>
</html>No CSS provided - browser uses built-in defaults.
Join us for three days of talks and workshops.
Date: March 15-17, 2025
Location: Convention Center
Price: $299
Fill out the form below:
Browser defaults:
Functional but generic - every site looks identical.
body {
font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
margin: 0;
}
h1 {
font-size: 48px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
margin-bottom: 10px;
}
h2 {
border-bottom: 3px solid rgba(255,255,255,0.5);
padding-bottom: 10px;
margin-top: 30px;
}
form {
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
input {
width: 100%;
padding: 12px;
border: none;
border-radius: 5px;
margin-top: 5px;
font-size: 16px;
}
button {
background: white;
color: #667eea;
padding: 15px 40px;
border: none;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: scale(1.05);
}Join us for three days of talks and workshops.
Date: March 15-17, 2025
Location: Convention Center
Price: $299
Fill out the form below:
Same HTML structure:
CSS transforms presentation:
Content identical - only appearance changed.


CSS connects to HTML through selectors:
HTML elements:
CSS rules:
Browser matches:
<h1> elements<p> elementsBoth paragraphs get same styling - selector matches element type.
Comments for documentation:
/* comment */ ignored by browser.
External stylesheet (recommended):
HTML file:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
<title>My Page</title>
</head>
<body>
<h1>Welcome</h1>
</body>
</html>styles.css file:
Benefits:
Internal stylesheet:
CSS in <style> tag within HTML.
Inline styles (avoid):
<h1 style="color: blue; font-size: 32px;">
Welcome
</h1>
<p style="color: gray; line-height: 1.6;">
Paragraph with inline styles.
</p>Style attribute on each element.
Problems:
Loading order matters:
<head>
<!-- External first -->
<link rel="stylesheet" href="base.css">
<!-- Then internal -->
<style>
h1 { color: red; } /* Overrides base.css */
</style>
</head>
<body>
<!-- Inline highest priority -->
<h1 style="color: green;">Title</h1>
</body>Later styles override earlier ones. Inline > Internal > External
External stylesheets cache across pages. Inline styles cannot be cached and mix structure with presentation.

Element selectors target HTML tags directly:
h1 {
color: blue;
font-size: 32px;
}
p {
color: gray;
line-height: 1.6;
}
a {
color: green;
text-decoration: none;
}HTML:
Result:
Universal selector targets everything:
Asterisk matches every single element on page.
Resets all default spacing - common in CSS reset stylesheets.

Classes identified by period (.):
CSS:
.highlight {
background: yellow;
}
.important {
font-weight: bold;
color: red;
}
.hidden {
display: none;
}HTML:
<p class="highlight">
Highlighted paragraph
</p>
<span class="important">
Critical information
</span>
<div class="highlight important">
Both classes applied
</div>
<section class="hidden">
Not displayed
</section>Class advantages:
Class naming conventions:
.error-message not .red-textIDs identified by hash (#):
CSS:
#header {
background: navy;
padding: 20px;
}
#sidebar {
width: 300px;
float: left;
}
#main-content {
margin-left: 320px;
}
#footer {
background: gray;
text-align: center;
}HTML:
<header id="header">
<h1>Site Title</h1>
</header>
<aside id="sidebar">
<nav>...</nav>
</aside>
<main id="main-content">
<p>Page content...</p>
</main>
<footer id="footer">
<p>Copyright 2025</p>
</footer>Each ID must be unique on the page - only one id="header" allowed.

When to use each:
Classes:
IDs:
Specificity matters:
ID wins over class due to higher specificity.

Style based on location:
/* Paragraphs in articles */
article p {
font-size: 18px;
line-height: 1.8;
}
/* Paragraphs in asides */
aside p {
font-size: 14px;
color: #666;
}
/* Links in navigation */
nav a {
text-decoration: none;
padding: 10px;
}
/* Links in content */
article a {
color: blue;
text-decoration: underline;
}Nested descendants:
Matches links inside list items inside unordered lists inside nav inside header.
Can traverse any depth - not just direct children.
Direct child selector (>):
> restricts to immediate children only.

Combining for specificity:
Element and class:
Only <p class="intro">, not other .intro elements.
Multiple classes:
Must have BOTH classes.
<!-- Matched -->
<button class="button primary">
Yes
</button>
<!-- Not matched -->
<button class="button">
No (missing primary)
</button>
<!-- Not matched -->
<button class="primary">
No (missing button)
</button>Adjacent sibling (+):
First paragraph after any h1 gets larger font.
Used for styling lead paragraphs after headings.
Interactive states:
/* Normal state */
a {
color: blue;
text-decoration: none;
}
/* Mouse hover */
a:hover {
color: darkblue;
text-decoration: underline;
}
/* Keyboard focus */
input:focus {
border: 2px solid #3498db;
outline: none;
background: #f0f8ff;
}
/* Button pressed */
button:active {
transform: scale(0.95);
}
/* Visited link */
a:visited {
color: purple;
}
/* Disabled form field */
input:disabled {
background: #f0f0f0;
cursor: not-allowed;
}Browser applies styles based on element state.
No JavaScript needed - pure CSS interactivity.
Structural pseudo-classes:
/* First child */
li:first-child {
font-weight: bold;
}
/* Last child */
li:last-child {
margin-bottom: 0;
}
/* Nth child patterns */
tr:nth-child(odd) {
background: #f8f8f8;
}
tr:nth-child(even) {
background: white;
}
/* Every 3rd item */
li:nth-child(3n) {
color: red;
}
/* Not matching */
p:not(.special) {
color: gray;
}| Row 1 (odd) |
| Row 2 (even) |
| Row 3 (odd) |
| Row 4 (even) |
Alternating row colors without adding classes to HTML.

Specificity calculation:
p { } /* 0-0-1 = 1 */
.class { } /* 0-1-0 = 10 */
#id { } /* 1-0-0 = 100 */
p.class { } /* 0-1-1 = 11 */
#id p { } /* 1-0-1 = 101 */
#id .class p { } /* 1-1-1 = 111 */Format: ID-Class-Element
Higher specificity wins regardless of declaration order (unless !important used).
Override with !important (avoid):
Overrides everything except other !important rules.
Makes CSS hard to maintain - use sparingly.

Specificity battle example:
/* Score: 0-0-1-1 = 11 */
div.content {
color: blue;
}
/* Score: 0-1-0-1 = 101 WINS */
div#main {
color: green;
}
/* Score: 0-0-2-3 = 23 */
body div.content.highlight {
color: red;
}Green wins despite being in middle.
Common misconceptions:
11 classes (110) beats 1 ID (100)? NO
ID still wins - categories don’t overflow.
Debugging specificity:
Browser DevTools shows:
When specificity fails:
Red wins - !important changes rules.
Only another !important with higher specificity can override.
Common layout problem:
.sidebar {
width: 300px;
padding: 20px;
border: 2px solid gray;
}
.content {
width: 700px;
padding: 20px;
}Container width: 1000px
Expected: Sidebar + Content = 1000px
Actual result: Content wraps to next line
Why? Total width exceeds container.
Hidden calculation:
Sidebar actual width:
Content actual width:
344px + 740px = 1084px > 1000px
Elements don’t fit - layout breaks.
Understanding the Box Model prevents these errors.

Every HTML element is a rectangular box with four areas:
Content - Text, images, or other content Padding - Space inside the border Border - Edge of the element Margin - Space outside to other elements
/* Font family stack */
body {
font-family: -apple-system,
BlinkMacSystemFont,
"Segoe UI",
Helvetica,
Arial,
sans-serif;
}
/* Font sizing */
h1 {
font-size: 32px; /* Fixed pixels */
font-weight: 700; /* Bold */
line-height: 1.2; /* Tight */
}
p {
font-size: 1rem; /* Relative to root */
font-weight: 400; /* Normal */
line-height: 1.6; /* Comfortable */
letter-spacing: 0.02em;
}
/* Text styling */
.lead {
font-size: 1.25rem;
font-weight: 300; /* Light */
color: #555;
text-align: left;
}
/* Text transforms */
.uppercase {
text-transform: uppercase;
letter-spacing: 0.1em;
}Regular paragraph text with comfortable line height for readability. Notice how spacing between lines makes text easier to read.
Lead paragraph with larger, lighter text to create visual hierarchy.
Uppercase with letter spacing
Font weights:
Units:
px: Fixed sizeem: Relative to parentrem: Relative to root%: Percentage of parent
Gradients:
/* Linear gradient */
.hero {
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 100%
);
}
/* Radial gradient */
.spotlight {
background: radial-gradient(
circle at center,
rgba(255,255,255,0.8),
transparent
);
}Shadows:
.card {
box-shadow:
0 4px 6px rgba(0,0,0,0.1),
0 1px 3px rgba(0,0,0,0.08);
}
.text-glow {
text-shadow:
0 0 20px rgba(52,152,219,0.5);
}Layered shadows create depth and hierarchy.

Inline-block combines both:
Cards sit side-by-side but accept width/height/padding like blocks.

Classic layout challenges:
Centering vertically:
/* This doesn't work as expected */
.parent {
height: 400px;
}
.child {
position: relative;
top: 50%; /* Not centered! */
}Equal-height columns:
/* Columns have different heights */
.column {
float: left;
width: 33.33%;
/* No way to match heights */
}Spacing between items:
Why these fail:
Traditional CSS layout designed for documents, not applications:
Modern requirements:
Flexbox designed specifically for these UI patterns.
/* Container becomes flex */
.container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
/* Children flex */
.item {
flex: 1; /* Equal width */
}
.item.wide {
flex: 2; /* Double width */
}
/* Direction control */
.vertical {
display: flex;
flex-direction: column;
}
/* Wrapping */
.gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* Alignment */
.center-everything {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}Main axis control:
justify-content: Horizontal alignmentalign-items: Vertical alignmentgap: Space between itemsPerfect for:
/* Mobile first approach */
.container {
width: 100%;
padding: 10px;
}
.card {
width: 100%;
margin-bottom: 20px;
}
/* Tablet and up */
@media (min-width: 768px) {
.container {
max-width: 750px;
margin: 0 auto;
padding: 20px;
}
.card {
width: 48%;
display: inline-block;
}
}
/* Desktop */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
padding: 40px;
}
.card {
width: 31%;
}
}
/* Print styles */
@media print {
.no-print {
display: none;
}
body {
font-size: 12pt;
color: black;
}
}
Breakpoints:
Mobile-first benefits:
Full programming language features:
// Variables and data types
let userName = "Alice";
const maxAttempts = 3;
let attempts = 0;
let isLoggedIn = false;
// Functions
function validateEmail(email) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
}
// Conditionals
if (attempts >= maxAttempts) {
console.log("Too many attempts");
lockAccount();
} else {
attempts++;
}
// Loops
const items = ["Home", "About", "Contact"];
for (let item of items) {
createNavLink(item);
}
// Objects and arrays
const user = {
name: "Alice",
email: "alice@example.com",
orders: [101, 102, 103]
};JavaScript has everything Python has:

Different from server languages:
JavaScript CANNOT:
JavaScript CAN:
JavaScript modifies HTML after loading:
Initial HTML:
<div id="message">Welcome</div>
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<button id="addBtn">Add Item</button>JavaScript manipulation:
// Get element by ID
const message = document.getElementById('message');
// Change text content
message.textContent = 'Hello, Alice!';
// Change styles
message.style.color = 'blue';
message.style.fontSize = '24px';
// Add CSS class
message.classList.add('highlight');
// Create new element
const newItem = document.createElement('li');
newItem.textContent = 'Item 3';
// Add to existing list
const list = document.getElementById('list');
list.appendChild(newItem);
// Remove element
const firstItem = list.firstElementChild;
firstItem.remove();
// Change HTML content
message.innerHTML = '<strong>Welcome</strong> back!';Page updates immediately without server request.

Common DOM methods:
// Finding elements
getElementById('id')
querySelector('.class')
querySelectorAll('p')
// Modifying elements
element.textContent = 'text'
element.innerHTML = '<b>html</b>'
element.style.property = 'value'
element.classList.add('class')
element.setAttribute('attr', 'val')
// Creating/removing
createElement('tag')
appendChild(element)
removeChild(element)
element.remove()Changes happen instantly in browser. No network request needed.
Code runs in response to user actions:
// Click event
const button = document.getElementById('submitBtn');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
// Do something
});
// Form submission
const form = document.getElementById('loginForm');
form.addEventListener('submit', function(event) {
event.preventDefault(); // Stop page reload
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
if (validateLogin(email, password)) {
submitLogin(email, password);
} else {
showError('Invalid credentials');
}
});
// Input changes
const searchBox = document.getElementById('search');
searchBox.addEventListener('input', function(event) {
const query = event.target.value;
if (query.length > 2) {
fetchSearchResults(query);
}
});
// Mouse events
element.addEventListener('mouseover', highlight);
element.addEventListener('mouseout', unhighlight);
// Keyboard events
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
}
});Making HTTP requests from JavaScript:
// Simple GET request
fetch('/api/users')
.then(response => response.json())
.then(data => {
console.log(data);
updateUserList(data);
})
.catch(error => {
console.error('Error:', error);
});
// POST request with data
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Alice',
email: 'alice@example.com'
})
})
.then(response => response.json())
.then(newUser => {
addUserToList(newUser);
});
// Modern async/await syntax
async function loadUserData() {
try {
const response = await fetch('/api/user/123');
const user = await response.json();
displayUser(user);
// Load related data
const ordersResponse = await fetch(`/api/orders?user=${user.id}`);
const orders = await ordersResponse.json();
displayOrders(orders);
} catch (error) {
showError('Failed to load data');
}
}
// Page stays responsive during fetch
loadUserData(); // Non-blocking
console.log('This runs immediately');
No page reload:
Traditional form submission:
With fetch():
Asynchronous requests prevent UI blocking - user interaction continues during 200ms network round trip.

Browser provides APIs beyond DOM manipulation for storage, networking, and media.
Client-side validation before submission:
const form = document.getElementById('signupForm');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const submitBtn = document.getElementById('submitBtn');
// Real-time email validation
emailInput.addEventListener('input', function() {
const email = this.value;
const emailError = document.getElementById('emailError');
if (!email) {
emailError.textContent = 'Email is required';
emailError.className = 'error';
} else if (!isValidEmail(email)) {
emailError.textContent = 'Invalid email format';
emailError.className = 'error';
} else {
emailError.textContent = 'Valid email';
emailError.className = 'success';
}
updateSubmitButton();
});
// Password strength checker
passwordInput.addEventListener('input', function() {
const password = this.value;
const strength = checkPasswordStrength(password);
const meter = document.getElementById('strengthMeter');
if (strength < 30) {
meter.style.width = '30%';
meter.style.backgroundColor = 'red';
meter.textContent = 'Weak';
} else if (strength < 70) {
meter.style.width = '60%';
meter.style.backgroundColor = 'orange';
meter.textContent = 'Medium';
} else {
meter.style.width = '100%';
meter.style.backgroundColor = 'green';
meter.textContent = 'Strong';
}
});
// Prevent invalid submission
form.addEventListener('submit', function(event) {
if (!validateForm()) {
event.preventDefault();
showErrors();
}
});Benefits of client-side validation:
BUT always validate on server too:
@app.route('/signup', methods=['POST'])
def signup():
email = request.form['email']
password = request.form['password']
# Server must re-validate
if not is_valid_email(email):
return {'error': 'Invalid email'}, 400
if len(password) < 8:
return {'error': 'Password too short'}, 400
# Client validation can be bypassed
# Server validation cannot
Infinite scroll implementation:
let page = 1;
let loading = false;
// Detect scroll near bottom
window.addEventListener('scroll', function() {
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
// Near bottom?
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreContent();
}
});
async function loadMoreContent() {
if (loading) return; // Already loading
loading = true;
showLoadingSpinner();
try {
const response = await fetch(`/api/items?page=${page}`);
const items = await response.json();
if (items.length > 0) {
appendItemsToList(items);
page++;
} else {
showEndOfContent();
}
} catch (error) {
showError('Failed to load more');
}
loading = false;
hideLoadingSpinner();
}Search suggestions:
const searchInput = document.getElementById('search');
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value;
if (query.length < 2) {
hideSuggestions();
return;
}
// Debounce: Wait 300ms after typing stops
searchTimeout = setTimeout(() => {
fetchSuggestions(query);
}, 300);
});
async function fetchSuggestions(query) {
const response = await fetch(`/api/search?q=${query}`);
const suggestions = await response.json();
displaySuggestions(suggestions);
}Tab switching without reload:
// Tab implementation
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('.panel');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
const targetId = this.dataset.panel;
// Hide all panels
panels.forEach(p => p.classList.remove('active'));
// Remove active from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Show selected panel
document.getElementById(targetId).classList.add('active');
this.classList.add('active');
});
});Profile content shown here. Click tabs to switch content without page reload.
Modal dialog:
const modal = document.getElementById('modal');
const openBtn = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');
openBtn.addEventListener('click', () => {
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
});
closeBtn.addEventListener('click', () => {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
});
// Close on outside click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});Drag and drop interface:
let draggedItem = null;
// Make items draggable
document.querySelectorAll('.draggable').forEach(item => {
item.draggable = true;
item.addEventListener('dragstart', function(e) {
draggedItem = this;
this.style.opacity = '0.5';
});
item.addEventListener('dragend', function(e) {
this.style.opacity = '';
});
});
// Set up drop zones
document.querySelectorAll('.dropzone').forEach(zone => {
zone.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.backgroundColor = '#e8f4ff';
});
zone.addEventListener('dragleave', function(e) {
this.style.backgroundColor = '';
});
zone.addEventListener('drop', function(e) {
e.preventDefault();
if (draggedItem) {
this.appendChild(draggedItem);
this.style.backgroundColor = '';
updateOrder();
}
});
});Drag tasks here
Without JavaScript, every interaction would require full page reload.

SPA routing example:
// Simple client-side router
class Router {
constructor() {
this.routes = {};
window.addEventListener('popstate', () => this.handleRoute());
}
addRoute(path, handler) {
this.routes[path] = handler;
}
navigate(path) {
window.history.pushState({}, '', path);
this.handleRoute();
}
handleRoute() {
const path = window.location.pathname;
const handler = this.routes[path] || this.routes['/404'];
handler();
}
}
// Usage
const router = new Router();
router.addRoute('/', () => loadHomePage());
router.addRoute('/about', () => loadAboutPage());
router.addRoute('/contact', () => loadContactPage());
// Navigation without page reload
document.querySelectorAll('a[data-route]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
router.navigate(e.target.href);
});
});Trade-offs:
JavaScript capabilities shown:
What if malicious code tried to:
The security problem:
Every website runs JavaScript:
If JavaScript had system access:
The solution: Browser Sandbox
JavaScript runs in isolated environment:
Browser enforces these limits - not optional.

Sandbox prevents malicious code from:
Anyone can view and modify JavaScript:
Original code in your file:
// api-key.js
const API_KEY = 'sk-abc123secret456';
const ADMIN_PASSWORD = 'supersecret';
function checkPremiumUser() {
return user.subscription === 'premium';
}
function calculatePrice(items) {
let total = 0;
for (let item of items) {
total += item.price * item.quantity;
}
// Apply secret discount
if (user.vip) {
total *= 0.5; // 50% off for VIPs
}
return total;
}
async function processPayment(amount) {
const response = await fetch('/api/payment', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({ amount })
});
return response.json();
}User opens DevTools:

What NOT to put in JavaScript:
Server must enforce all rules:
@app.route('/api/payment', methods=['POST'])
def payment():
# Server-side validation
amount = request.json['amount']
user = get_current_user()
# Recalculate on server
real_amount = calculate_price_server_side()
if amount != real_amount:
return {'error': 'Invalid amount'}, 400
# Process payment with server-side API key
process_payment(real_amount, SERVER_API_KEY)
JavaScript blocks rendering:
<!-- Blocks HTML parsing -->
<script src="large-library.js"></script>
<p>This won't appear until JS loads</p>
<!-- Better: Async loading -->
<script src="large-library.js" async></script>
<p>This appears immediately</p>
<!-- Better: Defer until DOM ready -->
<script src="app.js" defer></script>Bundle size matters:
// Bad: Including entire library
import _ from 'lodash'; // 70KB
const unique = _.uniq([1, 2, 2, 3]);
// Good: Import only needed function
import uniq from 'lodash/uniq'; // 2KB
const unique = uniq([1, 2, 2, 3]);Mobile performance:
Desktop with fast CPU:
Mobile with slow CPU:
Critical rendering path:
// Bad: Runs immediately, blocks page
for (let i = 0; i < 1000000; i++) {
// Heavy computation
}
// Good: Defer heavy work
requestIdleCallback(() => {
// Heavy computation when idle
});
// Good: Web Worker for heavy tasks
const worker = new Worker('heavy-task.js');
worker.postMessage({cmd: 'start'});Mobile users on slow connections suffer most from large JavaScript bundles.

Remember: Client-side JavaScript is for user experience, not security.
All security enforcement must happen on the server where users cannot modify the code.
HTTP request contains URL path:
Web server receives request on port 80 (HTTP) or 443 (HTTPS).
Server extracts path component: /docs/api.html
Path resolution algorithm:
document_root = /var/www/html
requested_path = /docs/api.html
file_path = document_root + requested_path
= /var/www/html/docs/api.html
Server attempts to open file at computed path.
If file exists:
If file doesn’t exist:
No code execution occurs between request and response.
Server performs pure I/O operation:
This mapping is deterministic - same URL always maps to same file path.

URL components:
http://example.com/docs/api.html ← used for file mappingCommon document roots:
/var/www/html/usr/share/nginx/htmlproject_directory/static/Security consideration:
Path traversal attempts blocked:
GET /../../../etc/passwd HTTP/1.1
Server validates path doesn’t escape document root.
Flask project structure:
project/
app.py
templates/
index.html
results.html
static/
styles.css
app.js
images/
logo.png
Flask establishes two conventions:
templates/ directory:
Contains HTML files returned by route functions:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/results')
def results():
# This could pass data to template
return render_template('results.html')Route / executes index() function. Function returns contents of templates/index.html.
static/ directory:
Contains CSS, JavaScript, images, data files.
No route function needed - Flask automatically maps:
/static/styles.css → file static/styles.css/static/images/logo.png → file static/images/logo.pngHTML references static files:

Development server behavior:
Flask development server handles both:
Production deployment:
Nginx serves static files directly:
location /static {
alias /path/to/project/static;
expires 1y;
}
location / {
proxy_pass http://flask_app;
}Static files bypass Flask entirely in production.
Initial HTML request:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/main.css">
<link rel="stylesheet" href="/static/theme.css">
<script src="/static/vendor.js"></script>
<script src="/static/app.js"></script>
</head>
<body>
<img src="/static/logo.png">
<img src="/static/hero.jpg">
</body>
</html>Browser parses HTML sequentially.
Each external resource reference triggers new HTTP request.
Request sequence:
<head>, finds two <link> tags<script> tags<body>, finds <img> tagsCSS may reference additional resources:
/* main.css */
@font-face {
font-family: 'Custom';
src: url('/static/fonts/custom.woff2');
}
body {
background: url('/static/bg.jpg');
}Total: 9 HTTP requests to display one page.

Browser constraints:
Performance implications:
Each file requires:
Optimization strategies:
User visits three pages in sequence:
/login.html
/dashboard.html
/settings.html
Static file limitations:
Page 1: /login.html
<form action="/authenticate" method="POST">
<input name="username">
<input name="password">
<button>Login</button>
</form>Form submission fails - no /authenticate handler exists.
Page 2: /dashboard.html
Server has no memory of login attempt. No way to identify user.
Page 3: /settings.html
Each request independent - no connection between pages.
HTTP statelessness prevents session tracking:
Each request stands alone - no built-in connection between requests.
Static server processes requests independently:
No mechanism to:

Session mechanism (requires code execution):
# Login endpoint
@app.route('/authenticate', methods=['POST'])
def authenticate():
username = request.form['username']
password = request.form['password']
user = verify_credentials(username, password)
if user:
session_id = generate_token()
store_session(session_id, user.id)
response = redirect('/dashboard')
response.set_cookie('session', session_id)
return response
# Dashboard with session
@app.route('/dashboard')
def dashboard():
session_id = request.cookies.get('session')
user_id = lookup_session(session_id)
user_data = get_user_data(user_id)
return render_template('dashboard.html',
user=user_data)Static serving cannot execute any of this logic.
User searches for “laptop under 1000”
Search requires:
Breaking down the search:
Text parsing:
SQL query construction:
SELECT * FROM products
WHERE (name LIKE '%laptop%'
OR description LIKE '%laptop%')
AND price < 1000
ORDER BY relevance DESC
LIMIT 20 OFFSET 0Consider search variations:
Each variation requires different query logic.
Pagination adds another dimension:
Page 1: OFFSET 0 Page 2: OFFSET 20 Page 3: OFFSET 40
Combined with search terms: infinite URL combinations.
Relevance scoring:
SELECT *,
(CASE WHEN name LIKE '%laptop%' THEN 10 ELSE 0 END +
CASE WHEN brand = 'Dell' THEN 5 ELSE 0 END +
CASE WHEN description LIKE '%laptop%' THEN 3 ELSE 0 END)
AS relevance
FROM products
WHERE ...
ORDER BY relevance DESCRequires computation impossible in static files.

Query variations multiply infinitely:
Search terms × Price ranges × Brands × Specifications × Sort orders × Pages
“laptop” → 47 results “gaming laptop” → 12 results “dell laptop” → 23 results “laptop 16gb” → 8 results
Each requires different SQL query.
Cannot pre-generate HTML for every possible search.
The form paradox:
Static HTML can create forms:
<!-- In register.html (static file) -->
<form action="/api/register" method="POST">
<input name="email" type="email">
<input name="password" type="password">
<button>Sign Up</button>
</form>But static server cannot:
Form data requires processing.
Static files only serve - they cannot compute.
Form data flow:
action URLThe transformation pipeline:
Static HTML Form
↓ (user input)
Browser Encoding
↓ (POST request)
Server Routing
↓ (to application)
Data Processing
↓ (validation, storage)
Response Generation
Understanding this flow explains why static sites need API backends for any user interaction.
Registration form submission lifecycle:
Browser sends raw form data:
POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email=alice%40example.com&password=secretpass123&
country=US&newsletter=on
Step 1: Parse URL encoding
# URL encoded string
raw = "email=alice%40example.com&password=secretpass123"
# Parse into dictionary
data = {
'email': 'alice@example.com', # %40 decoded to @
'password': 'secretpass123',
'country': 'US',
'newsletter': 'on'
}Step 2: Validate each field
errors = []
# Email validation
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
errors.append('Invalid email format')
# Password strength
if len(data['password']) < 8:
errors.append('Password too short')
# Country code check
if data['country'] not in VALID_COUNTRIES:
errors.append('Invalid country')Step 3: Business logic checks
# Check email uniqueness
existing = db.execute(
"SELECT id FROM users WHERE email = ?",
data['email']
).fetchone()
if existing:
return error("Email already registered")
# Rate limiting
recent = db.execute(
"SELECT COUNT(*) FROM users
WHERE ip_address = ?
AND created_at > datetime('now', '-1 hour')",
request.remote_addr
).fetchone()[0]
if recent > 5:
return error("Too many registrations")
Step 4: Password transformation
# Never store plain text
plain_password = data['password']
# Generate salt and hash
salt = bcrypt.gensalt(rounds=12)
password_hash = bcrypt.hashpw(
plain_password.encode('utf-8'),
salt
)
# Result: $2b$12$KIXx...QW8e (60 bytes)Step 5: Database insertion
user_id = db.execute(
"""INSERT INTO users
(email, password_hash, country,
newsletter, created_at, ip_address)
VALUES (?, ?, ?, ?, datetime('now'), ?)""",
data['email'], password_hash, data['country'],
data['newsletter'] == 'on', request.remote_addr
).lastrowidEach step requires computation unavailable in static serving.
Documentation sites work well with static serving:
<!DOCTYPE html>
<html>
<head>
<title>API Reference - Model Inference</title>
<link rel="stylesheet" href="/static/docs.css">
<link rel="stylesheet" href="/static/syntax.css">
</head>
<body>
<nav>
<a href="/docs/quickstart.html">Quick Start</a>
<a href="/docs/authentication.html">Authentication</a>
<a href="/docs/endpoints.html">Endpoints</a>
<a href="/docs/errors.html">Error Codes</a>
</nav>
<main>
<h1>POST /api/v1/predict</h1>
<p>Submit input data for model inference.</p>
<h2>Request Format</h2>
<pre><code class="language-json">{
"model_id": "resnet50-v2",
"inputs": [[0.1, 0.2, ...], ...],
"parameters": {
"temperature": 0.7,
"top_k": 10
}
}</code></pre>
<h2>Response Format</h2>
<pre><code class="language-json">{
"predictions": [0.92, 0.03, ...],
"model_version": "2.1.0",
"inference_time_ms": 47
}</code></pre>
</main>
</body>
</html>Why static works here:
Deployment simplicity:
Blog with static site generator:
# _posts/2024-01-15-model-deployment.md
---
title: "Deploying ML Models at Scale"
date: 2024-01-15
author: "Dr. Smith"
tags: [ml, deployment, kubernetes]
---
When deploying models to production...Build process:
# Jekyll/Hugo generates HTML from Markdown
jekyll build
# Creates:
# _site/2024/01/15/model-deployment.html
# _site/index.html (with post list)
# _site/tags/ml.html (tag archive)Generated HTML is static:
<article>
<header>
<h1>Deploying ML Models at Scale</h1>
<time>January 15, 2024</time>
<span>by Dr. Smith</span>
</header>
<p>When deploying models to production...</p>
<footer>
Tags: <a href="/tags/ml.html">ml</a>,
<a href="/tags/deployment.html">deployment</a>
</footer>
</article>Appropriate because:
Limitations accepted:
Static serving matches use case constraints.
User clicks “View Orders” link:
Browser initiates request sequence:
Phase 1: DNS Lookup (15ms) Browser cache: app.example.com → miss OS cache: miss DNS query: app.example.com → 93.184.216.34
Phase 2: TCP Connection (25ms) Three-way handshake:
Phase 3: TLS Handshake (40ms) Certificate verification Cipher suite negotiation Session key exchange
Phase 4: HTTP Request (5ms)
GET /orders HTTP/1.1
Host: app.example.com
Cookie: session=abc123def456
Phase 5: Server Processing (85ms)
Nginx receives request (1ms)
Routes to Flask application (2ms)
Flask executes route function (82ms):
Phase 6: Response Transfer (30ms) Server sends 15KB HTML document Network transfer at 4Mbps
Total time: 200ms from click to HTML received

Timing breakdown:
Server processing (85ms):
SELECT * FROM orders WHERE user_id = 42Nginx receives all HTTP requests first:
server {
listen 443 ssl;
server_name app.example.com;
# Static files - served directly
location /static {
alias /var/www/app/static;
expires 1y;
add_header Cache-Control "public";
}
# Media uploads - served directly
location /media {
alias /var/www/app/uploads;
expires 30d;
}
# Everything else - proxy to Flask
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Routing decision tree:
GET /orders HTTP/1.1/orders doesn’t match /static or /media/ (catch-all)Flask application receives proxied request:
@app.route('/orders')
def view_orders():
# This code executes
user_id = session.get('user_id')
orders = Order.query.filter_by(
user_id=user_id
).all()
return render_template('orders.html',
orders=orders)Static file requests never reach Flask.

Performance comparison:
| Request Type | Processing Time | Operations |
|---|---|---|
| Static CSS | 0.3ms | Read file from disk |
| Static Image | 0.5ms | Read file from disk |
| Dynamic Page | 85ms | Session + DB + Template |
| API Endpoint | 45ms | Session + DB + JSON |
Static files bypass application code - direct disk-to-network transfer (0.5ms) vs database queries and template rendering (45ms).
HTML response references multiple resources:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/base.css">
<link rel="stylesheet" href="/static/orders.css">
<script src="/static/vendor.js"></script>
<script src="/static/app.js"></script>
</head>
<body>
<img src="/static/logo.png" alt="Logo">
<h1>Your Orders</h1>
<div class="order-list">
<!-- Order data here -->
</div>
</body>
</html>Browser discovers resources progressively:
<link> tag → Request base.css<link> → Request orders.css<script> tags → Request both JS files<img> → Request logo.pngCSS may reference more resources:
/* base.css */
@font-face {
font-family: 'Custom';
src: url('/static/fonts/custom.woff2');
}
body {
background: url('/static/patterns/grid.svg');
}
/* orders.css */
.order-icon {
background: url('/static/icons/package.svg');
}Browser can’t discover these until CSS loads and parses.
Cascade effect: Must load A before discovering B exists.

Connection limit creates “rounds”:
With 6 parallel connections and 9 resources:
Each round adds latency. HTTP/2 multiplexing eliminates this bottleneck.
Browser converts HTML/CSS to visual display:
Step 1: Parse HTML → DOM Tree
Creates tree structure:
div (class="order")
├── h2
│ └── "Order #1234"
└── p
└── "Total: $89.99"
Step 2: Parse CSS → CSSOM
Creates style rules tree.
Step 3: Combine DOM + CSSOM → Render Tree
Each DOM node gets computed styles:
div.order: border, padding inherited from CSSh2: 24px navy textp: Default stylesStep 4: Layout (Reflow)
Calculate exact position and size:
Step 5: Paint
Draw pixels to screen in order:

Performance implications:
JavaScript DOM changes restart pipeline from step 1.
Network tab shows request waterfall:
Name Status Type Size Time
─────────────────────────────────────────────
orders 200 html 15KB 85ms
base.css 200 css 8KB 50ms
orders.css 200 css 5KB 45ms
vendor.js 200 js 45KB 100ms
app.js 200 js 12KB 70ms
logo.png 200 png 3KB 30ms
custom.woff2 200 font 25KB 30ms
grid.svg 200 svg 2KB 15ms
package.svg 200 svg 1KB 15ms
─────────────────────────────────────────────
Total: 111KB 440ms
Waterfall reveals:
Elements tab shows current DOM:
View Source shows:
Elements tab shows (after JavaScript):
JavaScript modified DOM after page load.
Console shows JavaScript activity:
[10:23:45.123] Fetching user data...
[10:23:45.234] API response: 200 OK
[10:23:45.245] Updated 3 elements
[10:23:45.267] Error: Cannot read property
'name' of undefined at line 47Errors appear immediately with file and line number.

What DevTools reveals:
DevTools reveals runtime behavior: