Creating a website that is blazing fast, cost-efficient, lightweight, and fully secure (meeting all Mozilla web security requirements) may seem straightforward at first. Yet, the number of high-profile websites that miss the mark proves it’s more challenging than it looks. The good news? With the right constraints and a clear strategy, it’s entirely achievable. By focusing on static content and delegating any dynamic behaviour to an Edge API (more on that here), you can strike the perfect balance between performance, cost-effectiveness, and robust security.

Here is an example of one we rolled out for a client last week.

Key Constraints

To simplify the process and maximise results, I set the following hard constraints:

1️⃣ Static HTML only: Fully cacheable content ensures top speed and eliminates request-handling compute costs.

2️⃣ No heavy client-side frameworks: Stick to native JavaScript for lightweight, fast-loading code.

3️⃣ Dynamic behaviour via an Edge API: If dynamic content is needed, deploy a separate Edge API alongside the site.

While this approach won’t work for every project, it’s ideal for lightweight projects, proof-of-concepts, microsites, and similar use cases. The key is to optimise for load time and security.

Step-by-Step Guide

1️⃣ Choose a Templating Framework

Static HTML doesn’t mean duplicating code for every page. A build-time templating framework like Handlebars can help manage reusable components such as headers, footers, or post lists. Alternatively, you can use a lightweight, custom solution that fits your workflow—personally, I rely on a set of bash scripts I wrote years ago.

To streamline the process, set up your templating to run automatically on every code commit. For example, use a CI/CD tool like GitHub Actions or AWS CodeBuild to generate your HTML (and CSS, if you’re using a preprocessor like Sass). This ensures your site is always up-to-date and ready for deployment.

2️⃣ Upload Assets to S3

Store all HTML, CSS, JS, and image assets in an S3 bucket:

Use Terraform's aws_s3_object resource and the fileset function to handle uploads dynamically. Assign the appropriate Content-Type header to each file type for proper browser handling.

Example:

resource "aws_s3_object" "html" {
  for_each = fileset("${path.module}/html", "*.html")
  bucket = aws_s3_bucket.web.bucket
  key = each.value
  source = "${path.module}/html/${each.value}"
  content_type = "text/html"
  etag = filemd5("${path.module}/html/${each.value}")
}

3️⃣ Configure CloudFront for Global Delivery

Set the S3 bucket as the origin for a CloudFront distribution.

Create a bucket policy granting CloudFront S3:GetObject access with the cloudfront.amazonaws.com service principal.

Use aws_cloudfront_origin_access_control with signing_behavior set to "always" and signing_protocol set to "sigv4". Attach it to the origin in CloudFront.

Key Points:

Set both your domain (e.g., example.com) and www.example.com as aliases for the distribution.

Deploy an ACM certificate for HTTPS and set the viewer_protocol_policy to "redirect-to-https".

4️⃣ Add CloudFront Functions for Security and Redirects

Viewer-Request Function: Redirect all example.com/path requests to www.example.com/path with a 301 status. (Or the other way around, if you choose, but it is important to serve all your content from a consistent URL, for SEO purposes)

Example function:

async function handler(event) {
  const request = event.request;
  const headers = request.headers;

  if (headers?.host?.value && headers.host.value.indexOf('www') === -1) {
    if (request.uri === '/') {
      request.uri = '/index.html';
    }

    return {
      statusCode: 301,
      statusDescription: "Moved Permanently",
      headers: {
        location: {
          value: https://www.${headers.host.value}${request.uri}
        }
      }
    };
  }

  if (request.uri === '/') {
    return {
      statusCode: 301,
      statusDescription: "Moved Permanently",
      headers: {
        location: {
          value: https://${headers.host.value}/index.html
        }
      }
    };
  }

  return request;
}

Viewer-Response Function: Set security headers to satisfy Mozilla’s Observatory requirements.

Example Headers:

  • Strict-Transport-Security (HSTS)
  • Content-Security-Policy
  • X-Content-Type-Options
  • X-Frame-Options
  • X-XSS-Protection
  • Referrer-Policy

Example function:

const csp_header = {
  "default-src": ["'self'"],
  "img-src": ["'self'"],
  "script-src": ["'self'"],
  "style-src": [
    "'self'",
    "https://fonts.googleapis.com"
  ],
  "frame-ancestors": ["'none'"],
  "font-src": [
    "'self'",
    "https://fonts.gstatic.com"
  ]
}
async function handler(event) {
  const response = event.response;
  const headers = response.headers;
  headers['strict-transport-security'] = {
    value: 'max-age=63072000; includeSubdomains; preload'
  };
  headers['content-security-policy'] = {
    value: Object.keys(csp_header).map(
      key => ${key} ${csp_header[key].join(' ')}
    ).join('; ')
  };
  headers['x-content-type-options'] = {
    value: 'nosniff'
  };
  headers['x-frame-options'] = {
    value: 'DENY'
  };
  headers['x-xss-protection'] = {
    value: '1; mode=block'
  };
  headers['referrer-policy'] = {
    value: 'same-origin'
  };
  return response;
}

5️⃣ Optimise Assets

Resize images to the largest practical size for both desktop and mobile devices. For responsive sites, consider serving separate image sets for different screen sizes. Set long cache expiry times (e.g., 1 year or longer) for stable assets to keep them cached within the CDN.

💡 Pro Tip: Cache-bust assets when updated by creating Cloudfront invalidations.

Why This Works

  • 🌍 Blazing Fast: Static content served from CloudFront ensures global low-latency delivery.
  • 💸 Cost-Effective: No compute costs (apart from the lightweight Cloudfront functions)—just storage and data transfer.
  • 🔒 Fully Secure: Satisfies all Mozilla web security guidelines.
  • ⚡ Lightweight: Minimal dependencies mean faster load times and easier debugging.

This approach is a game-changer for small websites, microsites, and POCs where speed, cost, and security are paramount.

👉 If you need more details about this stack, feel free to reach out to us — we'll be more than happy to share the code with you! 😊💻