├── LICENSE ├── README.md └── lambdaRewrite.js /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | 3 | This is an **AWS Lambda@Edge** function to enable **static website hosting on AWS S3 via CloudFront** with beautiful page URLs without .html suffixes and without requiring other hacks. 4 | 5 | 6 | ## What scenarios are supported? 7 | 8 | - Standard behaviour is adding a suffix like `.html` to URIs not ending width a slash (`/`). 9 | - Append a suffix like `index.html` to origin requests if the non-root request URIs ends with a slash, e.g. `/some/directory/`. 10 | - Redirect requests to non-root URIs ending with a slash (e.g. `/some/directory/`) to the same URI without the trailing slash (`/some/directory`). 11 | 12 | 13 | ## How does it work? 14 | 15 | Build and upload your HTML pages as usual with `.html` suffixes to S3 and access them or link to them without the suffix via CloudFront. 16 | 17 | Whenever CloudFront needs to request an object from the origin (e.g. S3 bucket), this Lambda function will dynamically rewrite the request URI before it is forwarded to the origin. If a redirect is required the Lambda function will respond with the redirect without hitting the origin. 18 | 19 | If the object is already cached by the CloudFront edge this function will not be triggered. 20 | 21 | 22 | ## Configuration options 23 | 24 | ### `suffix` ['string' | '' | false | null] 25 | 26 | **Recommended value:** `.html` 27 | 28 | **Description:** If defined, the given string (e.g. ".html") will be appended to request URIs ending with a suffix-less word. 29 | 30 | **Example:** If the config value is set to `.html` the URI `/some/page` will be requested from the origin as `/some/page.html`. URIs ending in "/" are not affected. 31 | 32 | 33 | ### `appendToDirs` ['string' | '' | false | null] 34 | 35 | **Recommended value:** `index.html` 36 | 37 | **Description:** If defined the given string will be appended to all non-root URIs ending with a slash. 38 | 39 | **Example:** If the value for is set to `index.html` the URI `/some/directory/` will be requested from the origin as `/some/directory/index.html`. 40 | 41 | 42 | ### `removeTrailingSlash` [boolean] 43 | 44 | **Description:** If `true` trailing slashes in a non-root request URI will be removed by responding with a `301 Moved Permanently` redirect to the same URI without the trailing slash. The request will not hit the origin and the response may be cached. 45 | 46 | **Example:** A request to `/some/directory/` will be answered directly by the Lambda function with a redirect to `/some/directory`. 47 | 48 | 49 | > You can use either *appendToDirs* or *removeTrailingSlash* (or none of them). Using both options would not make sense. If `appendToDirs` is defined, `removeTrailingSlash` will be ignored. 50 | 51 | 52 | ## Setup (in short) 53 | 54 | - Create a new Lambda function in the region us-east-1 (North Virginia) and a *Node.js 6.10* environment. 55 | - Adjust the function code [lambdaRewrite.js](./lambdaRewrite.js) and/or its config options as required and upload (or copy-paste) it to your Lambda function. 56 | - Publish the Lambda function as a new version. 57 | - Add trigger "CloudFront", associate with the CloudFront distribution and the distribution's cache behaviour of your choice (e.g. "*") and select "Origin Request" as CloudFront event. 58 | 59 | 60 | ## More information 61 | 62 | Find more information about AWS Lambda@Edge here: https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html 63 | 64 | 65 | ## License 66 | 67 | Do whatever you want. A link back to this [repository](https://github.com/CloudUnder/lambda-edge-nice-urls), comments, feedback, contributions are appreciated, but not required. 68 | 69 | [Cloud Under Ltd](https://cloudunder.io) is a small web engineering company based in Manchester, UK. 70 | -------------------------------------------------------------------------------- /lambdaRewrite.js: -------------------------------------------------------------------------------- 1 | /* Public domain project by Cloud Under (https://cloudunder.io). 2 | * Repository: https://github.com/CloudUnder/lambda-edge-nice-urls 3 | */ 4 | 5 | const config = { 6 | suffix: '.html', 7 | appendToDirs: 'index.html', 8 | removeTrailingSlash: false, 9 | }; 10 | 11 | const regexSuffixless = /\/[^/.]+$/; // e.g. "/some/page" but not "/", "/some/" or "/some.jpg" 12 | const regexTrailingSlash = /.+\/$/; // e.g. "/some/" or "/some/page/" but not root "/" 13 | 14 | exports.handler = function handler(event, context, callback) { 15 | const { request } = event.Records[0].cf; 16 | const { uri } = request; 17 | const { suffix, appendToDirs, removeTrailingSlash } = config; 18 | 19 | // Append ".html" to origin request 20 | if (suffix && uri.match(regexSuffixless)) { 21 | request.uri = uri + suffix; 22 | callback(null, request); 23 | return; 24 | } 25 | 26 | // Append "index.html" to origin request 27 | if (appendToDirs && uri.match(regexTrailingSlash)) { 28 | request.uri = uri + appendToDirs; 29 | callback(null, request); 30 | return; 31 | } 32 | 33 | // Redirect (301) non-root requests ending in "/" to URI without trailing slash 34 | if (removeTrailingSlash && uri.match(/.+\/$/)) { 35 | const response = { 36 | // body: '', 37 | // bodyEncoding: 'text', 38 | headers: { 39 | 'location': [{ 40 | key: 'Location', 41 | value: uri.slice(0, -1) 42 | }] 43 | }, 44 | status: '301', 45 | statusDescription: 'Moved Permanently' 46 | }; 47 | callback(null, response); 48 | return; 49 | } 50 | 51 | // If nothing matches, return request unchanged 52 | callback(null, request); 53 | }; 54 | --------------------------------------------------------------------------------