Skip to content

ManuJB023/serverless-blog-cms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Serverless Blog CMS

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.

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  CloudFront │────│ API Gateway │────│   Lambda    │────│     S3      β”‚
β”‚    (CDN)    β”‚    β”‚   (Routes)  β”‚    β”‚ (Processor) β”‚    β”‚  (Storage)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Components

  • 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)

πŸš€ Features

  • βœ… 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

πŸ“ Project Structure

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

πŸ› οΈ Prerequisites

  • 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)

Required AWS Permissions

Your AWS user/role needs these permissions:

  • lambda:*
  • s3:*
  • apigateway:*
  • iam:CreateRole, iam:AttachRolePolicy, iam:PutRolePolicy
  • cloudformation:* (if using CloudFormation)

πŸ“¦ Installation & Setup

1. Clone and Setup

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 ../..

2. Create S3 Bucket

# 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

3. Create Lambda Functions

# 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}"

4. Create API Gateway

# 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"

✍️ Writing Blog Posts

Blog Post Format

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/
  1. Access via URL: https://your-api-id.execute-api.region.amazonaws.com/prod/post/your-new-post

Supported Frontmatter Fields

  • title (required): Post title
  • date (required): Publication date (YYYY-MM-DD)
  • author (optional): Author name
  • tags (optional): Array of tags
  • excerpt (optional): Short description for post lists

πŸ”§ Configuration

Environment Variables

Lambda functions use these environment variables:

  • BLOG_BUCKET: S3 bucket name containing blog posts

Customization

Styling

Edit the CSS in lambda/render-post/index.js and lambda/list-posts/index.js in the generateHTML functions.

Markdown Processing

The project uses the marked library. You can customize parsing options in lambda/render-post/index.js.

πŸš€ Deployment

Manual Deployment

Follow the installation steps above.

Automated Deployment (Coming Soon)

./scripts/deploy.sh

CI/CD with GitHub Actions (Coming Soon)

See .github/workflows/deploy.yml for automated deployments.

πŸ“Š Monitoring & Logging

CloudWatch Logs

  • Function logs: /aws/lambda/blog-render-post and /aws/lambda/blog-list-posts
  • API Gateway logs: Enable in API Gateway console

Metrics to Monitor

  • Lambda invocations and duration
  • API Gateway 4xx/5xx errors
  • S3 GET requests
  • CloudFront cache hit ratio (if using CDN)

Cost Optimization

  • Enable CloudFront caching to reduce Lambda invocations
  • Monitor S3 storage costs
  • Use S3 Intelligent Tiering for older posts

πŸ§ͺ Testing

Local Testing

# 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

Integration Testing

# 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

πŸ”’ Security

Current Security Measures

  • Lambda functions run with minimal IAM permissions
  • S3 bucket is private (no public read access)
  • API Gateway handles HTTPS termination

Security Best Practices

  • Enable API Gateway throttling
  • Add WAF protection
  • Implement API keys for admin functions (future)
  • Enable CloudTrail logging
  • Use VPC for Lambda functions (if needed)

🚧 Roadmap

Phase 1 (Current)

  • Basic Markdown rendering
  • Post listing
  • Serverless architecture
  • S3 storage

Phase 2 (Next)

  • CloudFront CDN integration
  • RSS feed generation
  • Sitemap generation
  • Admin interface for post management

Phase 3 (Future)

  • Comments system (DynamoDB)
  • Search functionality (OpenSearch)
  • Image upload and optimization
  • Multi-author support
  • Post scheduling
  • Analytics integration

πŸ’° Cost Estimation

Monthly costs for a small blog (1000 page views):

  • 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

At scale (100K page views/month):

  • Approximately $15-25/month with proper caching

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Guidelines

  • Follow JavaScript ES6+ standards
  • Add error handling for all AWS service calls
  • Include JSDoc comments for functions
  • Test changes with real AWS resources

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ†˜ Troubleshooting

Common Issues

Lambda Function Not Found

# Check if functions exist
aws lambda list-functions --query 'Functions[].FunctionName'

# Redeploy if missing
aws lambda create-function [... parameters ...]

S3 Access Denied

# Check bucket exists and permissions
aws s3 ls s3://$BLOG_BUCKET
aws iam get-role-policy --role-name lambda-blog-role --policy-name S3Access

API Gateway 500 Errors

# Check CloudWatch logs
aws logs describe-log-groups --log-group-name-prefix "/aws/lambda/blog"
aws logs tail /aws/lambda/blog-render-post --follow

Posts Not Displaying

  1. Verify posts are uploaded to S3: aws s3 ls s3://$BLOG_BUCKET/posts/
  2. Check frontmatter formatting (YAML syntax)
  3. Ensure filename matches URL slug

Getting Help

πŸ“š Additional Resources


Built with ❀️ using AWS Serverless Technologies

About

A serverless blog CMS built with AWS Lambda, S3, and API Gateway. Features Markdown support, frontmatter metadata, and automatic HTML rendering with zero server maintenance.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors