This AWS Lambda service provides a streamlined workflow for authors and readers to interact with each other. It allows authors to sign up, configure their reader magnet, and integrate their email tool. It also allows readers to access the author's book landing page, confirm their email address, and access the exclusive content.
-
Author Sign-up: Authors come to the website to sign-up for the service and to configure/administer their reader magnet. The author account sign-up flow should simply be email and password. Admin has the ability to approve/reject/freeze accounts in order to save capacity for the 100 authors (in case a bunch of random people sign-up).
-
Author’s Book Landing Page Configuration: After an author is signed up, they have a landing page configuration capability that provides the inputs to their landing page that readers see for each of their books that contain an email magnet. Each book landing page should have the ability to upload a cover image and a few paragraphs of text. For each successfully configured book, a link is generated so the author can place that link at the end of their book.
-
Author’s Email Tool Integration: Author’s can select what email tool they utilize and login to their email provider to oauth/connect their email service. Example integrations include: Mailchimp, ConstantContact, MailerLite, ConvertKit, AWeber, etc. If an author doesn’t configure a tool, they have the option to download a csv of emails provided. Additionally, authors will also have the ability to select whether or not they want to ensure emails are confirmed before providing the reader landing page and this is not selected by default.
-
Author’s Reader Landing Page: Author’s can upload their exclusive book or chapter content. That book content is delivered to an Amazon S3 bucket.
-
Author Statistics Portal: This portal shows high level statistics including: how many readers have clicked the link at the end of the book and loaded their Author Landing Page per configured book, how many readers have signed up for their newsletter, and how many readers clicked the link to access the exclusive content.
-
Author’s book landing page: This is the actual page that readers can click and come to from the email magnet. The page should display on mobile and web and have a strong call to action to sign-up for the newsletter.
-
Email Confirmation: Once a reader submits their email on the landing page, if the author has selected the option that emails are confirmed, then a reader must look in their inbox and confirm the proper email address before progressing to the reader landing page.
-
Reader Landing Page: Once a reader signs up for the newsletter, a “Thank you for signing up” page displays with a link that directs the reader to download a book.
-
Author Sign-up:
- Create an AWS Lambda function to handle the author sign-up process.
- The function should accept an email and password as input and validate the credentials.
- If the credentials are valid, the function should create an author account and return a success message.
- If the credentials are invalid, the function should return an error message.
- The function should also have the ability to approve/reject/freeze accounts in order to save capacity for the 100 authors.
-
Author’s Book Landing Page Configuration:
- Create an AWS Lambda function to handle the author’s book landing page configuration.
- The function should accept inputs such as a cover image and a few paragraphs of text.
- The function should generate a link for each successfully configured book.
- The function should return the generated link to the author.
-
Author’s Email Tool Integration:
- Create an AWS Lambda function to handle the author’s email tool integration.
- The function should accept the author’s email provider as input and validate the credentials.
- If the credentials are valid, the function should connect the email service and return a success message.
- If the credentials are invalid, the function should return an error message.
- The function should also have the ability to select whether or not the author wants to ensure emails are confirmed before providing the reader landing page.
-
Author’s Reader Landing Page:
- Create an AWS Lambda function to handle the author’s reader landing page.
- The function should accept the exclusive book or chapter content as input.
- The function should upload the content to an Amazon S3 bucket.
- The function should return a success message when the upload is complete.
-
Author Statistics Portal:
- Create an AWS Lambda function to handle the author statistics portal.
- The function should query the database for the number of readers who have clicked the link at the end of the book and loaded their Author Landing Page per configured book, the number of readers who have signed up for their newsletter, and the number of readers who clicked the link to access the exclusive content.
- The function should return the statistics to the author.
-
Author’s book landing page:
- Create an AWS Lambda function to handle the author’s book landing page.
- The function should accept the reader’s email address as input and validate the credentials.
- If the credentials are valid, the function should display the landing page with a strong call to action to sign-up for the newsletter.
- If the credentials are invalid, the function should return an error message.
-
Email Confirmation:
- Create an AWS Lambda function to handle the email confirmation process.
- The function should check if the author has selected the option that emails are confirmed.
- If the option is selected, the function should send a confirmation email to the reader and wait for the reader to confirm the email address.
- If the option is not selected, the function should proceed to the reader landing page.
-
Reader Landing Page:
- Create an AWS Lambda function to handle the reader landing page.
- The function should display a “Thank you for signing up” page with a link that directs the reader to download a book.
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { email, password } = event.body;
// Validate email and password
if (!email || !password) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'Email and password are required'
})
});
}
// Check if user already exists
const userExists = checkIfUserExists(email);
if (userExists) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'User already exists'
})
});
}
// Create user
const user = createUser(email, password);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'User created successfully',
user
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { bookId, coverImage, text } = event.body;
// Validate bookId, coverImage, and text
if (!bookId || !coverImage || !text) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'BookId, coverImage, and text are required'
})
});
}
// Check if book already exists
const bookExists = checkIfBookExists(bookId);
if (bookExists) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'Book already exists'
})
});
}
// Create book
const book = createBook(bookId, coverImage, text);
// Generate link
const link = generateLink(bookId);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Book created successfully',
book,
link
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { emailTool, emailAddress, confirmEmails } = event.body;
// Validate emailTool, emailAddress, and confirmEmails
if (!emailTool || !emailAddress || !confirmEmails) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'EmailTool, emailAddress, and confirmEmails are required'
})
});
}
// Check if emailTool is supported
const isSupported = checkIfEmailToolIsSupported(emailTool);
if (!isSupported) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'EmailTool is not supported'
})
});
}
// Connect emailTool
const connection = connectEmailTool(emailTool, emailAddress);
// Generate csv
const csv = generateCsv(emailAddress);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'EmailTool connected successfully',
connection,
csv
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { bookId, content } = event.body;
// Validate bookId and content
if (!bookId || !content) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'BookId and content are required'
})
});
}
// Upload content to S3
const s3Url = uploadContentToS3(bookId, content);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Content uploaded successfully',
s3Url
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { bookId } = event.body;
// Validate bookId
if (!bookId) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'BookId is required'
})
});
}
// Get statistics
const statistics = getStatistics(bookId);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Statistics retrieved successfully',
statistics
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { bookId } = event.body;
// Validate bookId
if (!bookId) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'BookId is required'
})
});
}
// Get book
const book = getBook(bookId);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Book retrieved successfully',
book
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { emailAddress, confirmEmails } = event.body;
// Validate emailAddress and confirmEmails
if (!emailAddress || !confirmEmails) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'EmailAddress and confirmEmails are required'
})
});
}
// Check if email is confirmed
const isConfirmed = checkIfEmailIsConfirmed(emailAddress);
if (!isConfirmed && confirmEmails) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'Email is not confirmed'
})
});
}
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Email confirmed successfully'
})
});
};
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const { emailAddress } = event.body;
// Validate emailAddress
if (!emailAddress) {
return callback(null, {
statusCode: 400,
body: JSON.stringify({
message: 'EmailAddress is required'
})
});
}
// Sign up for newsletter
const signup = signupForNewsletter(emailAddress);
// Generate link
const link = generateLink(emailAddress);
// Return success
return callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'Signup successful',
signup,
link
})
});
};
Using Vue and Vuetify frameworks
<template>
<v-app>
<v-content>
<v-container>
<!-- Author Sign-up -->
<v-row>
<v-col>
<v-form>
<v-text-field label="Email" v-model="email" />
<v-text-field label="Password" v-model="password" type="password" />
<v-btn @click="signUp()">Sign Up</v-btn>
</v-form>
</v-col>
</v-row>
<!-- Author's Book Landing Page Configuration -->
<v-row>
<v-col>
<v-form>
<v-text-field label="Cover Image" v-model="coverImage" />
<v-textarea label="Text" v-model="text" />
<v-btn @click="generateLink()">Generate Link</v-btn>
</v-form>
</v-col>
</v-row>
<!-- Author's Email Tool Integration -->
<v-row>
<v-col>
<v-form>
<v-select label="Email Tool" v-model="emailTool" :items="emailTools" />
<v-btn @click="connectEmailTool()">Connect Email Tool</v-btn>
<v-btn @click="downloadCSV()">Download CSV</v-btn>
</v-form>
</v-col>
</v-row>
<!-- Author's Reader Landing Page -->
<v-row>
<v-col>
<v-form>
<v-text-field label="Book Content" v-model="bookContent" />
<v-btn @click="uploadBookContent()">Upload Book Content</v-btn>
</v-form>
</v-col>
</v-row>
<!-- Author Statistics Portal -->
<v-row>
<v-col>
<v-card>
<v-card-title>Statistics</v-card-title>
<v-card-text>
<p>Number of readers who clicked the link at the end of the book and loaded their Author Landing Page per configured book: {{ readersLinked }}</p>
<p>Number of readers who signed up for their newsletter: {{ readersSignedUp }}</p>
<p>Number of readers who clicked the link to access the exclusive content: {{ readersContent }}</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Reader Workflow -->
<v-row>
<v-col>
<v-form>
<v-text-field label="Email" v-model="readerEmail" />
<v-btn @click="submitReaderEmail()">Submit Email</v-btn>
</v-form>
</v-col>
</v-row>
<!-- Reader Landing Page -->
<v-row>
<v-col>
<v-card>
<v-card-title>Thank you for signing up!</v-card-title>
<v-card-text>
<p>Click the link below to download your book.</p>
<v-btn @click="downloadBook()">Download Book</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-content>
</v-app>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
email: '',
password: '',
coverImage: '',
text: '',
emailTool: '',
emailTools: ['Mailchimp', 'ConstantContact', 'MailerLite', 'ConvertKit', 'AWeber'],
bookContent: '',
readerEmail: ''
};
},
computed: {
...mapState(['readersLinked', 'readersSignedUp', 'readersContent'])
},
methods: {
signUp() {
// sign up logic
},
generateLink() {
// generate link logic
},
connectEmailTool() {
// connect email tool logic
},
downloadCSV() {
// download csv logic
},
uploadBookContent() {
// upload book content logic
},
submitReaderEmail() {
// submit reader email logic
},
downloadBook() {
// download book logic
}
}
};
</script>
provider "aws" {
region = "us-east-1"
}
Create an IAM user for the author to sign up and configure their reader magnet.
resource "aws_iam_user" "author" {
name = "author"
}
resource "aws_iam_access_key" "author" {
user = "${aws_iam_user.author.name}"
}
Create an S3 bucket for the author to upload their exclusive book or chapter content.
resource "aws_s3_bucket" "author_content" {
bucket = "author-content"
acl = "private"
}
Create a Lambda function to process the reader's email address and send a confirmation email if the author has selected the option that emails are confirmed.
resource "aws_lambda_function" "email_confirmation" {
filename = "email_confirmation.zip"
function_name = "email_confirmation"
role = "${aws_iam_role.lambda_role.arn}"
handler = "index.handler"
source_code_hash = "${base64sha256(file("email_confirmation.zip"))}"
runtime = "nodejs12.x"
}
resource "aws_iam_role" "lambda_role" {
name = "lambda_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda_policy"
role = "${aws_iam_role.lambda_role.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::author-content/*"
}
]
}
EOF
}
Create an API Gateway to handle the requests from the reader's landing page.
resource "aws_api_gateway_rest_api" "reader_landing_page" {
name = "reader_landing_page"
}
resource "aws_api_gateway_resource" "reader_landing_page" {
rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
parent_id = "${aws_api_gateway_rest_api.reader_landing_page.root_resource_id}"
path_part = "reader_landing_page"
}
resource "aws_api_gateway_method" "reader_landing_page" {
rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
resource_id = "${aws_api_gateway_resource.reader_landing_page.id}"
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "reader_landing_page" {
rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
resource_id = "${aws_api_gateway_resource.reader_landing_page.id}"
http_method = "${aws_api_gateway_method.reader_landing_page.http_method}"
integration_http_method = "POST"
type = "AWS_PROXY"
uri = "${aws_lambda_function.email_confirmation.invoke_arn}"
}
resource "aws_api_gateway_deployment" "reader_landing_page" {
depends_on = ["aws_api_gateway_integration.reader_landing_page"]
rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
stage_name = "prod"
}
GitLab CI Configuration
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
test:
stage: test
script:
- npm run test
deploy:
stage: deploy
script:
- aws deploy --type typescript --lambda-service
environment:
name: production
url: https://example.com
only:
- master
.deploy_job:
stage: deploy
script:
- aws deploy --type typescript --lambda-service
environment:
name: production
url: https://example.com
when: manual
allow_failure: true
only:
- master
name: CI
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Install Dependencies
run: npm install
- name: Build
run: npm run build
- name: Test
run: npm run test
- name: Deploy
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to AWS
run: npm run deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
# Return code should be 0 if success
- name: Check Return Code
run: exit 0