A fully serverless blog content management system built with AWS Lambda, S3, API Gateway, and CloudFront. Write blog posts in Markdown, store them in S3, and serve them globally through a fast, scalable, and cost-effective serverless architecture.
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β CloudFront ββββββ API Gateway ββββββ Lambda ββββββ S3 β
β (CDN) β β (Routes) β β (Processor) β β (Storage) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
- Amazon S3: Stores Markdown blog posts and static assets
- AWS Lambda: Processes requests and renders Markdown to HTML
- API Gateway: RESTful API routing and HTTP handling
- CloudFront: Global content delivery and caching (optional)
- β Markdown Support: Write posts in Markdown with YAML frontmatter
- β Serverless Architecture: Pay only for what you use
- β Auto-scaling: Handles traffic spikes automatically
- β Global Performance: Fast loading worldwide
- β SEO Friendly: Proper HTML rendering with metadata
- β Cost Effective: Minimal infrastructure costs
- β Easy Deployment: Infrastructure as Code
serverless-blog-cms/
βββ lambda/
β βββ render-post/
β β βββ index.js # Individual post renderer
β β βββ package.json # Dependencies (marked)
β β βββ node_modules/
β βββ list-posts/
β β βββ index.js # Posts list generator
β β βββ package.json # No external dependencies
β β βββ node_modules/
β βββ render-post.zip # Deployment package
β βββ list-posts.zip # Deployment package
βββ posts/
β βββ welcome-to-my-serverless-blog.md
β βββ building-serverless-applications.md
βββ infrastructure/
β βββ api-gateway.yaml # CloudFormation template
βββ scripts/
β βββ deploy.sh # Deployment script
β βββ create-post.sh # New post creation script
βββ README.md
βββ .gitignore
βββ package.json
- AWS CLI configured with appropriate permissions
- Node.js 18+ installed
- AWS Account with the following services enabled:
- Lambda
- S3
- API Gateway
- IAM
- CloudFormation (optional)
- CloudFront (optional)
Your AWS user/role needs these permissions:
lambda:*s3:*apigateway:*iam:CreateRole,iam:AttachRolePolicy,iam:PutRolePolicycloudformation:*(if using CloudFormation)
git clone <your-repo-url>
cd serverless-blog-cms
# Install dependencies for Lambda functions
cd lambda/render-post
npm install
cd ../list-posts
npm install
cd ../..# Replace with your unique bucket name
export BLOG_BUCKET="my-blog-content-$(date +%s)"
aws s3 mb s3://$BLOG_BUCKET
echo "Created bucket: $BLOG_BUCKET"
echo "BLOG_BUCKET=$BLOG_BUCKET" > .env# Create deployment packages
cd lambda/render-post
zip -r ../render-post.zip .
cd ../list-posts
zip -r ../list-posts.zip .
cd ../..
# Create IAM role for Lambda
aws iam create-role --role-name lambda-blog-role --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
# Attach basic execution policy
aws iam attach-role-policy --role-name lambda-blog-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Create S3 access policy
aws iam put-role-policy --role-name lambda-blog-role --policy-name S3Access --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::'$BLOG_BUCKET'",
"arn:aws:s3:::'$BLOG_BUCKET'/*"
]
}
]
}'
# Get your AWS account ID
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Deploy Lambda functions
aws lambda create-function \
--function-name blog-render-post \
--runtime nodejs18.x \
--role arn:aws:iam::$ACCOUNT_ID:role/lambda-blog-role \
--handler index.handler \
--zip-file fileb://lambda/render-post.zip \
--environment Variables="{BLOG_BUCKET=$BLOG_BUCKET}"
aws lambda create-function \
--function-name blog-list-posts \
--runtime nodejs18.x \
--role arn:aws:iam::$ACCOUNT_ID:role/lambda-blog-role \
--handler index.handler \
--zip-file fileb://lambda/list-posts.zip \
--environment Variables="{BLOG_BUCKET=$BLOG_BUCKET}"# Create REST API
aws apigateway create-rest-api --name serverless-blog-api
# Get API ID
API_ID=$(aws apigateway get-rest-apis --query 'items[?name==`serverless-blog-api`].id' --output text)
ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query 'items[?path==`/`].id' --output text)
# Create resources
aws apigateway create-resource --rest-api-id $API_ID --parent-id $ROOT_ID --path-part posts
aws apigateway create-resource --rest-api-id $API_ID --parent-id $ROOT_ID --path-part post
POSTS_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query 'items[?pathPart==`posts`].id' --output text)
POST_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query 'items[?pathPart==`post`].id' --output text)
aws apigateway create-resource --rest-api-id $API_ID --parent-id $POST_ID --path-part '{slug}'
SLUG_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query 'items[?pathPart==`{slug}`].id' --output text)
# Get region
REGION=$(aws configure get region)
# Create methods and integrations
aws apigateway put-method --rest-api-id $API_ID --resource-id $POSTS_ID --http-method GET --authorization-type NONE
aws apigateway put-integration --rest-api-id $API_ID --resource-id $POSTS_ID --http-method GET --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$REGION:$ACCOUNT_ID:function:blog-list-posts/invocations"
aws apigateway put-method --rest-api-id $API_ID --resource-id $SLUG_ID --http-method GET --authorization-type NONE
aws apigateway put-integration --rest-api-id $API_ID --resource-id $SLUG_ID --http-method GET --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$REGION:$ACCOUNT_ID:function:blog-render-post/invocations"
# Grant permissions
aws lambda add-permission --function-name blog-list-posts --statement-id api-gateway-invoke-list --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/*"
aws lambda add-permission --function-name blog-render-post --statement-id api-gateway-invoke-render --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/*"
# Deploy API
aws apigateway create-deployment --rest-api-id $API_ID --stage-name prod
echo "π Blog deployed successfully!"
echo "API URL: https://$API_ID.execute-api.$REGION.amazonaws.com/prod"
echo "Posts: https://$API_ID.execute-api.$REGION.amazonaws.com/prod/posts"Create Markdown files with YAML frontmatter:
---
title: "Your Post Title"
date: "2025-07-25"
author: "Your Name"
tags: ["tag1", "tag2", "tag3"]
excerpt: "A brief description of your post for the posts list page."
---
# Your Post Title
Your content goes here using **Markdown** formatting.
## Subheading
- Bullet points
- Are supported
```javascript
// Code blocks work too
console.log("Hello, World!");Links, images, and all standard Markdown features are supported.
### Adding New Posts
1. **Create the Markdown file** in the `posts/` directory
2. **Upload to S3**:
```bash
aws s3 cp posts/your-new-post.md s3://$BLOG_BUCKET/posts/
- Access via URL:
https://your-api-id.execute-api.region.amazonaws.com/prod/post/your-new-post
title(required): Post titledate(required): Publication date (YYYY-MM-DD)author(optional): Author nametags(optional): Array of tagsexcerpt(optional): Short description for post lists
Lambda functions use these environment variables:
BLOG_BUCKET: S3 bucket name containing blog posts
Edit the CSS in lambda/render-post/index.js and lambda/list-posts/index.js in the generateHTML functions.
The project uses the marked library. You can customize parsing options in lambda/render-post/index.js.
Follow the installation steps above.
./scripts/deploy.shSee .github/workflows/deploy.yml for automated deployments.
- Function logs:
/aws/lambda/blog-render-postand/aws/lambda/blog-list-posts - API Gateway logs: Enable in API Gateway console
- Lambda invocations and duration
- API Gateway 4xx/5xx errors
- S3 GET requests
- CloudFront cache hit ratio (if using CDN)
- Enable CloudFront caching to reduce Lambda invocations
- Monitor S3 storage costs
- Use S3 Intelligent Tiering for older posts
# Test Lambda functions locally (requires SAM CLI)
sam local invoke blog-render-post -e test-events/render-post.json
sam local invoke blog-list-posts -e test-events/list-posts.json# Test live endpoints
curl https://your-api-id.execute-api.region.amazonaws.com/prod/posts
curl https://your-api-id.execute-api.region.amazonaws.com/prod/post/your-post-slug- Lambda functions run with minimal IAM permissions
- S3 bucket is private (no public read access)
- API Gateway handles HTTPS termination
- Enable API Gateway throttling
- Add WAF protection
- Implement API keys for admin functions (future)
- Enable CloudTrail logging
- Use VPC for Lambda functions (if needed)
- Basic Markdown rendering
- Post listing
- Serverless architecture
- S3 storage
- CloudFront CDN integration
- RSS feed generation
- Sitemap generation
- Admin interface for post management
- Comments system (DynamoDB)
- Search functionality (OpenSearch)
- Image upload and optimization
- Multi-author support
- Post scheduling
- Analytics integration
- Lambda: ~$0.20 (2M requests free tier)
- API Gateway: ~$3.50 (1M requests free tier first year)
- S3: ~$0.02 (first 5GB free)
- CloudFront: ~$0.85 (first 1TB free first year)
- Total: ~$4.60/month
- Approximately $15-25/month with proper caching
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow JavaScript ES6+ standards
- Add error handling for all AWS service calls
- Include JSDoc comments for functions
- Test changes with real AWS resources
This project is licensed under the MIT License - see the LICENSE file for details.
# Check if functions exist
aws lambda list-functions --query 'Functions[].FunctionName'
# Redeploy if missing
aws lambda create-function [... parameters ...]# Check bucket exists and permissions
aws s3 ls s3://$BLOG_BUCKET
aws iam get-role-policy --role-name lambda-blog-role --policy-name S3Access# Check CloudWatch logs
aws logs describe-log-groups --log-group-name-prefix "/aws/lambda/blog"
aws logs tail /aws/lambda/blog-render-post --follow- Verify posts are uploaded to S3:
aws s3 ls s3://$BLOG_BUCKET/posts/ - Check frontmatter formatting (YAML syntax)
- Ensure filename matches URL slug
- Check AWS Lambda documentation
- Review API Gateway documentation
- Open an issue in this repository
- Check CloudWatch logs for detailed error messages
- AWS Serverless Application Lens
- Markdown Syntax Guide
- AWS CLI Reference
- Serverless Framework (alternative deployment method)
Built with β€οΈ using AWS Serverless Technologies