Getting Started
To get going, you’re going to need two things at minimum. First and foremost, I’m not going to go into detail about how OAuth works, so for that we’ll use Abraham’s Oauth Library. If you don’t use composer, click the Manual Installation tab, and download it from his GitHub. Secondly, you’ll need a Twitter app. For this DIY, we’re going to be bending the REST API to do our bidding, and for that, you need an app. If you want to wing it and think you’ll be okay without instructions, here’s a handy link to get you there. If you’re not entirely sure how to register an app with Twitter, follow this blog post on iag.me which shows you how to register a Twitter app.
Once you make your app, go to ‘Keys and Access Tokens’ and note the following (you’ll need them in the code coming up):
- Consumer Key
- Consumer Secret
- Access Token
- Access Token Secret
On to the Code!!!
For the purposes of this tutorial, we’re going to use a simple singleton class. We know that for a definite we need a template tag to display the count. One other thing to keep in mind is Twitter’s rate limit; each API call has its own limits, so for this we’re going to use the GET search/tweets endpoint, which has a rate limit of 180 calls per fifteen minutes. Due to this rate limit, you want to make sure to cache the resulting count; for this I’m using transients, however, if you have a persistent cache like WP Engine, you may want to use wp_cache_get/set functions instead. So here’s our scaffolding:
<?php class Twitter_Counts { /** * @var Twitter_Counts null */ public static $instance = null; private function __construct() { // Fancy stuff. } public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self; } return self::$instance; } public function tweet_count( $post_id ) { } } function Twitter_Counts() { return Twitter_Counts::get_instance(); } function display_tweet_counts( $post_id = 0 ) { if ( empty( $post_id ) ) { $post_id = get_the_ID(); } $cache_key = md5( 'twitter_counts_' . $post_id ); $count = get_transient( $cache_key ); if ( false == $count ) { $tc = Twitter_Counts(); // ... do stuff } return $count; }
Now that the scaffolding is setup, we need to start talking to Twitter with the OAuth library you downloaded. So setting it up is insanely easy (which is why I love this library):
require_once 'twitteroauth/autoload.php'; use Abraham\TwitterOAuth\TwitterOAuth; class Twitter_Counts { /** * @var Twitter_Counts null */ public static $instance = null; private $consumer_key = ''; private $consumer_secret = ''; private $access_token = ''; private $access_secret = ''; private function __construct() { // Fancy stuff. } public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self; } return self::$instance; } public function tweet_count( $post_id ) { $oauth = new TwitterOAuth( $this->consumer_key, $this->consumer_secret, $this->access_token, $this->access_secret ); } }
If you are using Composer, you can ignore the first two lines. For me, I downloaded the library into a twitteroauth folder. Below that, you’ll see that there are new private variables. Since these are basically like passwords, it’s best if they’re inaccessible to anyone but the main class (although of course your requirements may be different and you’ll have to accommodate for that accordingly). Here is where those app values you copied from Twitter will come in handy; you’ll need to fill in these variables.
Line 29 is where the values are used. This literally does the OAuth handshake for you, and now all we have to do is make the request we want and process the results.
Getting the Data
Using the OAuth library makes it simple to do get requests. If you want to know all the parameters for the endpoint we’re using, you’ll need to consult the official search/tweets endpoint documentation. For now, we only need to worry about q, count, and include_entities.
Since we’re using the search endpoint, we need to search something unique to the page we’re looking at, or wanting counts for, that would be included in the tweet. Can’t get much more unique than the URL, right? We also want to return as many results as possible, this will help us in possibly going around the rate limit (unless you have a page with a million likes). For this, we set count to 100. Finally, we want to make sure to include Entities, since from what I can tell, those include the original URL prior to it being converted to the t.co shortener.
The code should look something like this:
public function tweet_count( $post_id ) { $defaults = array( 'q' => get_permalink( $post_id ), 'count' => 100, 'include_entities' => true, ); $oauth = new TwitterOAuth( $this->consumer_key, $this->consumer_secret, $this->access_token, $this->access_secret ); $statuses = $oauth->get( 'search/tweets', $defaults ); }
So what about counts?
Looking at the results on the official documentation you’ll see that you get back a JSON object. A quite large one in fact, but don’t let that scare you, in the end, it’s all data, and we tell it what to do! So what do we do? Well, since the JSON data is keyed, you’ll see the main key we’re concerned with, statuses. Lastly we should also check if the property is available after the transformation by using an isset check.
Having as many checks as necessary prevents your debug log filling up. Alternatively, if you want to log these errors, you can do so in a much nicer manner. For that, you should read my other post on Debugging WordPress Tips and Snippets.
Now that we got those checks out of the way, it’s a simple as running count() over the statuses. The code goes like so:
public function tweet_count( $post_id ) { $defaults = array( 'q' => get_permalink( $post_id ), 'count' => 100, 'include_entities' => true, ); $oauth = new TwitterOAuth( $this->consumer_key, $this->consumer_secret, $this->access_token, $this->access_secret ); $statuses = $oauth->get( 'search/tweets', $defaults ); if ( ! $statuses ) { return false; } if ( ! isset( $statuses->statuses ) ) { error_log( __LINE__ ); return false; } return count( $statuses->statuses ); }
The Finish Line
Now we have to wrap up–this is the simple part! Here we need to update our display_tweet_counts() template tag to actually use our tweet counting method. Since our count method can return a boolean value (true/false) we want to check for that and set the count to zero if there was a problem. Otherwise, we want to use the actual value.
So here’s the full code:
require_once 'twitteroauth/autoload.php'; use Abraham\TwitterOAuth\TwitterOAuth; class Twitter_Counts { /** * @var Twitter_Counts null */ public static $instance = null; // You'll need to fill these in with your own data. private $consumer_key = ''; private $consumer_secret = ''; private $access_token = ''; private $access_secret = ''; private function __construct() { // Fancy stuff. } public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self; } return self::$instance; } public function tweet_count( $post_id ) { $defaults = array( 'q' => get_permalink( $post_id ), 'count' => 100, 'include_entities' => true, ); $oauth = new TwitterOAuth( $this->consumer_key, $this->consumer_secret, $this->access_token, $this->access_secret ); $statuses = $oauth->get( 'search/tweets', $defaults ); if ( ! $statuses ) { return false; } if ( ! isset( $statuses->statuses ) ) { return false; } return count( $statuses->statuses ); } } function Twitter_Counts() { return Twitter_Counts::get_instance(); } function display_tweet_counts( $post_id = 0 ) { if ( empty( $post_id ) ) { $post_id = get_the_ID(); } $cache_key = md5( 'twitter_counts_' . $post_id ); $count = get_transient( $cache_key ); if ( false == $count ) { $tc = Twitter_Counts(); $result = $tc->tweet_count( $post_id ); $count = false == $result ? 0 : $result; set_transient( $cache_key, $count, 1 * HOUR_IN_SECONDS ); } return $count; }
What about pages with 100+ shares?
That comes in part two, so stay tuned! In part two, we’re going to get into recursion, and how to walk over the results page-by-page. Keep an eye out for the second installment, and let me know if you have any questions!
Hi, Jay!
That’s a great DIY tutorial! I’d like to add a couple of things to have in mind:
1. This method only allows you to retrieve share counts of a single URL per Twitter API call (or even less if there are > 100 tweets). You can only make around 17K calls per day, which means that you have to be pretty smart about when to check which URLs if you have many pages. Like, you’d probably wish to check new articles more often and older articles less often (but at least once a week or tweets disappear from Twitter Search). That’s totally doable, just a bit complex.
2. Some tweets will be omitted by this method. Well, Twitter search omits certain tweets anyway, but those are low quality and deserve it. However, searching for page url may omit even more. Here is my tweet created with AddThis plugin: https://twitter.com/ArturBrugeman/status/687839729568579584. You will not find it in Twitter search if you search for tweeted URL: https://twitter.com/search?f=tweets&q=http%3A%2F%2Fnewsharecounts.com%2Ftwitter-share-counts-in-addthis%2F&src=typd That’s because AddThis added #.VphoIR12whU.twitter for tracking purposes and such URLs are handled differently by Twitter.
Anyway, that’s probably the simplest self-hosted method to get Twitter share counts. Looking forward to part 2.
Thanks!
Indeed, limits are to be considered. I was not aware tweets disappear from the search?! My idea behind this was to grab tweet counts when a page is viewed, but of course for popular sites, you get a high page hit, you will definitely hit the limit quickly, possibly leaving out other posts with tweets that have yet to be counted.
You also bring up a great point about the URLs, I’m sure you could hook into that somehow, javascript or otherwise, and grab that URL string. For example, if that URL is available via the add-this javascript API then you should be able to craft a nifty AJAX method for grabbing counts while passing through the proper URL.
I believe during my testing, I don’t have the API open so I can’t confirm, original URLs were stored in tweet entities; so if you enable the include_entities flag on the query param, it also uses the entities in its search.
Yes, tweets do disappear from search API, here is a quote from docs:
“Also note that the search results at twitter.com may return historical results while the Search API usually only serves tweets from the past week”. In fact, I’ve seen it return results older than 2 weeks, and the opposite – just a couple of days for very high number of shares.
Calling Twitter API on every page hit may not be very efficient – you can only make 180 call per 15 minutes, which means any spike in page views may result in rejected calls. You could increase that limit to 450 call by creating twitter app and using app auth, but that’s still a limiting factor.
As for grabbing URLs generated by AddThis – I’m not sure I got it right. AddThis generates unique URLs on every tweet, it makes no sense to grab them as you’d now have to search for every tweet instead of searching for canonical URL.
Twitter returns tweeted URLs. Internally it crawls them and knows their ultimate canonical destination, and uses it in search index, but does not serve this data. The problem is that in this particular case Twitter does not properly index the tweet – does not cut the #… part and does not include the tweet into the search by canonical URL.
There is one more thing, btw. Sometimes Twitter won’t return the “next_results” url even if there are further tweets (the url you need to make recursive call to grab older tweets). You’d have to discover required since_id yourself by parsing all tweets. Annoying.
Excellent article! I’ve been looking into something like this for my own blog. I can’t wait to try it out.
Teitter has limited the share count from their API, will your solution work as a fix for that or was it developed prior to that?
Hey Luis,
This does not use the old ‘count endpoint’ and the article here was written after Twitter’s decision to drop said endpoint.
The code above uses Twitter’s search API to basically search for any posts ( within reason ) that have the same permalink as the page the user is currently viewing.
The main caveat to this ( as pointed out in comments above ) is that
A) this can only go back so far.
B) It’s not 100% accurate as twitter can remove items from search.
The comment from Artur above has some great intel into this.