GuessMyPay.com – Sharing Custom Graphics on Social Media

A custom generated image, with your user’s profile picture, that you want to enable to be shared to create a personal invitation to their friends, followers and connections.

If you’ve signed up for guessmypay.com already you will be able to see this in action by going to our share page at api.guessmypay.com/social. At the time of posting it currently looks like the image attached to this article at the top. We generate a custom shareable image with your LinkedIn profile picture and share buttons for the three main sharing engines. To show what we are aiming to produce I have attached the output below.

What I have actually done is used Amazon S3, Route 53, CloudFront, IAM and Lambda to produce a very simple webpage that hosts the images in the meta tags at the top of the page and it is this page that I then offer to allow you to share.

The process is therefore to use Python to generate the image and then upload it to S3 storage. I use a secure hash of my users unique id as the key within my bucket so that I can use the head_object call to check if the object already exists. N.B. The body argument that I am passing in here is a function so that we need not call our potentially expensive data creating function if it has already been uploaded.

def upload_if_not_present_with_lazy_loading_body(bucket, key, body):
    s3_client = boto3.client('s3')
    try:
        s3_client.head_object(Bucket=bucket, Key=key)
    except ClientError:
        s3_client.put_object(
            Bucket=bucket, Key=key,
            Body=body(),
            ContentType=mimetypes.guess_type(key)[0]
        )

I also use the functions above with the flask render_template function to the html pages that I upload to S3 that I will then link to on Twitter, Facebook and LinkedIn.

From an image creation perspective LinkedIn encourage you to use an image that is at least 1200px wide. Each of Facebook, Twitter and Linked in chop off the top and bottom of your image to make it fit their ratio. Therefore after some experimentation I ended up choosing an image size of 1201×628 with an expectation that up to 3 pixels will get chopped from the top and bottom depending on social media site.

For Twitter I use the below example. The most import meta tag for our purposes is the twitter:image item. These all combine together to tell Twitter how to build the “card” that they will resolve the included url to within my user’s tweet.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:site" content="@guessmypay">
    <meta name="twitter:creator" content="@guessmypay">
    <meta name="twitter:title" content="What Do You Think I'm Paid?">
    <meta name="twitter:description" content="Guess the salaries of your LinkedIn connections with Guess My Pay & help make pay transparent.">
    <meta name="twitter:image" content="https://gmp.gg/s/LIR5Po5b/guess-my-pay.png">
    <meta http-equiv = "refresh" content = "0; url = https://guessmypay.com" />
</head>
<body></body>
</html>

It’s a similar idea for Facebook, though the meta tags are a little different. With Facebook (and supposedly LinkedIn) using the open graph format.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta property="og:type" content="website" />
    <meta property="og:title" content="What Do You Think I'm Paid?" />
    <meta property="og:description" content="Guess the salaries of your LinkedIn connections with Guess My Pay & help make pay transparent." />
    <meta property="og:image"  content="https://gmp.gg/s/LIR5Po5b/guess-my-pay.png" />
    <meta http-equiv = "refresh" content = "0; url = https://guessmypay.com" />
</head>
<body></body>
</html>

Unfortunately I was not able to make LinkedIn faithfully follow the instructions in the open graph meta tags and whatever I did LinkedIn seemed to use the first picture on the page and the content of the title tag. Therefore for LinkedIn we have to embed the image in the page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>What Do You Think I'm Paid?</title>
    <meta http-equiv = "refresh" content = "2; url = https://guessmypay.com" />
</head>
<body>
    <p>You will be redirected in 2 seconds, or you can click the image below.</p>
    <a href="https://guessmypay.com"><img src="guess-my-pay.png" width="100%"></a>
</body>
</html>

However, while we can now post social media links to these pages and each of them will be summarised containing our custom generated image they are not the actual pages to which we actually want to push users. Therefore I have included refresh tags in the head of each page. If I set the redirect at fewer than 2 seconds LinkedIn appeared to follow it and show the main image from the redirected website. Hence the requirement for a 2 second delay on the LinkedIn Page.

To embed these links in our /social page we then load the Facebook, Twitter and LinkedIn libraries in the way documented on their various developer pages to create a simple page like this.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>More guesses = better salary estimates.</title>
    <link rel="stylesheet" href="/static/css/social_styles.css">
</head>
<div id="wrapper" >
    <img id="guess-my-pay-banner-image" src="https://www.guessmypay.com/s/banner.jpg">
    <div class="social-share-buttons">
        <div id="fb-root"></div>
        <script>(function (d, s, id) {
            var js, fjs = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) return;
            js = d.createElement(s);
            js.id = id;
            js.src = "https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.0";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));</script>
        <div class="fb-share-button"
             data-href="https://gmp.gg/s/LIR5Po5b/facebook.html"
             data-layout="button"
             size="large">
        </div>
        <div class="tw-share-button">
            <a href="https://twitter.com/intent/tweet?ref_src=twsrc%5Etfw" class="twitter-hashtag-button"
               data-size="large"
               data-text="What do you think I'm paid? Guess the salaries of your LinkedIn connections with @GuessMyPay #payequality"
               data-related="guessmypay" data-url="gmp.gg/s/LIR5Po5b/" data-show-count="false" data-lang="en">Tweet</a>
            <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
        </div>
        <div class="linkedin-share-button">
            <script src="https://platform.linkedin.com/in.js" type="text/javascript">lang: en_US</script>
            <script type="IN/Share" data-url="https://gmp.gg/s/LIR5Po5b/linkedin.html"></script>
        </div>
    </div>
    <img src="https://gmp.gg/s/LIR5Po5b/guess-my-pay.png" id="guess-my-pay-promo-image"
         alt="An invitation to the guess the pay with your LinkedIn profile picture in the middle.">
</div>
</body>
</html>

While space on Twitter is no longer at the premium it once was, and certainly not for URLs, I did want a short form URL. I looked around on Hover and found that gmp.gg was still available. With the Bailiwick of Guernsey and Good Game being of sufficiently low controversy for my purposes I settled upon this as my choice for short domain.

It took several hours for gmp.gg to pick up reliably that I had moved to using AWS Route 53 for the DNS of the site. This was required as CloudFront works on the expectation of being forwarded by CNAME records. Naked domains can not support CNAME records and so an ALIAS record is required instead. This more sophisticated use case is supported by Route 53 but not by Hover and so I had to make that change.

Setting up https was fairly straightforward within AWS and found others had done a reasonable job explaining how to do that. I did find it a little cumbersome to connect my S3 buckets to CloudFront as the walk through I was using had discouraged letting AWS set up the users access, for most purposes I expect one would be better with the limited access users created by AWS.

The final piece of the puzzle was to allow the sub-directories to show the index.html page without it having to be typed out. I could not find a setting to do this other than on the very root of the site. I accomplished this task with a simple lambda with a CloudFront trigger to add index.html to any request that came in terminated with a slash.

'use strict';
exports.handler = (event, context, callback) => {
    var request = event.Records[0].cf.request;
    request.uri =  request.uri.replace(/\/$/, '\/index.html');
    return callback(null, request);
};

The very final item was to add a very small redirecting html file to gmp.gg so that if anyone visited the root site by accident they would get redirect to our home page.

<!DOCTYPE html>
<html lang="en"><head><meta http-equiv = "refresh" content = "0; url = https://www.guessmypay.com" /></head><body></body></html>

I hope you found that useful and if there was an easier way to do any of it I would be very happy to be so informed in the comments. Of particular interest would be if you were able to successfully get LinkedIn to faithfully execute instructions in the meta tags or for Facebook and Twitter to resolve the tags a little faster.