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
Userful links
Docs to read and tools to use:
- Documentation site Deploy nuxt.js 2.x at AWS Cloudfront
- Documentation site Facebook sharing for webmasters
- Documentation site Nuxt data fetching with asyncData
- Website of Open graph protocol
- Documentation of AWS Lambda@Edge