フリーの HTML テンプレートと AWS を使って問い合わせフォーム付きサーバーレス WEB サイトをつくってみよう

こんにちは、広野です。

静的 WEB サイトは一方向の情報伝達ですが、とりあえず問い合わせフォームだけ付ければアクセスしてくれた人からの連絡手段を確立できます。

ちょっとした商品・サービス紹介用途であればそんな WEB サイトで十分なときもあります。

本記事では、AWS を使って以下のような簡易 WEB サイトをつくってみたいと思います。

  • 静的 WEB サイトを独自ドメインでつくる
  • 静的 WEB サイトへの通信は暗号化する
  • 静的 WEB サイトはフリーの HTML テンプレートを使用する
  • 静的 WEB サイトのホスティングやメール送信の仕組みは AWS を使用する

実現方法

アーキテクチャは以下です。

  • 独自ドメインで WEB サイトを公開するため、Amazon CloudFront と Amazon Route 53 を使用します。
  • 通信を暗号化するため、SSL証明書を AWS Certificate Manager で作成、Amazon CloudFront に関連付けます。
  • 静的 WEB コンテンツは Amazon S3 でホスティングします。
  • 静的 WEB サイト内に問い合わせフォームを作り、そこからメール送信スクリプト (JavaScript + jQuery) を呼び出してメールを送信します。
  • メール送信には Amazon SES を使用しますが、命令を受け付ける API を Amazon API Gateway で作成し、Amazon SES に送信するメール内容を加工するために AWS Lambda 関数を関連付けます。

実装方法

独自ドメインの準備 (Amazon Route 53)

ドメイン名は、指定事業者と呼ばれるドメイン名の販売代理店?のような業者と契約して取得します。

AWS はドメインの使用権は提供してくれませんが、DNS レコードの管理は Amazon Route 53 で行うことができます。その方が AWS サービスと DNS の連携がしやすくなりますので、そのような準備をします。

AWS マネジメントコンソールから Amazon Route 53 の管理画面にアクセスし、取得したドメインを「パブリックホストゾーン」として登録します。登録すると、そのドメインに対してデフォルトで以下のような DNS レコードが登録された状態になります。

これだけでは、まだこのドメインは機能していません。ドメイン指定事業者側でこのドメインのネームサーバを AWS のネームサーバに変更する必要があります。AWS のネームサーバは、上記画面でタイプが NS の行に表示されている 4つです。

ドメイン指定事業者がお名前.comの場合ですが、契約者用の管理画面で以下のように AWS のネームサーバを登録します。

これで独自ドメインの DNS レコード管理が Amazon Route 53 の管理化に置かれたことになります。以降の作業は、この状態であることが前提となっています。
ただし DNS 関連の設定変更は、変更した設定が世界中のインターネットに伝播するまでに最大 72 時間かかることがあります。後続作業は念のため間を空けて予定するようにしましょう。通常は数時間で問題なく機能することが多いですが。

AWS 公式のドキュメントはこちら

パブリックホストゾーンの使用 - Amazon Route 53
Route 53 コンソールを使用してパブリックホストゾーンの作成、一覧表示、および削除を行います。

SSL 証明書の準備 (AWS Certificate Manager)

WEB サイトへの通信を暗号化するため、SSL 証明書 (パブリック証明書) が必要になります。

AWS で証明書を作らなくてもよいのですが、AWS Certificate Manager で作成するとこれまた AWS サービスとの親和性が高いですし、期限更新も自動的にやってくれたり、すぐに発行、そして何より安い、などメリットが多すぎるので AWS ユーザは利用しない手はないです。

AWS マネジメントコンソールで、AWS Certificate Manager を開きます。AWS Certificate Manager はリージョンサービスなので、証明書を関連付けたい AWS リソースと同じリージョンで証明書を作成することになるのですが、今回関連付けるのは Amazon CloudFront です。その場合はバージニア北部リージョン (us-east-1) で作成する必要があることに注意してください。

CloudFront で SSL/TLS 証明書を使用するための要件 - Amazon CloudFront
CloudFront で SSL/TLS 証明書を使用するための要件と制限について説明します。

さて、今回取得した独自ドメインの証明書を作成することになるのですが、私はよく「私が勝手に万能証明書と呼んでいる証明書」をつくっています。そのドメイン名のサイトがどんなサブドメイン名になっても共通して使える証明書です。

例えば、example.com というドメイン名を取得したとすると、

  • aaa.example.com
  • bbb.example.com
  • ccc.ddd.example.com
  • example.com

など、サブドメイン名がどのような名前でも、もしくは Zone Apex と言われるサブドメインなしの状態でも使用できる証明書です。これ1つつくっておくと、同じドメイン名のサイトを新たに立ち上げたときに証明書だけは共用できることになり手間が省けます。当然お財布にも優しいですしね。

AWS Certificate Manager の使用手順は AWS 公式ドキュメントを見てもらえたらと思いますが、万能証明書は以下のようにサブドメイン名を * のワイルドカードにして、追加で Zone Apex を指定する感じです。

証明書を作成する際、そのドメインの所有権の検証を求められます。Amazon Route 53 にそのドメインをパブリックホストゾーンとして登録済みですので、AWS Certificate Manager から指定された DNS レコードを登録するだけで検証は完了します。他にも所有権検証の方法はありますが、おそらくこれが最も早くて便利な方法です。

Amazon Route 53 への DNS レコード登録で検証をさせる場合は、証明書のステータス画面で「Route 53 でレコードを作成」のボタンを押すと自動的に検証用 DNS レコードを登録してくれます。

Amazon Route 53 の DNS レコードを見ると、自動的に CNAME レコードが追加されています。

この検証用 CNAME レコードを AWS Certificate Manager が読み取ると、証明書のステータスが「発行済み」となり、他の AWS リソースから使用可能な状態になります。

証明書まで作成できたら、証明書の ID (識別子) を控えておいてください。後の手順で使用します。

AWS 公式ドキュメントはこちら

パブリック証明書をリクエストする - AWS Certificate Manager
パブリックに信頼できる証明書を ACM からリクエストする方法について説明します。

メール送信環境準備 (Amazon SES)

AWS リソースからメールを送信するために、メールサーバの役割をしてくれるのが Amazon SES です。一般的には、SMTP サーバに送るという手続きではなく、Amazon SES に対して API でメール送信リクエストをします。

Amazon SES はリージョンサービスです。Amazon SES にリクエストをかける AWS リソースと同じリージョンの Amazon SES を使用するのがベストプラクティスです。ここで、使用するリージョンの Amazon SES に事前設定をしておく必要があります。

Amazon SES は、Sandbox と呼ばれる使用制限がデフォルトでかけられています。Sandbox を解除しない限り、メールの送信量や送信先に制限がかかります。したがって、想定する使用量を決めて AWS サポートに Sandbox 解除を依頼します。

Amazon SES サンドボックス外への移動 - Amazon Simple Email Service
アカウントを Amazon SES サンドボックス外に移動し、送信クォータを引き上げます。

Sandbox を解除すると、メールの送信先が制限されなくなります。

要件によっては、Sandbox を解除する必要はないかもしれません。その場合は、メール送信先を「検証済み ID」として Amazon SES にあらかじめ登録しておく必要があります。メールのドメイン名単位で登録するにはドメインの所有者確認があり厄介なので、数が少なければメールアドレス単位で登録するのが早いです。登録されたメールアドレスには、存在確認のため AWS からメールが飛びますのでご注意ください。そのメールに記載されたリンクをクリックすることで、登録が完了します。

Amazon SES の検証済みID - Amazon Simple Email Service
Amazon SES を使用して E メールアドレスまたはドメインを検証し、それを所有していることを証明します。

Amazon SES 経由でメールを送信する際、メール送信元のメールアドレスを指定することになります。そのメールアドレスについては、Sandbox を解除していようがいまいが「検証済み ID」として登録されている必要があります。事前に実施しておきましょう。

WEB サイト環境構築 (AWS CloudFormation)

ここから先は、AWS CloudFormation で以下のサービスを連携させた状態でまとめて自動構築したいと思います。

  • Amazon CloudFront + AWS Certificate Manager で作成した証明書 + Amazon Route 53 の DNS レコード
  • Amazon S3
  • Amazon API Gateway
  • AWS Lambda

以下のパラメータを準備して、AWS CloudFormation でスタックを作成します。

  • SiteName
    WEB サイトの管理用の名前です。各種リソース名にこの名前が付加されます。
  • DomainName
    WEB サイトのドメイン名です。
  • SubDomainName
    WEB サイトのサブドメイン名です。このテンプレートでは、Zone Apex には対応していませんので必須です。
  • FeedbackSenderEmail
    問い合わせフォームがメールを送るときの送信元メールアドレスです。
  • FeedbackReceiverEmail
    問い合わせフォームがメールを送るときの送信先メールアドレスです。このテンプレートでは1つしか登録できません。
  • CertificateId
    AWS Certificate Manager で作成した証明書の ID です。

テンプレートはこちらです。

Amazon CloudFront からのみアクセスを受け付ける Amazon S3 バケットを作成するとき、従来は Origin Access Identity (OAI) をセットアップしていましたが、今後は Origin Access Control (OAC) が推奨になりました。以下のテンプレートは OAS を使用するよう修正しました。2022.10.29
Amazon CloudFront オリジンアクセスコントロール(OAC)のご紹介 | Amazon Web Services
本記事は、「Amazon CloudFront introduces Origin Access Contro
 
AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a static website hosting environment with a backend API for receiving emails from feedback form.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SiteName:
    Type: String
    Description: Your Site name. (Not any upper case characters)
    Default: xxxxx
    MaxLength: 40
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL.
    Default: example.com
    MaxLength: 100
    MinLength: 5
    AllowedPattern: "([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"

  SubDomainName:
    Type: String
    Description: Sub domain name for URL.
    Default: xxxxx
    MaxLength: 20
    MinLength: 1

  FeedbackSenderEmail:
    Type: String
    Description: E-mail address that sends feedback emails.
    Default: sender@example.com
    MaxLength: 100
    MinLength: 5
    AllowedPattern: "[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"

  FeedbackReceiverEmail:
    Type: String
    Description: E-mail address that receives feedback emails.
    Default: receiver@example.com
    MaxLength: 100
    MinLength: 5
    AllowedPattern: "[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"

  CertificateId:
    Type: String
    Description: ACM certificate ID. CloudFront only supports ACM certificates in us-east-1 region.
    Default: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    MaxLength: 36
    MinLength: 36

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub website-${SiteName}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Cost
          Value: !Sub website-${SiteName}
  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - "s3:GetObject"
            Effect: Allow
            Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
            Principal:
              Service: cloudfront.amazonaws.com
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
    DependsOn:
      - S3Bucket
      - CloudFrontDistribution

# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------#
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: !Sub CloudFront distribution for website-${SiteName}
        Aliases:
          - !Sub ${SubDomainName}.${DomainName}
        HttpVersion: http2
        IPV6Enabled: true
        PriceClass: PriceClass_200
        DefaultCacheBehavior:
          TargetOriginId: !Sub S3Origin-website-${SiteName}
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: !Ref CloudFrontCachePolicy
          OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy
          ResponseHeadersPolicyId: !Ref CloudFrontResponseHeadersPolicy
          Compress: true
          SmoothStreaming: false
        DefaultRootObject: index.html
        CustomErrorResponses:
          - ErrorCachingMinTTL: 10
            ErrorCode: 403
            ResponseCode: 404
            ResponsePagePath: /404.html
        Origins:
          - Id: !Sub S3Origin-website-${SiteName}
            DomainName: !Sub ${S3Bucket}.s3.${AWS::Region}.amazonaws.com
            S3OriginConfig:
              OriginAccessIdentity: ""
            OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
            ConnectionAttempts: 3
            ConnectionTimeout: 10
        ViewerCertificate:
          AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}"
          MinimumProtocolVersion: TLSv1.2_2021
          SslSupportMethod: sni-only
      Tags:
        - Key: Cost
          Value: !Sub website-${SiteName}
    DependsOn:
      - CloudFrontCachePolicy
      - CloudFrontOriginRequestPolicy
      - CloudFrontResponseHeadersPolicy
      - CloudFrontOriginAccessControl
      - S3Bucket
  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig: 
        Description: !Sub CloudFront OAC for website-${SiteName}
        Name: !Sub OriginAccessControl-website-${SiteName}
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
  CloudFrontCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: !Sub CachePolicy-website-${SiteName}
        Comment: !Sub CloudFront Cache Policy for website-${SiteName}
        DefaultTTL: 3600
        MaxTTL: 86400
        MinTTL: 60
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: none
          EnableAcceptEncodingBrotli: true
          EnableAcceptEncodingGzip: true
          HeadersConfig:
            HeaderBehavior: whitelist
            Headers:
              - Access-Control-Request-Headers
              - Access-Control-Request-Method
              - Origin
          QueryStringsConfig:
            QueryStringBehavior: none
  CloudFrontOriginRequestPolicy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: !Sub OriginRequestPolicy-website-${SiteName}
        Comment: !Sub CloudFront Origin Request Policy for website-${SiteName}
        CookiesConfig:
          CookieBehavior: none
        HeadersConfig:
          HeaderBehavior: whitelist
          Headers:
            - Access-Control-Request-Headers
            - Access-Control-Request-Method
            - Origin
        QueryStringsConfig:
          QueryStringBehavior: none
  CloudFrontResponseHeadersPolicy:
    Type: AWS::CloudFront::ResponseHeadersPolicy
    Properties:
      ResponseHeadersPolicyConfig:
        Name: !Sub ResponseHeadersPolicy-website-${SiteName}
        Comment: !Sub CloudFront Response Headers Policy for website-${SiteName}
        SecurityHeadersConfig:
          ContentTypeOptions:
            Override: true
          FrameOptions:
            FrameOption: SAMEORIGIN
            Override: true
          ReferrerPolicy:
            Override: true
            ReferrerPolicy: strict-origin-when-cross-origin
          StrictTransportSecurity:
            AccessControlMaxAgeSec: 31536000
            IncludeSubdomains: true
            Override: true
            Preload: true
          XSSProtection:
            ModeBlock: true
            Override: true
            Protection: true

# ------------------------------------------------------------#
# Route 53
# ------------------------------------------------------------#
  Route53RecordIpv4:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub ${DomainName}.
      Name: !Sub ${SubDomainName}.${DomainName}.
      Type: A
      AliasTarget:
        HostedZoneId: Z2FDTNDATAQYW2
        DNSName: !GetAtt CloudFrontDistribution.DomainName
    DependsOn:
      - CloudFrontDistribution
  Route53RecordIpv6:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub ${DomainName}.
      Name: !Sub ${SubDomainName}.${DomainName}.
      Type: AAAA
      AliasTarget:
        HostedZoneId: Z2FDTNDATAQYW2
        DNSName: !GetAtt CloudFrontDistribution.DomainName
    DependsOn:
      - CloudFrontDistribution

# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
  LambdaSendFeedbackByEmail:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub SendFeedbackByEmail-website-${SiteName}
      Description: !Sub Lambda Function to send user's feedback email for website-${SiteName}
      Runtime: python3.9
      Timeout: 3
      MemorySize: 128
      Role: !GetAtt LambdaSesInvocationRole.Arn
      Handler: index.lambda_handler
      Tags:
        - Key: Cost
          Value: !Sub website-${SiteName}
      Code:
        ZipFile: !Sub |
          import json
          import datetime
          import boto3
          def datetimeconverter(o):
            if isinstance(o, datetime.datetime):
              return str(o)
          def lambda_handler(event, context):
            try:
              # Define Variables
              ses = boto3.client('ses',region_name='${AWS::Region}')
              d = json.loads(event['body'])
              print({"ReceivedData": d})
              RECEIVERS = []
              RECEIVERS.append('${FeedbackReceiverEmail}')
              SENDER = '${FeedbackSenderEmail}'
              # Send Email
              res = ses.send_email(
                Destination={ 'ToAddresses': RECEIVERS },
                Message={
                  'Body': {
                    'Text': {
                      'Charset': 'UTF-8',
                      'Data': 'NAME: ' + d['name'] + '\nMAILADDRESS: ' + d['email'] + '\n\nMESSAGE:\n' + d['body']
                    }
                  },
                  'Subject': {
                    'Charset': 'UTF-8',
                    'Data': d['subj'] + ' (${SubDomainName}.${DomainName})'
                  }
                },
                Source=SENDER,
                ReplyToAddresses=[d['email']]
              )
            except Exception as e:
              print(e)
              return {
                'isBase64Encoded': False,
                'statusCode': 200,
                'body': str(e)
              }
            else:
              return {
                'isBase64Encoded': False,
                'statusCode': 200,
                'body': json.dumps(res, indent=4, default=datetimeconverter)
              }

# ------------------------------------------------------------#
# API Gateway
# ------------------------------------------------------------#
  HttpApiSendFeedbackByEmail:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Sub SendFeedbackByEmail-website-${SiteName}
      Description: !Sub HTTP API to send user's feedback email for website-${SiteName}
      ProtocolType: HTTP
      CorsConfiguration:
        AllowCredentials: false
        AllowHeaders:
          - "*"
        AllowMethods:
          - POST
          - OPTIONS
        AllowOrigins:
          - !Sub https://${SubDomainName}.${DomainName}
        ExposeHeaders:
          - "*"
        MaxAge: 600
      DisableExecuteApiEndpoint: false
      Tags:
        Cost: !Sub website-${SiteName}
  HttpApiIntegrationSendFeedbackByEmail:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref HttpApiSendFeedbackByEmail
      IntegrationMethod: POST
      IntegrationType: AWS_PROXY
      IntegrationUri: !GetAtt LambdaSendFeedbackByEmail.Arn
      CredentialsArn: !GetAtt ApiGatewayLambdaInvocationRole.Arn
      PayloadFormatVersion: 2.0
      TimeoutInMillis: 5000
  HttpApiRouteSendFeedbackByEmail:
    Type: AWS::ApiGatewayV2::Route
    Properties: 
      ApiId: !Ref HttpApiSendFeedbackByEmail
      RouteKey: POST /SendFeedbackByEmail
      Target: !Sub integrations/${HttpApiIntegrationSendFeedbackByEmail}
  HttpApiStageSendFeedbackByEmail:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref HttpApiSendFeedbackByEmail
      AutoDeploy: true
      StageName: $default

# ------------------------------------------------------------#
# Lambda SES Invocation Role (IAM)
# ------------------------------------------------------------#
  LambdaSesInvocationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub LambdaSesInvocationRole-website-${SiteName}
      Description: This role allows Lambda functions to invoke SES.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSESFullAccess
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess

# ------------------------------------------------------------#
# API Gateway Lambda Invocation Role (IAM)
# ------------------------------------------------------------#
  ApiGatewayLambdaInvocationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ApiGatewayLambdaInvocationRole-website-${SiteName}
      Description: This role allows API Gateways to invoke Lambda functions.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
# S3 Bucket
  S3BucketName:
    Value: !Ref S3Bucket
# API Gateway
  APIGatewayEndpointSendFeedbackByEmail:
    Value:
      !Join
        - ""
        - - !GetAtt HttpApiSendFeedbackByEmail.ApiEndpoint
          - /SendFeedbackByEmail
# Site URL
  SiteURL:
    Value: !Sub https://${SubDomainName}.${DomainName}

スタック完成後、出力タブで Amazon S3 バケット名と Amazon API Gateway エンドポイント名を確認しておいてください。後の作業で使用します。

静的 WEB コンテンツ準備

ここまでで、バックエンドの仕組みはできました。ここからは、フロントエンドの作業に入ります。

静的 WEB サイトのコンテンツにはフリーの HTML テンプレートを使用します。以下の提供サイトから「Forty」というテンプレートをダウンロードしてみましょう。

https://html5up.net/

ダウンロードした ZIP ファイルを解凍すると、以下のようなフォルダ、ファイルがあります。

この中の、index.html をダブルクリックして直接ブラウザで表示してみましょう。

下にスクロールしていくと、問い合わせフォームが見つかります。

この時点では問い合わせフォームはただの枠なので全く動作しませんが、先ほど作成したメール送信の仕組みを連動させるようにします。

メール送信用スクリプト準備 (JavaScript & jQuery)

問い合わせフォームに入力し「SEND MESSAGE」ボタンを押すとメールが送信されるようなスクリプトを、この HTML テンプレートに追加で仕込みます。

feedback.js

以下のスクリプトファイルを新規に作成します。

定数定義の箇所にある変数の値は環境に応じて修正します。
SUBJECT には、送信されるメールの件名を入力します。
URL には、メールを送信するための API の URL を入力します。ここでは、先ほど AWS CloudFormation で構築した Amazon API Gateway のエンドポイント URL になります。

const sendMessageButton = document.getElementById('sendMessageButton');

sendMessageButton.addEventListener('click', (e) => {
  // inputタグのデフォルトの動作を無効化
  e.preventDefault();
  // 定数定義
  const SUBJECT = "WEBサイトからの問い合わせ";
  const URL = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/SendFeedbackByEmail";
  // 入力エラーチェック
  const name = $("#name").val();
  if (!name) {
    alert ("名前を入力して下さい。");
    return;
  }
  const email = $("#email").val();
  if (!email) {
    alert ("メールアドレスを入力して下さい。");
    return;
  }
  const message = $("#message").val();
  if (!message) {
    alert ("メッセージを入力して下さい。");
    return;
  }
  if (!email.match(/^[a-zA-Z0-9]+[a-zA-Z0-9\._\-]*@[a-zA-Z0-9_\-]+[a-zA-Z0-9\._-]*\.[a-z]+$/)) {
    alert ("メールアドレスの形式が誤っているようです。");
    return;
  }
  // 送信処理
  const data = {
    "subj": SUBJECT,
    "name": name,
    "email": email,
    "body": message
  };
  $.ajax({
    type: "POST",
    cache: false,
    url: URL,
    crossDomain: true,
    contentType: "application/json; charset=utf-8",
    data: JSON.stringify(data),
    dataType: "json",
    timespan: 5000
    }).done(function(data1,textStatus,jqXHR) { //通信成功時
      alert("お問い合わせを送信しました。");
      document.getElementById("contact-form").reset();
    }).fail(function(jqXHR, textStatus, errorThrown) { //失敗時
      alert("お問い合わせの送信に失敗しました。");
    }).always(function(jqXHR, textStatus, errorThrown){ //成功、失敗関わらず実行
  });
});

このコードは JavaScript で書かれていますが、API に POST するために ajax を使っています。これは、jQuery モジュールがあらかじめロードされていないと動きません。その他、jQuery モジュールが存在する前提で書かれているコードがいくつかあります。

実はこの HTML テンプレートには、あらかじめ jQuery モジュールが適切にロードされるように最初から仕込まれています。テンプレートのファイル一式を確認すると、assets/js フォルダに jquery.min.js というファイルがあります。これが jQuery モジュールです。スクラッチから HTML コンテンツを作成する場合には、公式サイトからダウンロードして配置する必要があります。

その他、画面レイアウト調整のために用意されたスクリプトが格納されています。作成した feedback.js はこの assets/js フォルダに保存しておきましょう。後ほど HTML との関連付けの際に保存先パスを指定することになります。

index.html

作成した feedback.js は、問い合わせフォームに入力した内容を読み取ってメール送信 API に投げてくれるスクリプトですが、作成しただけではボタンを押したときに呼び出されません。

その関連付けをするために、index.html を修正します。

  • 修正箇所(黄色マーカー部分)
<form method="post" action="#">
  <div class="fields">
    <div class="field half">
      <label for="name">Name</label>
      <input type="text" name="name" id="name" />
    </div>
    <div class="field half">
      <label for="email">Email</label>
      <input type="text" name="email" id="email" />
    </div>
    <div class="field">
      <label for="message">Message</label>
      <textarea name="message" id="message" rows="6"></textarea>
    </div>
  </div>
  <ul class="actions">
    <!-- SEND MESSAGE ボタンにid属性を追加 -->
    <li><input type="submit" value="Send Message" class="primary" id="sendMessageButton" /></li>
    <li><input type="reset" value="Clear" /></li>
  </ul>
</form>

<!-- 中略 -->
<!-- Scripts -->
  <script src="assets/js/jquery.min.js"></script>
  <script src="assets/js/jquery.scrolly.min.js"></script>
  <script src="assets/js/jquery.scrollex.min.js"></script>
  <script src="assets/js/browser.min.js"></script>
  <script src="assets/js/breakpoints.min.js"></script>
  <script src="assets/js/util.js"></script>
  <script src="assets/js/main.js"></script>
  <!-- feedback.js をscript定義の最後尾に追加 -->
  <script src="assets/js/feedback.js"></script>

これにて、用意したメール送信用スクリプトが SEND MESSAGE ボタンを押したときに発動するようになります。

静的 WEB コンテンツ配置

ここまでに作成、修正したファイルを含むテンプレートファイルを一式、作成した Amazon S3 バケットに配置します。

設定したドメイン名の URL にブラウザでアクセスすると、無事静的 WEB コンテンツが表示されました。

もし Amazon S3 バケットに配置したコンテンツを更新した場合、Amazon CloudFront にはキャッシュが残ってしまっているため、更新がすぐに反映されません。コンテンツ更新後、公開されるコンテンツにすぐに反映させたい場合は、Amazon CloudFront のキャッシュをクリアしてください。

以下、AWS 公式ドキュメントによるキャッシュクリアの手順です。ファイルの無効化と翻訳されていますが、キャッシュがクリアされるだけなのでご心配なく。削除対象のキャッシュは /* を指定することで全てクリアされます。

ファイルの無効化 - Amazon CloudFront
コンテンツの有効期限が切れる前に、ファイルを無効にして、CloudFront エッジキャッシュからオブジェクト (ファイル) を削除します。

問い合わせフォーム動作確認

実際に問い合わせフォームの動作確認をしてみましょう。以下のように問い合わせフォームに入力して SEND MESSAGE ボタンを押してみます。

正常にメール送信 API にリクエストが送信されれば、以下のようにアラートが表示されます。

実際に、このようなメールが届きます。

めでたし、めでたし。

まとめ

いかがでしたでしょうか?

問い合わせフォーム付きの簡易な静的 WEB サイトをサーバーレスでつくるための一連の手順を紹介いたしました。今回は動的な機能を jQuery で作成しましたが、この程度のちょっとした機能であれば jQuery は非常に便利です。SPA 環境をつくるまでもないかな、と。

本記事が皆様のお役に立てれば幸いです。

タイトルとURLをコピーしました