Introduction

Recently, I faced a challenge with serving my ReactJS application using an S3 bucket and CloudFront distribution. While this setup simplifies deployment and maintenance, it has a notable limitation: it doesn’t support Server-Side Rendering (SSR) out of the box.

Server-side rendering (SSR) improves web application performance, SEO, and user experience by rendering HTML on the server before sending it to the client. This results in faster initial load times and better search engine indexing. SSR is crucial for social media as it ensures that shared links display rich previews with proper meta tags, improving engagement and visibility on platforms like Facebook, WhatsApp, Slack, Discord, and X (formerly Twitter). Here is a preview of a BBC article shared on Slack:

I needed a solution to dynamically inject OpenGraph meta tags into my HTML content based on the specific page requested. After some research, I found the perfect tool: AWS Lambda@Edge.

What is Lambda@Edge?

Lambda@Edge is an extension of AWS Lambda that allows you to run code closer to your end users, reducing latency and improving performance. When you deploy a Lambda function to an edge location, your code physically lives in multiple locations worldwide, just like CloudFront distributes your content. By pairing CloudFront with Lambda@Edge, I could customize the behavior of my content delivery. Read the developer guide on Lambda@Edge.

The Solution

To dynamically inject meta tags, I used AWS Lambda@Edge to modify CloudFront requests and responses. Here’s a step-by-step guide on how I implemented this solution:

  1. Fetch Meta data: Retrieve meta tags data from a backend service based on the page slug.
  2. Fetch HTML Content: Retrieve the index.html file from the S3 bucket.
  3. Inject Meta Tags: Replace a placeholder in the HTML content with the fetched meta tags.
  4. Return the Modified HTML: Send the modified HTML content back to the user.

Step 1: Create an Execution Role

Before creating the Lambda@Edge function, you need to create an IAM role with the necessary permissions to access S3 and execute Lambda@Edge functions.

  1. Go to the AWS Management Console and navigate to the IAM service.
  2. Click on “Roles” in the left-hand menu, then click the “Create role” button.
  3. Select “Lambda” as the trusted entity and click “Next: Permissions”.
  4. Attach permission policies required for proper access:
  5. Click “Next”
  6. Name your role (e.g., LambdaEdgeS3AccessRole) and provide it a good description.
  7. Click “Create Role”
  8. Update the role’s Trust relationships such that it looks like this:

Step 2: Create the Lambda@Edge Function

  1. Navigate to the AWS Lambda console.
  2. Click “Create function”.
  3. Choose “Author from scratch”, name your function (e.g., InjectMetaTags), and select the runtime as Node.js 14.x or newer.
  4. Under “Permissions”, choose the execution role you created earlier.
  5. Click “Create function”.

Step 3: Deploy the Function to AWS Lambda@Edge

  1. Add the code to your Lambda function. Here’s the complete code for reference:
/*global fetch*/

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

export const handler = async (event) => {
  const request = event.Records[0].cf.request;
  let response = event.Records[0].cf.response;

  if (response.status === "403") {
    const lastSlashIndex = request.uri.lastIndexOf("/");
    const slug = request.uri.substring(lastSlashIndex + 1);

		// Replace with your actual URL
    const metadataUrl = `https://example.com/meta.js?slug=${slug}`;

    try {
      const data = await fetchData(metadataUrl);
      if (data) {
      
        // Fetch index.html from S3 Bucket
        const s3Content = await fetchS3Content(
          "my-bucket",
          "index.html",
          "us-west-2"
        );

        // Replace placeholder in index.html with fetched data
        const modifiedHtml = s3Content.replace(
          '<meta http-equiv="refresh" content="999999">',
          data
        );

        response.body = modifiedHtml;
      }
    } catch (error) {
      console.error("Error fetching data or index.html:", error);

      // Set default content if there's an error
      response.body = "<h1>Error</h1><p>Failed to fetch data or index.html</p>";
    }
    response.status = "200";
    response.statusDescription = "OK";
    response.headers["content-type"] = [
      { key: "Content-Type", value: "text/html" },
    ];
  }
  return response;
};

// Function to fetch data using fetch API
const fetchData = async (url) => {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.text();
  } catch (error) {
    throw new Error(`Error fetching data: ${error.message}`);
  }
};

// Function to fetch content from S3
const fetchS3Content = async (bucketName, key, region) => {
  const s3Client = new S3Client({ region: region });
  const command = new GetObjectCommand({ Bucket: bucketName, Key: key });
  try {
    const response = await s3Client.send(command);
    const str = await response.Body.transformToString();
    return str;
  } catch (error) {
    throw new Error(`Error fetching content from S3: ${error.message}`);
  }
};

  1. After adding the code, click “Deploy”.
  2. Next, you need to associate this function with a CloudFront distribution. Click on “+ Add Trigger” and select “CloudFront” from the dropdown.
  3. Click “Deploy to Lambda@Edge”
  4. In the modal that pops up select the correct CloudFront distribution, set the cache behaviour and select the event that triggers the lambda function. (In my case it was “Origin Response” as I intercept the response and modify it in the lambda function)
  5. Tick the option “Confirm deploy to Lambda@Edge” and Click on “Deploy”
  6. Wait for the CloudFront deployment to make the necessary updates.

Testing the Function

To test the function, simply access your CloudFront distribution’s URL and append the path to a page. For example:

<https://your-cloudfront-deployment.cloudfront.net/page-slug>

You should see the meta tags dynamically injected into the HTML content based on the page slug.

Common issues and Fixes

While following this tutorial, you might encounter a few challenges. Here are some common issues and how to solve them.

AWS Lambda Timeout

If your AWS Lambda function is timing out, it could be due to network latency or long processing times. Try increasing the timeout setting in your Lambda function configuration. You could also try increasing the memory allocated for the function which increases the cpu cores used as well.

S3 Bucket Permissions

Sometimes, the S3 bucket permissions are not set correctly, causing access issues. Ensure your S3 bucket policy allows read access for the content you want to serve.

CloudFront Cache Not Updating

If you update your S3 content but don’t see changes in the browser, CloudFront might be serving cached content. Invalidate the CloudFront cache to force it to fetch the latest content from S3.

Conclusion

By leveraging AWS Lambda@Edge and CloudFront, I was able to dynamically inject OpenGraph meta tags into my HTML content, enhancing the SEO and user experience of my ReactJS application. This solution provides a flexible and scalable way to serve dynamic content without the need for a traditional server-side rendering setup.

I hope this guide helps you implement a similar solution for your projects.

Happy coding!