Contact form on a static site

Contact form on a static site6 min read

Add a contact form to an Amazon S3 static website

S3 website endpoints accept only GET and HEAD over plain HTTP, so post the contact form to a form backend and get redirects right with CloudFront.

Preet SutharFounder, Ikiform

An S3 website endpoint answers a form POST with 405 MethodNotAllowed, so point the form's action at your Ikiform endpoint instead. Because the endpoint is plain http:, a _redirect back to it is never followed; set Redirect after submit in the form's settings, or put CloudFront in front for https:. With CloudFront in front of the bucket's REST endpoint, redirect to a full file name such as thanks.html, because the default root object applies only to the root.

What's different on S3

A bucket configured for static website hosting gets its own website endpoint, such as http://www.example.com.s3-website-us-east-1.amazonaws.com. AWS documents that it supports only GET and HEAD requests on objects. A form post gets an HTML error page:

Response to a POST
<html>
<head><title>405 Method Not Allowed</title></head>
<body>
<h1>405 Method Not Allowed</h1>
<ul>
<li>Code: MethodNotAllowed</li>
<li>Message: The specified method is not allowed against this resource.</li>
<li>Method: POST</li>
<li>ResourceType: OBJECT</li>

Three more S3 traits shape the contact form:

S3 traitWhat it means for the form
Website endpoints don't support HTTPSIkiform only follows a _redirect to an https: URL, so a redirect back to the bare endpoint is ignored
Response headers come from CloudFront, not the bucketA Content-Security-Policy is set in a CloudFront response headers policy
CloudFront's default root object only applies to the rootWith a REST origin, https://example.com/thanks/ doesn't load thanks/index.html; redirect to thanks.html

For an https: site, AWS recommends Amplify Hosting, which runs on CloudFront, or CloudFront in front of the bucket. The form works either way. What changes is where the visitor ends up after sending.

Where the visitor lands after sending, with and without CloudFrontbare S3 website endpoint, httpCloudFront and your domain, httpshttp://…s3-website…contact.htmlIkiformstores, emails youIkiform'sthank-you page_redirect to httpignoredhttps://www.example.comcontact.htmlIkiformstores, emails youyour thanks.html_redirect followedPOST303POST303Where the visitor lands after sending, with and without CloudFrontbare S3 website endpoint, httpCloudFront and your domain, httpshttp://…s3-website…contact.htmlIkiformstores, emails youIkiform's thank-you page_redirect to http ignoredhttps://www.example.comcontact.htmlIkiformstores, emails youyour thanks.html_redirect followedPOST303POST303
  1. On the bare website endpoint, the page is served over http and posts to Ikiform.
  2. Ikiform stores the message, but ignores a _redirect to an http address and shows its hosted thank-you page.
  3. Setting Redirect after submit in the dashboard is the way around that, because it accepts http URLs.
  4. Behind CloudFront with your own domain, the page is https.
  5. Ikiform follows the _redirect to your thanks.html, as long as the domain is in Allowed domains.
The message is stored either way. Only the last step, where the browser goes, depends on https.

Which CloudFront origin

CloudFront can read from the bucket in two ways, and the choice decides how your thank-you URL must look:

OriginBucketFolder index pagesThank-you URL
The S3 website endpoint, as a custom originMust be publicly readableWork: /thanks/ loads thanks/index.html/thanks/ or /thanks.html
The REST endpoint, with origin access controlStays private, read only by CloudFrontOnly the root, through the default root object/thanks.html, a full file name

A full file name works with both, which is why the samples use one. If you'd rather not run CloudFront yourself, Amplify Hosting can deploy from the bucket with https: and a managed CDN; the form needs no change there either.

The form

S3 sites are often company sites, so this form asks for a work email and the company name. Upload it as contact.html next to thanks.html:

contact.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="message">How can we help?</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="_redirect" value="https://www.example.com/thanks.html" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send</button>
</form>
  • The _redirect points at the CloudFront domain, with a file name. It's followed only when www.example.com or example.com is under Allowed domains.
  • On the bare http: endpoint, remove the _redirect input and set Redirect after submit to the thank-you page instead. That setting accepts http: URLs, because only your team can change it.
  • Keep email as the field name. Notification emails use it as the reply-to address.

On a company site, more than one person usually reads the contact inbox. Add up to 10 confirmed notification recipients to the form; each gets the message, and whoever presses Reply answers the sender directly. If messages should also reach a shared channel or a ticket queue, add a signed webhook and route it through Zapier or your own code. The company field arrives as plain text, so it's ready to filter on in either place.

Before you upload, test the page from a local server with localhost under Allowed domains. Posts from localhost count as tests, so you can get the form right before the bucket ever serves it.

Sync the folder to the bucket, and invalidate the pages in CloudFront so the new form is served right away:

Terminal
aws s3 sync ./site s3://www.example.com --delete

aws cloudfront create-invalidation --distribution-id E1EXAMPLE --paths "/contact.html" "/thanks.html"

Allowed domains on AWS hosts

Where the page is servedAdd
Bare website endpointThe bucket's full endpoint host, such as www.example.com.s3-website-us-east-1.amazonaws.com
CloudFront default domainThe exact d111111abcdef8.cloudfront.net host, while you test
Your own domainexample.com, which also covers www.example.com

Never add amazonaws.com, a regional s3-website suffix or cloudfront.net on their own. Every bucket or distribution in the world would match. AWS registers the S3 website endpoint domains in the Public Suffix List for the same reason: each bucket is a separate site.

Headers through CloudFront

S3 object metadata can't set a Content-Security-Policy, so set it in a CloudFront response headers policy and attach it to the distribution's default cache behavior. The header value is limited to 1,783 characters:

csp-policy.json
{
  "Name": "site-csp",
  "SecurityHeadersConfig": {
    "ContentSecurityPolicy": {
      "Override": true,
      "ContentSecurityPolicy": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
    }
  }
}
Terminal
aws cloudfront create-response-headers-policy --response-headers-policy-config file://csp-policy.json
Override: true replaces any CSP the origin sends. With a bare S3 origin there isn't one, but it keeps the policy in charge if you change origins later.

Check it works

  1. Add the domains from the table above, plus localhost while you test.
  2. Submit from your CloudFront domain. You should land on thanks.html on your own domain.
  3. If you also keep the bare website endpoint public, submit from it and confirm you reach the page set in Redirect after submit.
  4. Open Submissions in the dashboard. The new submission is there with every field you sent.
  5. Check the inbox of each notification recipient. Each gets an email with the fields, and Reply goes to the submitter when the form has an email field.
  6. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.

Gotchas

ProblemWhat to do
405 Method Not Allowed with Code: MethodNotAllowedThe form posted to the bucket. CloudFront may still serve the old contact.html; invalidate it.
AccessDenied or 403 after the redirectThe redirect points at a folder such as /thanks/ on a CloudFront distribution with a REST origin. Point it at thanks.html.
The browser lands on Ikiform's thank-you page instead of yours_redirect is ignored unless its domain is in Allowed domains and it is an https: URL. Add the domain, or set Redirect after submit in the form's settings instead.
403 origin_not_allowed, or every post lands in spamThe page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.