Generate Facebook preview for static web with Lambda@edge

4 min. read


Do you use nuxt.js to create a static site with dynamic content hosted at AWS Cloudfront? You might have noticed that facebook share thumbnails are not working. Here’s how to fix it.

1. Understand the problem

First things first. Let’s understand the problem.

When you share a link to your site on facebook, it will try to fetch the image from the link. If it’s a static site, it will try to fetch the image from the root of the site. If it’s a dynamic site, it will try to fetch the image from the page you are sharing.

In our case, we have a static site with dynamic content. So, when we share a link to our site, facebook will try to fetch the og:meta data (title, or image) from the root of the site. But, we don’t have any data there yet as it’s added in the asyncData directive during the load. So, it will fail or just show a default meta tag information.

We might have great app, but if we can’t share it on facebook, it’s useless in eyes of our customers and clients.

That’s why we need to create a lambda function that will generate all the required data for us.

Here’s how:

2. Idea behind the solution

Our website loads the data from the API endpoint before page is rendered. We can interfere the request comming to the website, catch the facebook by the user-agent and modify the request with the lambda @edge function.

Lambda function will generate HTML code including the og:meta tags and return response to the facebook. Facebook will parse the HTML code and show the correct meta data at the share request.

3. Prerequisites

  • You need to have a AWS account with Cloudfront distribution enabled
  • You need to have a Nuxt.js site with dynamic content hosted at AWS Cloudfront 1
  • You need to have a facebook developer account. Getting the fb:app_id helps facebook to assign websie with Open Graph entity 2.
  • You use asyncData3. to fetch data from your API, including the meta tags (f.e. title, description, image).

Our API endpoint returns the following data. We’ll use it to generate and populate the meta data for facebook share request.

{
  ...
  "metaTitle": "My awesome site",
  "metaDescription": "This is my awesome site",
  "metaImageUrl": "https://my-awesome-site.com/images/awesome-image.jpg"
  ...
}

We need to create a lambda @edge function that will fetch the data from our API endpoint and return it to the Cloudfront.

4. Create a lambda function

Create a lambda @edge5 function with the following code. Don’t forget to:

  • change the api-endpoint to your API endpoint
  • add the fb:app_id
  • modify the og:meta tags 4
'use strict';

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const https = require('https');

export const handler = async(event, context, callback) => {
    
  let content = "";
  let userAgent = false;
    
  const request = event.Records[0].cf.request;
  const headers = request.headers;
  if(headers){
    userAgent = request.headers['user-agent'];
  }

  // NOTE: Identify the facebook share request by user-agent header
  // I added few more, but 'facebookexternalhit' is the most important one
  let prerender = /Facebot|FacebookBot|facebookexternalhit|twitterbot/i.test(userAgent[0].value);
    
  if(prerender){
    let path = request.uri;
    let options = {
      // NOTE: link to your API endpoint
      hostname: "<api-endpoint>",
      path: path,
      headers: {
        "Content-Type": "application/json",
      }
    }
    
    const initialResponse = await new Promise((resolve, reject) => {
      const req = https.get(options, function(res) {
        res.on('data', chunk => {
          content += chunk;
        });
        res.on('end', () => {
          resolve({
            statusCode: 200,
            body: content.toString()
          });
        });
      });
              
      req.on('error', (e) => {
        reject({
          statusCode: 400,
          body: e
        });
      });
    });
        
    // Our API returns the meta data in JSON format so we need to parse it
    let data = JSON.parse(initialResponse.body);

    // NOTE: We need to get the full url of the page we are sharing
    let ogUrl = `https://${headers['host'][0]['value']}${request.uri}`

    // NOTE: We need to generate the HTML code with the meta data
    // Our meta keys fetched from API are:
    // metaTitle - title of the page
    // metaDescription - description of the page
    // metaImageUrl - image of the page
    // if any of these are different, don't forget to change them in the code below (f.e. og:locale)
    // The fb:app_id is the id of your facebook app
    let htmlContent = `
<\!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>${data.metaTitle}</title>
    <meta property="fb:app_id" content="<your-fb-app-id>">
    <meta property="og:title" content="${data.metaTitle}" />
    <meta property="og:description" content="${data.metaDescription}" />
    <meta property="og:image" content="${data.metaImageUrl}" />
    <meta property="og:url" content="${ogUrl}" />
    <meta property="og:type" content="website" />
    <meta property="og:locale" content="sk_SK" />
  </head>
  <body>
   
  </body>
</html>
`;
        
    const response = {
      status: '200',
      statusDescription: 'OK',
      headers: {
        "content-type": [{
          "key": "Content-Type",
          "value": "text/html; charset=utf-8"
        }],
        "cache-control": [{
          "key": "Cache-Control",
          "value": "max-age=100"
        }]
      },
      body: htmlContent,
    };
      
    callback(null, response);
        
  } else {
    callback(null, request);
  }    
};

You can add some feedback to this gist at Github

Docs to read and tools to use:

4. January 2023
Posted in Gists
Tomas
Tomas

Software developer, lives in Zilina, Slovakia. Fan of modern web technologies, digitalization, cloud and education. Also co-owner of a local coffee brand - Kava Doppio