Hand-coding a small website in a weekend, no framework required

A site with four or five pages does not need a framework, a build step or a database. It needs a folder of HTML files, one stylesheet, a few well-prepared images and somewhere that serves files. You can write all of that by hand in a weekend, and you will understand every line of it afterwards, which is the real payoff. This is the plan we would give a friend who knows a little HTML and wants a site for a club, a small business or a portfolio.
The schedule is loose. Friday evening is for planning, Saturday for building, Sunday for testing and publishing.
Friday evening: decide what the pages are for
Pick three to five pages and write one sentence for each saying what a visitor should be able to do there. If you cannot write the sentence, you do not need the page. Say the site is for a small retro computer club:
| Page | File | What a visitor should be able to do |
|---|---|---|
| Home | index.html |
Learn what the club is and when it meets |
| Meetings | meetings.html |
Find the next date, the address and what to bring |
| Projects | projects.html |
See a few member builds with photos |
| About | about.html |
Read who runs it and how to join |
Contact details can live in the footer of every page, so they do not need a page of their own. Sketch each page on paper: a box for the header, a box for the main content, a box for the footer. Ten minutes of pencil saves an hour of moving things around in code.
The folder layout
Keep the structure flat and boring:
club-site/
index.html
meetings.html
projects.html
about.html
css/
style.css
img/
meeting-room.webp
meeting-room.jpg
sitemap.xml
robots.txt
Two habits avoid most beginner bugs. Use lowercase file names with hyphens instead of spaces, because some servers treat About.html and about.html as different files. And use relative paths (css/style.css, about.html) rather than root-relative ones (/css/style.css). Relative paths work when you open the files straight from your disk, and they keep working if the site ends up in a subfolder, which is exactly what happens with a GitHub Pages project site at username.github.io/repository-name/.
Saturday morning: one good page, then copy it
Write index.html first and get it right. Everything else is a copy of it with different content in the middle.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Riverside Retro Computer Club</title>
<meta name="description" content="A small club that meets monthly to repair, restore and use old computers. Visitors welcome.">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header class="site-header">
<a class="site-name" href="index.html">Riverside Retro</a>
<nav>
<ul class="nav-links">
<li><a href="index.html" aria-current="page">Home</a></li>
<li><a href="meetings.html">Meetings</a></li>
<li><a href="projects.html">Projects</a></li>
<li><a href="about.html">About</a></li>
</ul>
</nav>
</header>
<main>
<h1>Old computers, kept running</h1>
<p>We meet on the first Saturday of every month.</p>
</main>
<footer class="site-footer">
<p>Questions? <a href="mailto:club@example.com?subject=Club%20question">Email the club</a>.</p>
</footer>
</body>
</html>
The club name and address above are made up; swap in your own. A few details in that skeleton are doing real work:
lang="en"tells screen readers and translation tools what language the page is in.- The viewport
metatag stops phones from rendering the page at desktop width and shrinking it. Without it, your careful CSS is viewed through the wrong end of a telescope. header,nav,mainandfooterare semantic elements. They look the same as plaindivs, but they tell browsers, screen readers and search engines what each region is. Use oneh1per page for the page's main heading, andh2andh3below it in order.aria-current="page"marks the current link, for assistive technology and for your CSS.
The shared header, without a framework
Plain HTML has no "include" feature, so the header and footer are copied into every page. With four pages that is fine: paste, then move aria-current="page" to the right link on each copy. If you later change a menu item, you change it four times. Once a site grows past a dozen or so pages, that chore is the honest reason to reach for a static site generator, not before.
The stylesheet, on the other hand, is shared properly. Every page links to the same css/style.css, so one edit restyles the whole site. Our lesson on external stylesheets explains how the link element works if it is new to you.
Saturday afternoon: a little CSS goes a long way
Start with a handful of rules that make text comfortable to read, then lay out the header with flexbox:
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #1d1d1f;
background: #fdfcf9;
}
main, .site-header, .site-footer {
max-width: 46rem;
margin: 0 auto;
padding: 1rem;
}
.site-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1.5rem;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
.nav-links a[aria-current="page"] { font-weight: 700; }
img { max-width: 100%; height: auto; }
What each part buys you. box-sizing: border-box makes widths include padding, which keeps layout arithmetic sane. A max-width of about 46rem keeps lines of text at a readable length on a wide monitor. display: flex puts the site name and the menu on one line, justify-content: space-between pushes them to opposite ends, and flex-wrap: wrap lets the menu drop below the name on a narrow phone instead of overflowing. The last rule stops any image from being wider than its container.
For the Projects page, the same idea makes a row of cards that wraps:
.cards { display: flex; flex-wrap: wrap; gap: 1rem; }
.card { flex: 1 1 14rem; }
flex: 1 1 14rem means each card starts at 14rem wide, grows to fill spare space and shrinks when needed. On a phone you get one column; on a laptop, three. No media queries required yet. If you want the background on margins, padding and display, the CSS tutorial builds a header and menu step by step.
Images: size, format and alt text
Images are where small sites get slow. A modern phone photo is often around 4000 by 3000 pixels. If the layout shows it 800 pixels wide, the original carries five times the pixels needed in each direction, which is 25 times the pixel count. Resize before you upload: roughly the largest width the image will display at, maybe double that if you care about sharp results on high-density screens.
MDN's image format guide puts lossy WebP files at 25 to 35 percent smaller than JPEGs of similar visual quality, on average. The picture element lets you offer WebP with a JPEG fallback for any browser that cannot show it:
<picture>
<source srcset="img/meeting-room.webp" type="image/webp">
<img src="img/meeting-room.jpg" alt="Club members repairing a beige desktop PC at a long table"
width="800" height="600" loading="lazy">
</picture>
MDN recommends setting width and height on every image so the browser can reserve the right space before the file arrives, which prevents the page from jumping as images load. loading="lazy" delays images further down the page until the reader scrolls near them; leave it off the first image at the top.
Alt text describes what the image shows for someone who cannot see it. Write what matters in context, in a plain sentence. A purely decorative image gets an empty alt="", which tells screen readers to skip it.
A contact link instead of a form
A contact form needs something on a server to receive the submission, plus spam protection. A mailto: link needs nothing:
<a href="mailto:club@example.com?subject=Club%20question">Email the club</a>
It opens the visitor's mail app with the address and subject filled in (spaces in the subject are written as %20). The trade-off is that the address is visible to scrapers, so use a dedicated address with a good spam filter rather than your personal one.
Sunday morning: test on real phones
Your desktop browser's developer tools have a device mode that resizes the page to phone widths. Use it, but also look at the site on an actual phone, because thumbs and small screens find problems that a mouse does not. To do that before publishing, serve the folder from your computer:
cd club-site
python3 -m http.server 8000
Then open http://<your-computer's-local-IP>:8000 on a phone connected to the same Wi-Fi. For trying small snippets before they go into a page, our online HTML editor is quicker than saving and reloading files, and the JavaScript tutorial covers the next step if you want a little interactivity later.
A short checklist for every page:
- No sideways scrolling at phone width.
- Body text readable without zooming.
- Menu links far enough apart to tap without hitting the neighbor.
- Every image has sensible alt text and does not jump the layout as it loads.
- Every link works, including the
mailto:link. - The W3C markup validator (validator.w3.org) reports no errors.
Sunday afternoon: publish the files
Because the site is only static files, almost any host will do. Some common routes:
- GitHub Pages publishes HTML, CSS and JavaScript files straight from a repository. A repository named
username.github.iobecomes a user site at that address; any other repository can become a project site atusername.github.io/repository-name/. MDN's beginner guide walks through it with drag-and-drop uploads in the browser, no Git commands needed. - Cloudflare Pages and Netlify both host static sites, either connected to a Git repository or from files you upload directly.
- Any web host that serves files. Upload the folder with the host's file manager, SFTP or FTP, and put
index.htmlin the web root.
Whichever you choose, check its current terms and limits on its own site before committing.
A domain of your own
A domain is rented from a registrar, usually by the year. Each host documents which DNS records to set for a custom domain (often a CNAME for www and A or ALIAS records for the bare domain). Follow your host's page exactly, wait for DNS to update, and make sure HTTPS is switched on before you share the address.
Basic SEO, the parts that matter for five pages
Give every page a unique title that says what is on it, and a meta name="description" with a one- or two-sentence summary. MDN notes that search engines use the description to adjust how the page appears in results. Then add a sitemap, an XML file listing your pages with full URLs:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://www.example.com/</loc></url>
<url><loc>https://www.example.com/meetings.html</loc></url>
<url><loc>https://www.example.com/projects.html</loc></url>
<url><loc>https://www.example.com/about.html</loc></url>
</urlset>
Google's documentation asks for absolute URLs in sitemaps and says it ignores the priority and changefreq fields, so leave them out. Point to the sitemap from a one-line robots.txt in the site root:
Sitemap: https://www.example.com/sitemap.xml
Then submit the sitemap in Google Search Console once the domain is live. On Monday, open index.html, read the code top to bottom, and notice that there is nothing in it you cannot explain.
Sources and further reading
- MDN Web Docs, Publishing your website
- MDN Web Docs, The Image Embed element (img) (alt text, width and height, lazy loading)
- MDN Web Docs, The Picture element
- MDN Web Docs, Image file type and format guide (WebP compared with JPEG)
- MDN Web Docs, Standard metadata names (the description field)
- GitHub Docs, What is GitHub Pages?
- Google Search Central, Build and submit a sitemap
