Skip to the content

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

Last updated: By Lukas Brandt9 min readWeb Design Notes

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
Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

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 meta tag 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, main and footer are semantic elements. They look the same as plain divs, but they tell browsers, screen readers and search engines what each region is. Use one h1 per page for the page's main heading, and h2 and h3 below 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 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:

  1. No sideways scrolling at phone width.
  2. Body text readable without zooming.
  3. Menu links far enough apart to tap without hitting the neighbor.
  4. Every image has sensible alt text and does not jump the layout as it loads.
  5. Every link works, including the mailto: link.
  6. 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.io becomes a user site at that address; any other repository can become a project site at username.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.html in 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

Headshot of Lukas Brandt
Lukas Brandt

Lukas keeps the web design corner: beginner HTML, CSS and JavaScript lessons with code you can paste and run. He also writes the general tech notes, usually after untangling something that should have been simple.