Production business website

Deploying a Production Business Website for Free — No Frameworks

Advertisements

In Part 1, we built a high-performance homepage using pure HTML and modern CSS.
In Part 2, we expanded that into a full multi-page business website with services, forms, and navigation.

Now in Part 3, we’re taking it to the next level — optimizing performance, improving accessibility, enhancing design polish, and preparing the website for real-world deployment. Let’s finish what we started.

Download the Source Code (HTML & CSS) and Follow up.

Section 1: Performance Engineering

Most business websites score between 40 and 60 on Google’s performance test. Ours is going to score above 95 — using nothing but HTML and CSS. In this section, we’ll make five targeted improvements that together transform a good-looking site into a genuinely fast one. No tools to install, no build process, no complexity. Let’s get into it.

STEP 1 — Modern Image Formats (AVIF / WebP)

Images are almost always the heaviest files on any webpage. JPEG and PNG files that your camera or a stock image site gives you are larger than they need to be. Two modern formats, WebP and AVIF, can shrink the same image by 30 to 70 percent with no visible loss in quality. Every modern browser supports them. The trick is that we can’t just rename a .jpg file to .avif, we need to actually convert it. And we need a fallback for the rare browser that doesn’t support these formats yet. That’s what the <picture> element is for.

Converting Your Images

Go to squoosh.app, it’s Google’s free, browser-based image converter. Browse or drag any image onto the page. On the right panel, change the format to WebP, set the quality to around 50 to 80, and download. Do the same again and choose AVIF. You now have three versions of the same image: the original JPEG, a WebP version, and an AVIF version. Save all three into your images/ folder.

The <picture> Element

Wherever we use an <img> tag for a visible image, we replace it with a <picture> element. The browser reads the sources from top to bottom and picks the first format it understands. We’ll use AVIF first, as it has the smallest file size. Then, WebP is second because it’s widely supported. And the original JPEG/PNG at the bottom as a guaranteed fallback. By doing so, no browser is left behind. The HTML code should look like this.

<picture>
<source srcset="images/about-team.avif" type="image/avif">
<source srcset="images/about-team.webp" type="image/webp">
<img
src="images/about-team.jpg"
alt="The Kmacims team working together in our Lagos office."
width="600"
height="400"
loading="lazy"
>
</picture>

Do this for every <img> tag across all four pages. The pattern is always the same — AVIF source, WebP source, then the original as the fallback <img>. Keep the alt, width, height, and loading attributes on the <img> tag, not on the <picture> element.

Hero Background Image

The hero background image is set in CSS, not in HTML, so the <picture> element won’t work here. Instead, we use a CSS technique. We write two rules stacked on top of each other. Browsers that support AVIF will use it. Browsers that don’t will fall through to the JPEG. It’s the CSS equivalent of the <picture> fallback pattern.

.hero {
/* Modern browsers override with AVIF */
background-image: url('../images/hero-bg.avif');
background-image: url('../images/hero-bg.webp');
/* Fallback for older browsers */
background-image: url('../images/hero-bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
text-align: center;
padding-block: 0;
}

STEP 2 — Resource Hints (preload & preconnect)

When a browser loads a webpage, it discovers resources one by one as it reads through the HTML. By the time it finds your hero image, which is the largest, most visible thing on the page, it’s already been reading for a fraction of a second. Resource hints let us tell the browser in advance: ‘You’re going to need this file. Start fetching it now.’ Two hints matter for our site: preload for the hero image, and preconnect for any external font service. Add the following HTML inside the head tag, after the <title> tag.

Preload the Hero Image

<!-- Inside <head>, just after the <title> tag -->

<!-- Tell the browser to fetch the hero image immediately -->
<link
rel="preload"
as="image"
href="images/hero-bg.avif"
type="image/avif"
>

This one line tells the browser to start downloading the hero image the moment it reads the <head>, long before it reaches the hero section in the body. The result is a faster Largest Contentful Paint. That’s the official name for how quickly the main visible content appears. It’s one of Google’s three core ranking signals. This link tag is only needed on the page that shows the hero image.

Preconnect for Google Fonts (if used)

If your site loads fonts from Google Fonts or any other external service, add these two lines before the font link tag. preconnect opens the network connection to Google’s servers early, so when the font request actually fires, the connection is already warm. It typically saves 100 to 300 milliseconds on the first load.

html
<!-- Add these BEFORE your Google Fonts <link> tag -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

If you’re not using Google Fonts, which is actually the case for our site since we used system fonts, you can skip this step. Not adding unnecessary hints is itself a performance decision.

STEP 3 — Critical CSS

Here’s something most developers don’t realize. That single line, the <link> tag that loads our stylesheet, actually blocks the page from rendering. The browser refuses to show anything on screen until it has fully downloaded and processed styles.css. For most of our file that’s fine. But the styles for the navigation and hero section are needed immediately, before anything else. We can pull those styles out and place them directly in the <head> using a <style> tag. The browser applies them instantly, with zero network delay. Then the rest of the stylesheet loads normally in the background. So, add the following above the fold CSS in the head section of the index.html page as inline CSS.

html
<!-- In <head> of index.html, BEFORE the <link> to styles.css -->
<style>
/* Critical CSS — styles needed before the page is visible */
/* Base Reset */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: system-ui, sans-serif;
line-height: 1.5;
color: #1a1a1a;
}

/* create css variables */

:root {
--primary-color: #2563eb;
--text-color: #1a1a1a;
--bg-color: #ffffff;
--max-width: 1200px;
--spacing: 1rem;
}

/* Style the header and navigation bar */

header {
background: var(--bg-color);
border-bottom: 1px solid #e5e7eb;
}

nav {
max-width: var(--max-width);
margin: auto;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}

.nav-links {
list-style: none;
display: flex;
gap: 1.5rem;
}

.nav-links a {
text-decoration: none;
color: var(--text-color);
font-weight: 500;
}

/* Style logo as an anchor tag */
.logo {
text-decoration: none;
font-size: 1.25rem;
font-weight: 800;
color: var(--text-color);
letter-spacing: -0.02em;
}
/* Style the hero section */

.hero {
background-image: url('../images/hero-bg.avif');
background-image: url('../images/hero-bg.webp');
background-image: url('../images/hero-bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
text-align: center;
padding-block: 0;
}

.hero-overlay {
max-width: var(--max-width);
margin: auto;
padding: 6rem 1rem;
background: rgba(0, 0, 0, 0.55);
}

.hero h1 {
font-size: clamp(2rem, 5vw, 3rem);
margin-bottom: 1rem;
color: white;
}

.hero p {
margin-bottom: 2rem;
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.88);
}

.cta-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 6px;
}

</style>

<!-- Full stylesheet loads after — non-blocking for above-the-fold content -->
<link rel="stylesheet" href="css/styles.css">

Note this practical rule: critical CSS is whatever styles control the navigation and the first visible section, the hero. Everything below the fold, meaning everything the user has to scroll to reach, can wait for the full stylesheet. We’re not duplicating styles here permanently — in a production workflow, tools automate the extraction. But for our site, doing it manually takes five minutes and delivers the same result.

STEP 4 — Fixing Layout Shift Permanently

A layout shift is when the page visibly jumps as images and fonts load in. Google measures it with a score called CLS — Cumulative Layout Shift. A high CLS score hurts your ranking and, more importantly, it makes your site feel broken and unpolished to visitors. We already added width and height to our images in Part 2. Let’s make sure every single image across all pages has them, and add one more CSS rule that makes the fix bulletproof. Open css/styles.css. Scroll to the base, reset at the top, and add the following styles.

css
/* Add to the base reset in styles.css */

img, picture, video, canvas, svg {
display: block;
max-width: 100%;
height: auto;
}

This rule does two things. display: block removes the small gap that appears beneath inline images. This is a subtle but noticeable visual glitch on many sites. height: auto ensures that when we set a width attribute on an image, the height scales proportionally. Together with the width and height attributes on every <img> tag, the browser can now reserve exactly the right amount of space before the image downloads. The page never jumps. Now, do a quick visual check by scanning through index.html, about.html, services.html, and contact.html. Highlight any <img> tag that’s missing the width or height attributes and add them. The <img> tag HTML should look like this.

html
<!-- Every <img> tag should look like this -->
<img
src="images/about-team.jpg"
alt="The KmaCIMS team"
width="600"
height="400"
loading="lazy"
>

Go through every page and confirm that every <img> has both a width and a height. The values don’t have to be the exact pixel dimensions of the file; they just need to reflect the correct aspect ratio. A 1200×800 image and a 600×400 attribute produce the same ratio. The browser uses this ratio to reserve space, not the actual display size.

STEP 5 — Measuring With PageSpeed Insights

Before we finish this section, let’s measure what we’ve actually achieved. PageSpeed Insights is Google’s official tool. It tests your live URL and gives you four scores: Performance, Accessibility, Best Practices, and SEO. We care about all four, but Performance is what this section is about.

Here are the four numbers that matter. After deploying in Section 6, run this test on your own live URL and show your clients the results. A score above 90 puts your site in the top tier of business websites globally. Most agency-built sites using WordPress or React frameworks score in the 40s and 50s. Our site, built with nothing but HTML and CSS, beats them on every metric.

Five improvements. No tools to install, no commands to run, no build process. We converted our images to modern formats, told the browser what to load early, pulled the critical styles out of the network queue, and eliminated layout shift. In the next section, we’ll make the site feel alive — scroll-driven animations using pure CSS, no JavaScript required. Let’s go.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top