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 14, 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 4 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 Astro template already attaches SubdirectoryIndexFunction at viewer-request (post 14), and the React template attaches SPARoutingFunction (post 15).
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::Functionwith just thewww.check below, and attach it with a newFunctionAssociationsentry.
A www-to-apex redirect only applies to an apex domain. The Astro and React example templates both run on an apex and merge this block into their existing function. A SPA deployed on a subdomain has no www to redirect, so it skips the block.
The Merged Function
The redirect check goes first, before the routing logic. Here is the updated SubdirectoryIndexFunction from the Astro template.
SubdirectoryIndexFunction:
Type: 'AWS::CloudFront::Function'
Properties:
AutoPublish: true
Name: !Sub '${AWS::StackName}-IndexRewrite'
FunctionConfig:
Comment: 'Redirects www to apex, then appends index.html to subdirectory requests'
Runtime: cloudfront-js-2.0
FunctionCode: |
function handler(event) {
var request = event.request;
var host = request.headers.host.value.toLowerCase();
if (host.startsWith('www.')) {
var qs = '';
var keys = Object.keys(request.querystring);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var entry = request.querystring[key];
if (entry.multiValue) {
for (var j = 0; j < entry.multiValue.length; j++) {
qs += (qs ? '&' : '?') + key + (entry.multiValue[j].value ? '=' + entry.multiValue[j].value : '');
}
} else {
qs += (qs ? '&' : '?') + key + (entry.value ? '=' + entry.value : '');
}
}
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: 'https://' + host.slice(4) + request.uri + qs }
}
};
}
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
The function lowercases the Host header (it is case-insensitive) and checks it first. If it starts with www., the function returns a 301 Moved Permanently response pointing to the same path on the apex domain. The host.slice(4) strips the www. prefix, and the query string is rebuilt from request.querystring and appended so parameters like ?utm_source=... survive the redirect. In a CloudFront Function request.uri is the path only, without the query string, so skipping this step would silently drop every query parameter.
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 subdirectory index-rewrite logic from post 14, 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.
The React template’s
SPARoutingFunctionfrom post 15 takes the sameif (host.startsWith('www.'))block at its top, ahead of the SPA-routing logic.
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 14. 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
No role changes needed for this post. The CloudFront Function permissions (CreateFunction, DescribeFunction, GetFunction, UpdateFunction, DeleteFunction, PublishFunction) and the Route 53 permissions are already in the complete Git Sync role from post 3.