├── CHANGELOG.md ├── LICENSE ├── README.md ├── code.js ├── index.html ├── prerender-cloudfront.yaml └── testimage.png /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | The runtime parameter of nodejs6.10 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (nodejs8.10) while creating or updating functions. (Service: AWSLambdaInternal; Status Code: 400; Error Code: InvalidParameterValueException; Request ID: 579f3922-7baa-11e9-adaf-11ff68a3ace1) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brian Sutherland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | prerender.io cloudfront example middleware 2 | == 3 | 4 | This is an example of integrating prerender.io and a SPA in S3 served 5 | over Cloudfront. 6 | 7 | Instructions 8 | -- 9 | 10 | 1. Upload prerender-cloudfront.yaml to Cloudformation as a new stack, 11 | that'll setup the example for you. Enter your prerender.io token when 12 | asked. 13 | 2. Upload code.js and index.html to the S3 bucket created by 14 | Cloudformation in step 1. 15 | 3. Change the read permissions for code.js and index.html to be publicly 16 | readable. 17 | 18 | Testing 19 | -- 20 | 21 | To see the page as rendered by prerender.io run: 22 | 23 | curl -H 'User-Agent: Facebot' https://${CLOUDFRONT_DOMAIN}/over/here 24 | 25 | The same page without pre-rendering: 26 | 27 | curl https://${CLOUDFRONT_DOMAIN}/over/here 28 | 29 | Implementation 30 | -- 31 | 32 | Two Lambda@edge functions are used. The first detects bot requests on 33 | requests entering the system, it sets a header which Cloudfront uses to 34 | partition the cache. The second function, run after the cache, detects 35 | the presence of the header and, if present, routes the request to 36 | Prerender.io 37 | 38 | Caching 39 | -- 40 | 41 | By default, static resources from the bucket are cached for a long time 42 | period. This improves performance but means that the deploy process of 43 | any real app will need a Cloudfront purge step. 44 | 45 | As prerender.io does NOT want any Cloudfront caching (see 46 | https://github.com/prerender/prerender/issues/93#issuecomment-366774910) 47 | we disable that by including a X-Prerender-Cachebuster header which 48 | effectively disables cloudfront caching. 49 | 50 | NOTE: using X-Prerender-Cachebuster is probably not optimal, if you find 51 | a better way, please let me know. 52 | -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var current = window.location.pathname; 3 | var paths = ['/', '/index.html', '/somewhere', '/anywhere', '/over/here', '/over/there']; 4 | if (paths.indexOf(current) === -1) { 5 | content = '

404 Not Found

'; 6 | } else { 7 | content = '

Welcome to the ' + current + ' page.

\n'; 8 | } 9 | content = content + '

\n'; 14 | content = content + ''; 15 | document.getElementById('content').innerHTML = content; 16 | document.getElementById('header').innerHTML = 'Simplest SPA ever'; 17 | })(); 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Loading from JavaScript...

8 |
loading from javascript...
9 | 10 | 11 | -------------------------------------------------------------------------------- /prerender-cloudfront.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | PrerenderToken: 3 | Type: String 4 | Resources: 5 | WebBucket: 6 | Type: "AWS::S3::Bucket" 7 | Properties: 8 | AccessControl: PublicRead 9 | WebsiteConfiguration: 10 | ErrorDocument: index.html 11 | IndexDocument: index.html 12 | LambdaEdgeExecutionRole: 13 | Type: AWS::IAM::Role 14 | Properties: 15 | AssumeRolePolicyDocument: 16 | Version: '2012-10-17' 17 | Statement: 18 | - Effect: Allow 19 | Principal: 20 | Service: 21 | - lambda.amazonaws.com 22 | - edgelambda.amazonaws.com 23 | Action: 24 | - sts:AssumeRole 25 | Policies: 26 | - PolicyName: logging 27 | PolicyDocument: 28 | Version: 2012-10-17 29 | Statement: 30 | - Resource: "*" 31 | Effect: Allow 32 | Action: 33 | - "logs:CreateLogGroup" 34 | - "logs:CreateLogStream" 35 | - "logs:PutLogEvents" 36 | SetPrerenderHeader: 37 | Type: "AWS::Lambda::Function" 38 | Properties: 39 | Handler: "index.handler" 40 | Role: 41 | Fn::GetAtt: 42 | - "LambdaEdgeExecutionRole" 43 | - "Arn" 44 | Code: 45 | ZipFile: 46 | !Sub | 47 | 'use strict'; 48 | /* change the version number below whenever this code is modified */ 49 | exports.handler = (event, context, callback) => { 50 | const request = event.Records[0].cf.request; 51 | const headers = request.headers; 52 | const user_agent = headers['user-agent']; 53 | const host = headers['host']; 54 | if (user_agent && host) { 55 | var prerender = /googlebot|adsbot\-google|Feedfetcher\-Google|bingbot|yandex|baiduspider|Facebot|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|redditbot|applebot|whatsapp|flipboard|tumblr|bitlybot|skypeuripreview|nuzzel|discordbot|google page speed|qwantify|pinterestbot|bitrix link preview|xing\-contenttabreceiver|chrome\-lighthouse|telegrambot/i.test(user_agent[0].value); 56 | prerender = prerender || /_escaped_fragment_/.test(request.querystring); 57 | prerender = prerender && ! /\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)$/i.test(request.uri); 58 | if (prerender) { 59 | headers['x-prerender-token'] = [{ key: 'X-Prerender-Token', value: '${PrerenderToken}'}]; 60 | headers['x-prerender-host'] = [{ key: 'X-Prerender-Host', value: host[0].value}]; 61 | headers['x-prerender-cachebuster'] = [{ key: 'X-Prerender-Cachebuster', value: Date.now().toString()}]; 62 | headers['x-query-string'] = [{ key: 'X-Query-String', value: request.querystring}]; 63 | } 64 | } 65 | callback(null, request); 66 | }; 67 | Runtime: "nodejs14.x" 68 | SetPrerenderHeaderVersion3: 69 | Type: "AWS::Lambda::Version" 70 | Properties: 71 | FunctionName: 72 | Ref: "SetPrerenderHeader" 73 | Description: "SetPrerenderHeader" 74 | RedirectToPrerender: 75 | Type: "AWS::Lambda::Function" 76 | Properties: 77 | Handler: "index.handler" 78 | Role: 79 | Fn::GetAtt: 80 | - "LambdaEdgeExecutionRole" 81 | - "Arn" 82 | Code: 83 | ZipFile: | 84 | 'use strict'; 85 | /* change the version number below whenever this code is modified */ 86 | exports.handler = (event, context, callback) => { 87 | const request = event.Records[0].cf.request; 88 | if (request.headers['x-prerender-token'] && request.headers['x-prerender-host'] && request.headers['x-query-string']) { 89 | request.querystring = request.headers['x-query-string'][0].value; 90 | request.origin = { 91 | custom: { 92 | domainName: 'service.prerender.io', 93 | port: 443, 94 | protocol: 'https', 95 | readTimeout: 20, 96 | keepaliveTimeout: 5, 97 | customHeaders: {}, 98 | sslProtocols: ['TLSv1', 'TLSv1.1'], 99 | path: '/https%3A%2F%2F' + request.headers['x-prerender-host'][0].value 100 | } 101 | }; 102 | } 103 | callback(null, request); 104 | }; 105 | Runtime: "nodejs14.x" 106 | RedirectToPrerenderVersion1: 107 | Type: "AWS::Lambda::Version" 108 | Properties: 109 | FunctionName: 110 | Ref: "RedirectToPrerender" 111 | Description: "RedirectToPrerender" 112 | CloudFront: 113 | Type: "AWS::CloudFront::Distribution" 114 | Properties: 115 | DistributionConfig: 116 | DefaultCacheBehavior: 117 | Compress: true 118 | # NOTE: we let cloudfront cache heavily the resurces of the SPA. Your deploy 119 | # step will need to include an invalidation of the cloudfromt cache. 120 | # The requests to prerender.io are NOT cached thanks to the X-Prerender-Cachebuster 121 | # header. 122 | MinTTL: 31536000 123 | DefaultTTL: 31536000 124 | ForwardedValues: 125 | QueryString: false 126 | Headers: 127 | - "X-Prerender-Token" 128 | - "X-Prerender-Host" 129 | - "X-Prerender-Cachebuster" 130 | - "X-Query-String" 131 | TargetOriginId: origin 132 | ViewerProtocolPolicy : allow-all 133 | LambdaFunctionAssociations: 134 | - EventType: viewer-request 135 | LambdaFunctionARN: !Join [ ":", [ !GetAtt [SetPrerenderHeader, Arn], !GetAtt [SetPrerenderHeaderVersion3, Version] ] ] 136 | - EventType: origin-request 137 | LambdaFunctionARN: !Join [ ":", [ !GetAtt [RedirectToPrerender, Arn], !GetAtt [RedirectToPrerenderVersion1, Version] ] ] 138 | Enabled: true 139 | CustomErrorResponses: 140 | - ErrorCode: 404 141 | ResponseCode: 200 142 | ResponsePagePath: /index.html 143 | HttpVersion: http2 144 | Origins: 145 | - CustomOriginConfig: 146 | OriginProtocolPolicy: http-only 147 | DomainName: !Select [2, !Split [ '/', !GetAtt [WebBucket, WebsiteURL]]] 148 | Id: origin 149 | PriceClass: PriceClass_100 150 | -------------------------------------------------------------------------------- /testimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinty/prerender-cloudfront/f4d896a28fff83a5a8b4bb39c4efc5156601c0c1/testimage.png --------------------------------------------------------------------------------