Back to notes

Visitors who type www.example.net should land on example.net. A CloudFront Function handles the 301 redirect at the edge with zero origin calls. This post extends the routing function from post 15, adds two Route 53 records, and updates the distribution’s aliases.

Why CloudFront Functions

Lambda@Edge can do redirects, but it is overkill for this. CloudFront Functions run in every edge location, execute in sub-millisecond time, have no cold starts, and cost about 1/6th the price. They are purpose-built for lightweight request/response transformations like redirects, header manipulation, and URL rewrites.

Lambda@Edge runs in regional edge caches (a subset of locations), has cold starts on the first invocation, and bills per GB-second. For a simple host check and 301, CloudFront Functions are the right tool.

Certificate

The ACM certificate from post 3 already covers *.example.net via the wildcard SAN. The wildcard matches www.example.net, so no certificate changes are needed.

One Function per Event Type

CloudFront allows exactly one function per event type on a cache behavior. The React template already attaches SPARoutingFunction at viewer-request (post 15), and the Astro template attaches SubdirectoryIndexFunction (post 14).

A second viewer-request association is not possible, and a standalone redirect function would replace your routing function instead of running alongside it. The fix is to add the redirect check to the top of the function you already have.

The plain static template from post 13 has no viewer-request function yet. There you can define the redirect as its own AWS::CloudFront::Function with just the www. check below, and attach it with a new FunctionAssociations entry.

The Merged Function

The redirect check goes first, before the routing logic. Here is the updated SPARoutingFunction from the React template.

  SPARoutingFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: !Sub '${ProjectName}SPARouting'
      AutoPublish: true
      FunctionConfig:
        Comment: Redirects www to apex, rewrites non-asset paths to /index.html
        Runtime: cloudfront-js-2.0
      FunctionCode: |
        function handler(event) {
          var request = event.request;
          var host = request.headers.host.value;
          if (host.startsWith('www.')) {
            return {
              statusCode: 301,
              statusDescription: 'Moved Permanently',
              headers: {
                location: { value: 'https://' + host.slice(4) + request.uri }
              }
            };
          }
          var uri = request.uri;
          var isAsset = /\.(html|js|css|png|jpg|jpeg|gif|ico|svg|json|webp|woff|woff2|ttf|eot|map|txt|xml|mp4)$/i.test(uri);
          if (isAsset) {
            return request;
          }
          request.uri = '/index.html';
          return request;
        }

The function checks the Host header first. If it starts with www., the function returns a 301 Moved Permanently response with the Location header pointing to the same path on the apex domain. The host.slice(4) strips the www. prefix.

Returning a response object short-circuits CloudFront. No cache lookup, no origin fetch, no data transfer charges. The redirect happens entirely at the edge.

Apex requests fall through to the SPA routing logic from post 15, unchanged.

Runtime: cloudfront-js-2.0 is the latest CloudFront Functions runtime. It supports ES 6+ syntax and the crypto module. The older cloudfront-js-1.0 runtime works too, but there is no reason to use it for new functions.

AutoPublish: true promotes the function to the LIVE stage on every deploy. Without this, CloudFormation creates a DEVELOPMENT stage function that is not attached to any distribution.

For the Astro template, add the same if (host.startsWith('www.')) block to the top of the SubdirectoryIndexFunction from post 14.

Distribution Update

One change to the CloudFront distribution from post 8: add www.example.net to the Aliases array.

  CloudFrontDistribution:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref DomainName
          - !Sub 'www.${DomainName}'

Aliases now includes both the apex and the www subdomain. CloudFront requires an alias entry for every domain it serves. Without the www alias, CloudFront rejects requests for www.example.net with a 403 before your function ever runs.

The FunctionAssociations on the DefaultCacheBehavior stays exactly as it was in post 15. The function is already attached at viewer-request; only its code changed.

Route 53 Records

Two new alias records point www.example.net to the same CloudFront distribution. One for IPv4, one for IPv6. Same pattern as the apex records in post 12.

  WwwDomainRecord:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Sub 'www.${DomainName}'
      Type: A
      AliasTarget:
        DNSName: !GetAtt CloudFrontDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2

  WwwDomainRecordAAAA:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Sub 'www.${DomainName}'
      Type: AAAA
      AliasTarget:
        DNSName: !GetAtt CloudFrontDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2

Name: !Sub 'www.${DomainName}' creates records for the www subdomain. Route 53 resolves www.example.net to the distribution’s edge IPs, the request hits CloudFront, and the function redirects to the apex.

HostedZoneId: Z2FDTNDATAQYW2 is the fixed AWS constant for CloudFront, same as post 12.

Git Sync Role

The CloudFront Function permissions (CreateFunction, DescribeFunction, GetFunction, UpdateFunction, DeleteFunction, PublishFunction) are already in the Git Sync role from post 8. The Route 53 permissions are in the role from post 4. No role changes needed for this post.