Build Real
Websites with
Pure Front-End
A complete, practical guide to building professional static websites — from blank file to live deployment. HTML5 structure, CSS design systems, JavaScript interactivity, forms, SEO, accessibility, and performance. No server required.
§ 01 — Foundations
What Is a Static Site?
A static website serves pre-built HTML, CSS, and JavaScript files directly to the browser — no database queries, no server-side rendering, no PHP or Java processing. The browser does all the work.
✓ Instantly fast — no database round trips
✓ Secure — no server means no server vulnerabilities
✓ Works offline with Service Workers
✓ Easy to version-control, maintain, and deploy
✗ No server-side logic (use client-side JS instead)
✗ No real-time server push (use a WebSocket service or polling)
// 1. Browser requests index.html GET /index.html // 2. Server returns the raw file // No PHP, no processing, no database <html>...your pre-built HTML...</html> // 3. Browser fetches CSS + JS GET /css/main.css GET /js/main.js // 4. JS calls external APIs directly const data = await fetch( 'https://api.weather.com/forecast' ); // 5. Everything renders in the browser // Your "backend" = 3rd party APIs // Auth → Auth0, Firebase, Supabase // Forms → Formspree, Netlify Forms // DB → Supabase, Airtable, Firebase
§ 02 — File Organization
Project Structure
A well-organized project prevents chaos as it grows. Separate folders for CSS, JS, images, and fonts. One CSS file per responsibility. One JS file per feature. A clear naming convention throughout.
/* Import order matters! */ @import 'reset.css'; /* first */ @import 'variables.css'; /* tokens */ @import 'layout.css'; /* structure */ @import 'components.css'; /* UI pieces */
// Modules (type="module" in HTML) import { initNav } from './nav.js'; import { initReveal } from './animations.js'; import { initTheme } from './theme.js'; document.addEventListener('DOMContentLoaded', () => { initNav(); initReveal(); initTheme(); });
type="module" on your script tag so you can use import/export. Modules are deferred automatically and scoped — no global namespace pollution.§ 03 — Document Metadata
The HTML Head
The <head> is invisible to users but critical for SEO, social sharing, browser behavior, and performance. Every element here serves a specific purpose.
<!DOCTYPE html> <html lang="en"> <head> <!-- ENCODING — must be first --> <meta charset="UTF-8"> <!-- VIEWPORT — required for mobile --> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- PAGE TITLE — 50-60 chars, appears in browser tab + search --> <title>Product Name — Tagline | Brand</title> <!-- SEO META TAGS --> <meta name="description" content="One compelling sentence, 150-160 chars."> <meta name="author" content="Your Name"> <meta name="robots" content="index, follow"> <link rel="canonical" href="https://yoursite.com/page"> <!-- OPEN GRAPH — Facebook, LinkedIn, Discord previews --> <meta property="og:type" content="website"> <meta property="og:title" content="Page Title"> <meta property="og:description" content="Social share description"> <meta property="og:image" content="https://yoursite.com/og.jpg"> <meta property="og:url" content="https://yoursite.com/"> <!-- TWITTER CARD --> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:site" content="@yourhandle"> <meta name="twitter:title" content="Page Title"> <meta name="twitter:image" content="https://yoursite.com/og.jpg"> <!-- FAVICONS --> <link rel="icon" href="/favicon.ico"> <link rel="icon" href="/icon.svg" type="image/svg+xml"> <link rel="apple-touch-icon" href="/apple-touch-icon.png"> <!-- FONTS — preconnect reduces DNS lookup time --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> <!-- CSS — always in <head> so it loads before first paint --> <link rel="stylesheet" href="/css/main.css"> <!-- THEME COLOR — browser chrome on mobile --> <meta name="theme-color" content="#0ea5e9"> </head>
§ 04 — Meaningful Markup
Semantic HTML5
Semantic HTML uses elements that describe their meaning rather than their appearance. <nav> instead of <div class="nav">. This helps screen readers, search engines, and future developers understand your page instantly.
<body>
<!-- Keyboard users skip to main content -->
<a href="#main" class="skip-link">Skip to content</a>
<header>
<nav aria-label="Main navigation">
<a href="/" aria-label="Home">
<img src="logo.svg" alt="Brand Name">
</a>
<ul role="list">
<li><a href="/about">About</a></li>
<li><a href="/pricing">Pricing</a></li>
</ul>
</nav>
</header>
<main id="main"> <!-- ONE per page -->
<section id="hero" aria-label="Hero">
<h1>Main headline — ONE h1 per page</h1>
<p>Value proposition.</p>
</section>
<section id="features" aria-labelledby="feat-h">
<h2 id="feat-h">Features</h2>
<article> <!-- self-contained piece -->
<h3>Feature One</h3>
<p>Description.</p>
</article>
</section>
<section id="pricing"> ... </section>
<section id="contact"> ... </section>
</main>
<footer>
<nav aria-label="Footer links"> ... </nav>
<p><small>© 2025 Brand</small></p>
</footer>
<!-- JS at end of body: defer load -->
<script src="/js/main.js" type="module"></script>
</body>
§ 05 — CSS Design Tokens
CSS Architecture
Start every project with a CSS reset, a custom property system (design tokens), and a consistent naming convention. This prevents specificity wars and makes the whole codebase predictable and themeable.
/* ── MODERN CSS RESET ───────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; padding: 0; } html { scroll-behavior: smooth; text-size-adjust: 100%; } body { line-height: 1.6; -webkit-font-smoothing: antialiased; } img, video, svg { display: block; max-width: 100%; } p, h1, h2, h3 { overflow-wrap: break-word; } input, button, textarea, select { font: inherit; } /* ── DESIGN TOKENS ──────────────────────────────────── */ :root { /* Colors — HSL for easy programmatic manipulation */ --color-primary: hsl(210 100% 56%); --color-primary-d: hsl(210 100% 44%); --color-success: hsl(160 84% 39%); --color-danger: hsl(350 89% 60%); --color-bg: hsl(0 0% 100%); --color-surface: hsl(210 20% 98%); --color-text: hsl(222 47% 11%); --color-muted: hsl(215 16% 47%); --color-border: hsl(214 32% 91%); /* Typography */ --font-display: 'Space Grotesk', sans-serif; --font-body: 'Inter', system-ui, sans-serif; --font-mono: 'Fira Code', monospace; /* Fluid type scale (Major Third 1.25×) */ --text-sm: clamp(.8rem, 1.4vw, .875rem); --text-base: clamp(1rem, 1.6vw, 1.063rem); --text-lg: clamp(1.1rem, 2vw, 1.25rem); --text-xl: clamp(1.3rem, 2.5vw, 1.5rem); --text-2xl: clamp(1.6rem, 3vw, 2rem); --text-3xl: clamp(2rem, 4vw, 3rem); /* Spacing — 4px grid */ --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s6:24px; --s8:32px; --s12:48px; --s16:64px; --s24:96px; /* Layout */ --container: 1200px; --radius: 8px; --radius-lg: 16px; --shadow: 0 1px 3px rgba(0,0,0,.06),0 4px 12px rgba(0,0,0,.08); --shadow-lg: 0 8px 32px rgba(0,0,0,.12); /* Transitions */ --ease: cubic-bezier(.4,0,.2,1); --t-fast: 150ms var(--ease); --t-base: 250ms var(--ease); } /* ── UTILITY CLASSES ────────────────────────────────── */ .container { max-width:var(--container); margin:0 auto; padding:0 var(--s4); } .sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); } .text-primary { color:var(--color-primary); } .visually-hidden { /* same as .sr-only */ }
§ 07 — Above the Fold
Hero Section
The hero is the first thing visitors see. You have about 3 seconds. A compelling headline, a single value proposition, and one primary CTA button. The announcement badge + gradient background pattern works across nearly every product type.
Ship Faster with Less Code
The design system that makes building beautiful interfaces feel effortless. Used by 40,000+ developers worldwide.
.hero { min-height: 100svh; /* full viewport */ display: grid; place-items: center; /* center vertically */ text-align: center; padding: var(--s16) var(--s4); background: radial-gradient( ellipse at top, hsl(210 100% 97%), white ); } .hero__badge { display: inline-flex; align-items: center; gap: var(--s2); padding: var(--s1) var(--s3); border: 1px solid hsl(210 100% 80%); border-radius: 100px; background: hsl(210 100% 96%); color: var(--color-primary); font-size: var(--text-sm); margin-bottom: var(--s4); } .hero__title { font-size: var(--text-3xl); /* clamp fluid */ font-weight: 700; line-height: 1.1; letter-spacing: -0.03em; max-width: 16ch; /* ~16 characters wide */ margin: 0 auto var(--s4); } .hero__ctas { display: flex; gap: var(--s3); justify-content: center; flex-wrap: wrap; margin-top: var(--s6); }
§ 08 — Cards and Grids
Feature Cards
Use CSS Grid with auto-fit and minmax() for a layout that's automatically responsive — no media queries needed for the grid itself. Cards go from 3-column on desktop to 1-column on mobile automatically.
Lightning Fast
Optimized for performance from day one. Sub-second load times on any device, anywhere in the world.
Fully Customizable
Design token system lets you brand every component from one CSS file. No overrides needed.
Accessible by Default
WCAG 2.1 AA compliant. Every component ships with correct ARIA roles and keyboard support.
Mobile-First
Built for small screens, enhanced for large ones. Looks perfect at any viewport width.
Secure
No server means no server vulnerabilities. Files served over HTTPS from a CDN edge network.
One-Click Deploy
Push to GitHub and your site deploys automatically to Netlify, Vercel, or GitHub Pages.
/* Auto-responsive — no media queries needed */ .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--s6); /* 3-col desktop → 2-col tablet → 1-col mobile */ } .card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--s6); transition: transform var(--t-base), box-shadow var(--t-base); } .card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--color-primary); }
§ 09 — Social Proof
Testimonials
Testimonials build trust and handle objections. Store them in a JS array and render dynamically — easy to update without touching HTML. Add structured data markup for rich snippets in search.
"We replaced our entire component library in a weekend. The design token system is incredibly thoughtful — our team ships UI twice as fast now."
"The accessibility built into every component saved us hundreds of hours. We passed our WCAG audit on the first try. That never happens."
// Store data as a JS array (or fetch from a JSON file) const testimonials = [ { quote:"We ship UI twice as fast now.", author:"Sarah Lee", title:"Engineer · Stripe", stars:5, initials:"SL", color:"hsl(210,100%,56%)" }, // add as many as you need... ]; function renderTestimonials() { const grid = document.querySelector('.testimonials-grid'); grid.innerHTML = testimonials.map(t => ` <article class="card"> <div class="stars">${'★'.repeat(t.stars)}</div> <blockquote><p>${t.quote}</p></blockquote> <footer> <div class="avatar" style="background:${t.color}">${t.initials}</div> <div> <cite class="name">${t.author}</cite> <small>${t.title}</small> </div> </footer> </article> `).join(''); } renderTestimonials();
§ 10 — Conversion Component
Pricing Table
The three-tier pricing pattern (Starter / Pro / Enterprise) is the industry standard for SaaS products. Highlight the recommended tier. A monthly/annual billing toggle shows value and increases conversions.
Perfect for personal projects and learning.
- 3 projects
- 10GB storage
- Community support
- Basic analytics
For professionals and growing teams.
- Unlimited projects
- 100GB storage
- Priority email support
- Advanced analytics
- Custom domains
For large organizations with custom needs.
- Everything in Pro
- Unlimited storage
- Dedicated support
- SSO / SAML
- SLA guarantee
§ 11 — Frequently Asked Questions
FAQ Accordion
An FAQ section reduces support load and helps SEO (Google often shows FAQ rich results). Build it as an accordion — one item open at a time. Use aria-expanded for screen readers.
§ 12 — Forms Without a Backend
Contact Form
A fully-validated contact form that sends real emails via Formspree. Client-side validation prevents bad submissions. Change the action URL to your Formspree endpoint to activate it.
Message Sent!
We'll get back to you within 24 hours.
<!-- 1. Create free account at formspree.io 2. Create a new form, get your endpoint 3. Paste endpoint as the action attribute --> <form action="https://formspree.io/f/YOUR_FORM_ID" method="POST" novalidate id="contact"> <input type="text" name="name" required> <input type="email" name="email" required> <textarea name="message" required></textarea> <button type="submit">Send</button> </form> // Client-side validation function validate() { const rules = [ { id:'name', test: v => v.trim().length >= 2, msg: 'Name must be at least 2 characters' }, { id:'email', test: v => /^[^@]+@[^@]+\.[^@]+$/.test(v), msg: 'Please enter a valid email address' }, { id:'message', test: v => v.trim().length >= 10, msg: 'Message must be at least 10 characters' }, ]; let valid = true; rules.forEach(rule => { const field = document.getElementById(rule.id); const errEl = document.getElementById('err-' + rule.id); const ok = rule.test(field.value); errEl.textContent = ok ? '' : rule.msg; field.classList.toggle('error', !ok); if (!ok) valid = false; }); return valid; } // Submit with fetch (JSON response, no page reload) form.addEventListener('submit', async e => { e.preventDefault(); if (!validate()) return; const res = await fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }); if (res.ok) showSuccess(); else showError('Something went wrong.'); });
§ 13 — JavaScript for Static Sites
JS Patterns
Five patterns cover 90% of static-site JavaScript: event delegation, the fetch API, the module pattern, debouncing for search/resize, and template literals for dynamic HTML rendering.
// ── EVENT DELEGATION ────────────────────── // One listener on the parent handles all // children — including dynamically added ones document.querySelector('.card-list') .addEventListener('click', e => { const card = e.target.closest('.card'); if (!card) return; handleCard(card.dataset.id); }); // ── FETCH PATTERN ───────────────────────── async function getData(url) { try { const res = await fetch(url); if (!res.ok) throw new Error(res.status); return await res.json(); } catch(err) { console.error('Fetch error:', err); showErrorUI(); return null; } } // ── DEBOUNCE ────────────────────────────── // Prevents firing too often on input/resize function debounce(fn, ms = 200) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } searchInput.addEventListener('input', debounce(e => filterItems(e.target.value)) );
// ── MODULE PATTERN ──────────────────────── // Group related code, avoid global scope const CartModule = (function() { let items = loadFromStorage(); function loadFromStorage() { return JSON.parse( localStorage.getItem('cart') ?? '[]' ); } function save() { localStorage.setItem('cart', JSON.stringify(items)); } return { add(item) { items.push(item); save(); }, remove(id) { items=items.filter(i=>i.id!==id); save(); }, total() { return items.reduce((s,i)=>s+i.price,0); }, count() { return items.length; }, }; })(); // ── LIVE SEARCH / FILTER ────────────────── function filterItems(query) { const q = query.toLowerCase().trim(); document.querySelectorAll('.item').forEach(el => { const text = el.textContent.toLowerCase(); el.style.display = text.includes(q) ? '' : 'none'; }); } // ── TEMPLATE RENDERING ──────────────────── const Card = ({ title, desc, img }) => ` <article class="card"> <img src="${img}" alt="" loading="lazy"> <h3>${title}</h3> <p>${desc}</p> </article> `; container.innerHTML = data.map(Card).join('');
§ 14 — Browser Storage
localStorage & State
Replace a database with the browser's built-in storage. localStorage persists across sessions (5MB). sessionStorage clears when the tab closes. IndexedDB handles large structured data.
// ── STORAGE HELPER ──────────────────────── // Wraps localStorage with JSON + error handling const Store = { get(key, fallback = null) { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } }, set(key, val) { localStorage.setItem(key, JSON.stringify(val)); }, remove(key) { localStorage.removeItem(key); }, update(key, updater, fallback) { const current = this.get(key, fallback); this.set(key, updater(current)); }, }; // Usage examples Store.set('theme', 'dark'); Store.get('theme', 'light'); // → 'dark' Store.update('cart', c => [...c, item], []); // ── WHAT TO STORE ───────────────────────── // ✓ User preferences (theme, font size, lang) // ✓ Form drafts (auto-save on input) // ✓ Shopping cart, favorites, bookmarks // ✓ UI state (sidebar collapsed, tab active) // ✓ Cached API responses (with timestamp) // ✗ Passwords or payment info — never // ✗ Large files — use IndexedDB instead
§ 15 — Entrance Animations
Scroll Animations
IntersectionObserver triggers CSS classes when elements enter the viewport — no scroll event listeners, no jank. GPU-composited opacity and transform ensure 60fps performance.
/* CSS — hidden by default, revealed on scroll */ .reveal { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s ease; } .reveal.in { opacity: 1; transform: translateY(0); } /* Stagger items in a grid */ .card:nth-child(1) { transition-delay: 0ms; } .card:nth-child(2) { transition-delay: 100ms; } .card:nth-child(3) { transition-delay: 200ms; } /* Respect user's motion preference! */ @media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } } // JavaScript — IntersectionObserver const prefersReduced = window .matchMedia('(prefers-reduced-motion: reduce)').matches; if (!prefersReduced) { const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); // animate once only } }); }, { threshold: .15, rootMargin: '0px 0px -40px 0px' }); document.querySelectorAll('.reveal, .reveal-left') .forEach(el => io.observe(el)); } else { // Show everything immediately document.querySelectorAll('.reveal').forEach(el => { el.classList.add('in'); }); }
§ 16 — Color Scheme Toggle
Dark / Light Mode
Detect the OS preference with prefers-color-scheme, allow manual override via a toggle button, and persist the choice in localStorage. Apply the theme in <head> to prevent flash.
Dark Mode
Deep backgrounds, light text. Avoid pure #000 — use dark-gray (#0d1117) for a premium feel. Brand color lightness should be 55-65% to pop against dark surfaces.
Light Mode
Off-white (#f8fafc) backgrounds, very dark text. Brand color needs to be darker (40-50% lightness) to maintain contrast on light surfaces. Shadows are more important here.
/* ── CSS VARIABLES APPROACH ───────────────── */ :root { /* light defaults */ --bg: hsl(210 20% 99%); --text: hsl(222 47% 11%); --surface: hsl(210 20% 97%); --border: hsl(214 32% 91%); --primary: hsl(210 100% 40%); /* darker on light */ } html[data-theme="dark"] { --bg: hsl(222 47% 5%); --text: hsl(210 40% 96%); --surface: hsl(222 47% 8%); --border: hsl(222 47% 16%); --primary: hsl(210 100% 56%); /* brighter on dark */ } * { transition: background-color .3s, color .3s, border-color .3s; } // ── THEME SCRIPT (in <head> prevents flash) ── const theme = localStorage.getItem('theme') ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; // Toggle button click handler document.getElementById('theme-toggle') .addEventListener('click', () => { const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; localStorage.setItem('theme', next); });
§ 17 — Search Engine Optimization
SEO & Open Graph
Three layers: meta tags (page title, description), Open Graph (social share previews), and JSON-LD structured data (rich results in Google). Add all three before launch — retrofitting is painful.
<!-- JSON-LD — Google reads this for rich results -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Your Site Name",
"url": "https://yoursite.com",
"potentialAction": {
"@type": "SearchAction",
"target": "https://yoursite.com/?q={q}",
"query-input": "required name=q"
}
}
</script>
<!-- For a local business: -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Acme Corp",
"telephone": "+1-555-000-0000",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality":"Suwanee",
"addressRegion": "GA",
"postalCode": "30024",
"addressCountry": "US"
}
}
</script>
<!-- FAQ schema — shows as accordion in Google -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{ "@type": "Question",
"name": "Is hosting really free?",
"acceptedAnswer": { "@type":"Answer",
"text": "Yes, GitHub Pages and Netlify..." } }
]
}
</script>
<!-- robots.txt (in site root) -->
User-agent: *
Allow: /
Sitemap: https://yoursite.com/sitemap.xml
<!-- sitemap.xml (in site root) -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yoursite.com/</loc>
<lastmod>2025-07-01</lastmod>
<priority>1.0</priority>
</url>
</urlset>
✓ Test JSON-LD at search.google.com/test/rich-results
✓ Validate your sitemap at Google Search Console
✓ Page title 50-60 chars, description 150-160 chars
§ 18 — Inclusive Design
Accessibility
15% of users have a disability. Accessible sites rank better in search, convert better, and avoid legal liability (ADA and WCAG compliance is legally required for some organizations). The basics take less than an hour.
<!-- SKIP LINK — lets keyboard users jump past nav --> <a href="#main" class="skip-link">Skip to main content</a> /* CSS */ .skip-link { position: absolute; top: -100%; left: 1rem; padding: 8px 16px; background: var(--color-primary); color: white; border-radius: 0 0 8px 8px; z-index: 9999; transition: top .1s; } .skip-link:focus { top: 0; } /* visible on Tab */ <!-- FOCUS STYLES — visible for keyboard navigation --> :focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; } :focus:not(:focus-visible) { outline: none; } <!-- ARIA LABELS ——————————————————————————————— --> <button aria-label="Close dialog">✕</button> <button aria-expanded="false" aria-controls="menu-id">Menu</button> <div aria-live="polite" id="status"></div> <img alt="Descriptive text for screen readers" src="..."> <img alt="" role="presentation" src="decorative.svg"> <!-- ACCESSIBLE FORMS ——————————————————————————— --> <label for="email">Email address</label> <input type="email" id="email" aria-describedby="email-hint" aria-required="true" aria-invalid="false"> <span id="email-hint">We never share your email.</span> <!-- REDUCED MOTION ————————————————————————————— --> @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
✓ Run the axe DevTools Chrome extension — catches 80% of issues automatically
✓ Test at 200% browser zoom — nothing should overflow or break
✓ Check color contrast: minimum 4.5:1 for body text (use the checker in our CSS guide)
§ 19 — Core Web Vitals
Performance
Google uses Core Web Vitals as a ranking signal. A static site naturally wins on server response time — but image format, JS loading order, and font loading still determine your Lighthouse score.
<!-- ── IMAGES: the biggest win ──────────────── --> <img src="hero-800.webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w" sizes="(max-width:640px) 100vw, 800px" width="800" height="450" <!-- prevents layout shift --> loading="lazy" <!-- defer off-screen images --> decoding="async" <!-- off-main-thread decode --> fetchpriority="high" <!-- hero image only --> alt="Hero image description"> <!-- Use Squoosh.app to convert images to WebP/AVIF --> <!-- Rule: hero ≤ 200KB, cards ≤ 50KB, icons = SVG --> <!-- ── JAVASCRIPT ───────────────────────────── --> <script src="main.js" type="module"></script> <!-- type="module" is deferred automatically --> <script src="analytics.js" async></script> <!-- async: execute immediately when fetched --> <!-- ── CSS ──────────────────────────────────── --> <!-- Critical CSS (above-fold styles) inline in head --> <style> /* nav, hero: max 3KB */ </style> <!-- Rest of CSS via link tag --> <link rel="stylesheet" href="styles.css"> <!-- ── FONTS ─────────────────────────────────── --> <link rel="preconnect" href="https://fonts.googleapis.com"> <!-- OR self-host for fastest loading: --> <style> @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; <!-- shows fallback, then swaps --> unicode-range: U+0000-00FF; /* Latin only */ } </style>
§ 20 — Going Live
Deployment
Three excellent free platforms for static sites — each includes HTTPS, a global CDN, and automatic deployment on every git push. Choose based on what you need beyond hosting.
Sign up at netlify.com — no credit card
Free account. Connect to GitHub, GitLab, or Bitbucket. Or drag-and-drop your project folder directly onto the Netlify dashboard for an instant deploy.
New Site → Import from Git
Select your repository. Netlify detects it's a static site. Leave build command empty (unless you use a build tool like Vite). Set publish directory to / for root-level projects.
Click Deploy Site
Your site is live at a Netlify subdomain (project.netlify.app) in under 60 seconds. HTTPS is automatic via Let's Encrypt.
Add your custom domain
Site Settings → Domain Management → Add custom domain. Point your domain's DNS to Netlify's nameservers. SSL certificate provisions automatically.
Auto-deploy is now active
Every git push to main triggers a new deploy. Pull requests get preview URLs automatically — perfect for client review before going live.
✓ Netlify Forms: 100 submissions/month free — replaces Formspree!
✓ Netlify Functions: 125k serverless invocations/month free
✓ Split testing, analytics, edge functions all available
Push your site to a public GitHub repo
Create a repository at github.com and push your HTML/CSS/JS files. The repository name can be anything for a project site, or username.github.io for your root domain.
Enable GitHub Pages
Repository → Settings → Pages → Source: Deploy from a branch → Branch: main → Folder: / (root) → Save.
Your site is live
After ~60 seconds: username.github.io/repo-name. HTTPS is included. No build step for plain HTML/CSS/JS.
Custom domain (optional)
Add a file called CNAME to your repo root containing just your domain: www.yourdomain.com. Then add a CNAME DNS record pointing to username.github.io. SSL provisions automatically.
✓ Directly integrated with GitHub — no extra account needed
✓ Perfect if you already use GitHub for version control
Sign up at vercel.com with GitHub
Free Hobby plan: unlimited projects, 100GB bandwidth/month, automatic HTTPS. One click to authorize Vercel to access your GitHub repositories.
New Project → Import your repo
Click "New Project" → select your GitHub repository → Framework Preset: "Other" for plain HTML/CSS/JS → Deploy. Done in 30 seconds.
Instant global CDN
Vercel's edge network serves your files from 100+ locations worldwide. Users in Tokyo, London, and São Paulo all get sub-100ms response times.
Every PR gets a preview URL
Vercel automatically creates a unique preview deployment for every pull request. Share the URL with clients for review before merging to production.
✓ Preview deployments for every pull request automatically
✓ Vercel Analytics: 2,500 page views/day free
✓ Serverless Functions: 100GB-hours/month free