├── 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';
10 | for (let s of paths) {
11 | content = content + '- ' + s + '
\n';
12 | }
13 | 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 |
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
--------------------------------------------------------------------------------