How to integrate dynamic functionality into static site. Contact form example from my blog.

I guess you have built a shiny new website using a static site generator like Jekyll, Hugo, Hexo, or similar, but you want more.

For example, you want your own form handling or other custom functionality. I will demonstrate how I built my contact form using AWS Lambda, SES, and API Gateway, and Serverless.js to do that. My plan is to use this approach to add a section with the most popular posts, and maybe to add a full content search. There are other ways to implement these, but I think this is the most cost effective.

You will need an Amazon Web Services account, and Node.js, NPM, and Serverless.js installed.

Install Serverless and create you projects

1

2

3

npm install serverless -g

mkdir sls-api && cd sls-api

serverless create -t aws-nodejs



You will need SU privileges to install serverless globally if you are using Linux.

If everything goes fine, you will have two files at your current working dir - handler.js, and serverless.yml.

Configure your new projects

I will paste my configuration. You will need to change your AWS user name, service name, roles, etc. - everything in square brackets.

serverless.yml 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

service: [service name]



provider:

name: aws

runtime: nodejs6.10

role: [user role]











functions:

sendEmail:

handler: handler.sendEmail

events:

- http:

path: contact/send

method: post

cors:

origins:

- '[your website url]'

- '[maybe your website url with www.]'

- 'http://localhost:4000'

environment:

MY_FORM_EMAIL: "Contact Form <form@[your website].com>"

MY_EMAIL: [your email]



resources:

Resources:

[user role]:

Type: AWS::IAM::Role

Properties:

Path: /

RoleName: [user role]

AssumeRolePolicyDocument:

Version: '2012-10-17'

Statement:

- Effect: Allow

Principal:

Service:

- lambda.amazonaws.com

Action: sts:AssumeRole



ManagedPolicyArns:

- arn: aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole

Policies:

- PolicyName: default

PolicyDocument:

Version: '2012-10-17'

Statement:

- Effect: Allow

Action:

- logs: CreateLogGroup

- logs: CreateLogStream

- logs: PutLogEvents

Resource:

- 'Fn::Join' :

- ':'

-

- 'arn:aws:logs'

- Ref: 'AWS::Region'

- Ref: 'AWS::AccountId'

- 'log-group:/aws/lambda/*:*:*'

- Effect: "Allow"

Action:

- "s3:PutObject"

Resource:

Fn: :Join:

- ""

- - "arn:aws:s3:::"

- "Ref" : "ServerlessDeploymentBucket"

- Effect: "Allow"

Action:

- ses: sendEmail

- ses: SendRawEmail

Resource: "*"



SES will throw errors if you don’t verify your domain and email address. Use this guide for SES email and domain verification.

Set up your AWS credentials

To give Serverless permission to create AWS services, you will need to set up an AWS user and grant him administrative permissions and configure serverless CLI with the users AWS key and secret.

I found this video very helpful.

Edit your AWS Lambda function

handler.js 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

;

const AWS = require ( 'aws-sdk' );

const ses = new AWS.SES();





module .exports.sendEmail = ( event, context, callback ) => {

console .log(event);

var mailer = new Mailer(ses);

var data = JSON .parse(event.body);



var origins = [

'[your website url]' ,

'[maybe your website url with www.]' ,

'http://localhost:4000'

];

var origin = origins.find( org => {

return org === event.headers.origin;

});



if (!origin) {

return callback( new Error ( 'Origin not known' ));

}





if (data._sd) {

return callback( new Error ( 'Possible spam' ));

}



var response = {

statusCode: 200 ,

headers: {

"Access-Control-Allow-Origin" : origin

},

};





mailer.send(data.name, data.email, data.message).then( () => {

callback( null , Object .assign(response, {

body: JSON .stringify({

success: true ,

message: 'Message sent successfully!'

})

}));

}).catch(callback);

};





class Mailer {

constructor (ses) {

this .ses = ses;

}



send(name, email, message) {

return new Promise ( ( resolve, reject ) => {

if (!name || !email || !message) {

throw new Error ( 'Missing parameters' );

}

if (! this ._isValidEmail(email)) {

throw new Error ( 'Invalid email address' );

}



var params = {

Destination: {

ToAddresses: [

process.env.MY_EMAIL

]

},

Message: {

Body: {

Text: {

Data: message,

Charset: 'UTF-8'

}

},

Subject: {

Data: 'Message from ' + name,

Charset: 'UTF-8'

}

},

Source: process.env.MY_FORM_EMAIL,

ReplyToAddresses: [ email ]

};



this .ses.sendEmail(params, err => {

if (err) return reject(err);

resolve();

});

});

}



_isValidEmail(email) {

const re = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ ;

return re.test(email);

}

}



Deploy to AWS

Deploy your project with the following command:

1

serverless deploy



Save the function URL for later use to submit your form.

Form with AJAX call

Add the form to your website:

1

2

3

4

5

6

7

8

9

< form class = "contact-form" >

< input type = "text" name = "name" placeholder = "Name" required />

< input type = "email" name = "email" placeholder = "Email address" required />

< input type = "text" name = "_sd" >



< textarea name = "message" placeholder = "Your message" required > </ textarea >



< button type = "submit" > Send </ button >

</ form >



Make sure to hide input with name “_sd” with CSS.

JavaScript code to submit the form:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

$( '.contact-form' ).submit( function ( evt ) {

var form = this ;

evt.preventDefault();

var data = {

name: $( '[name="name"]' ).val(),

email: $( '[name="email"]' ).val(),

message: $( '[name="message"]' ).val(),

_sd: $( '[name="_sd"]' ).val()

};



$.ajax({

method: 'POST' ,

url: [AWS_URL],

data: JSON .stringify(data),

success: function ( res ) {

alert( 'hooray!' );

},

error: function ( ) {

alert( "Don't blame the blog post author" );

}

});

});



AWS_URL should be changed with endpoint URL which is printed on successful deploy. You can always use “serverless info” command to get the URL.

You can test the function with cURL:

1

curl 'https://ibmsxw1iq3.execute-api.us-east-1.amazonaws.com/dev/contact/send' -H 'origin: http://localhost:4000' -d '{"name":"Name","email":"you@yourmail.com","message":"test","_sd":""}'



I would appreciate your suggestions, so please leave me a comment. My current concern is that someone who really hates me (thou I can’t think of anyone), trying to flood me with a huge number of emails. I would have to modify my function to do more security checks if that happens.