Add link elements around tweet entities such as hash tags, mentions, and urls

If you’re using the Twitter v1.1 API to fetch a user’s statuses from their timeline, then you’ve likely come across the fact that user mentions, hashtags, and urls do not have links in the “text” node of the JSON response the API returns. There is no <a> element to follow the link, and there are no links to follow to check out a mention or hashtag on Twitter site. This can easily be overcome using PHP and parsing some of the data in the JSON response and wrapping the entities in the desired <a> elements.

For example, a tweet that looks like this:

@joesexton00 contributes to #webtipblog, check it out at!

will look like this after we run it through our function:

<a href='' target='_blank'>@joesexton00</a> contributes to <a href='' target='_blank'>#webtipblog</a>, check it out at <a href='' target='_blank'></a>!

For reference, the API will return JSON that looks like this when you make a valid request for statuses/user_timeline per the documentation:

    "coordinates": null,
    "favorited": false,
    "truncated": false,
    "created_at": "Wed Aug 29 17:12:58 +0000 2012",
    "id_str": "240859602684612608",
    "entities": {
      "urls": [
          "expanded_url": "",
          "url": "",
          "indices": [
          "display_url": "\u2026"
      "hashtags": [

      "user_mentions": [

    "in_reply_to_user_id_str": null,
    "contributors": null,
    "text": "Introducing the Twitter Certified Products Program:",
    "retweet_count": 121,
    "in_reply_to_status_id_str": null,
    "id": 240859602684612608,
    "geo": null,
    "retweeted": false,
    "possibly_sensitive": false,
    "in_reply_to_user_id": null,
    "place": null,
    "user": {
      "profile_sidebar_fill_color": "DDEEF6",
      "profile_sidebar_border_color": "C0DEED",
      "profile_background_tile": false,
      "name": "Twitter API",
      "profile_image_url": "",
      "created_at": "Wed May 23 06:01:13 +0000 2007",
      "location": "San Francisco, CA",
      "follow_request_sent": false,
      "profile_link_color": "0084B4",
      "is_translator": false,
      "id_str": "6253282",
      "entities": {
        "url": {
          "urls": [
              "expanded_url": null,
              "url": "",
              "indices": [
        "description": {
          "urls": [

      "default_profile": true,
      "contributors_enabled": true,
      "favourites_count": 24,
      "url": "",
      "profile_image_url_https": "",
      "utc_offset": -28800,
      "id": 6253282,
      "profile_use_background_image": true,
      "listed_count": 10775,
      "profile_text_color": "333333",
      "lang": "en",
      "followers_count": 1212864,
      "protected": false,
      "notifications": null,
      "profile_background_image_url_https": "",
      "profile_background_color": "C0DEED",
      "verified": true,
      "geo_enabled": true,
      "time_zone": "Pacific Time (US & Canada)",
      "description": "The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.",
      "default_profile_image": false,
      "profile_background_image_url": "",
      "statuses_count": 3333,
      "friends_count": 31,
      "following": null,
      "show_all_inline_media": false,
      "screen_name": "twitterapi"
    "in_reply_to_screen_name": null,
    "source": "<a href=\"\" rel=\"nofollow\">YoruFukurou</a>",
    "in_reply_to_status_id": null

As you can see, we are given the raw tweet text in the ‘text” node, with no links, so we’re left to our own devices to add the <a> elements. Note that we are given an “entities” node that has details about any link, mention, or hashtag entities within the tweet text. The “entities” node has the index of the entity location within the text node, so we can use that to find the original raw entity text within the tweet. We can wrap that entity with a link to back to Twitter and just replace the original text with the new attribute.

This PHP function that will take a single node and return the formatted text:

 * addTweetEntityLinks
 * adds a link around any entities in a twitter feed
 * twitter entities include urls, user mentions, and hashtags
 * @author     Joe Sexton <>
 * @param      object $tweet a JSON tweet object v1.1 API
 * @return     string tweet
function addTweetEntityLinks( $tweet )
	// actual tweet as a string
	$tweetText = $tweet->text;

	// create an array to hold urls
	$tweetEntites = array();

	// add each url to the array
	foreach( $tweet->entities->urls as $url ) {
		$tweetEntites[] = array (
				'type'    => 'url',
				'curText' => substr( $tweetText, $url->indices[0], ( $url->indices[1] - $url->indices[0] ) ),
				'newText' => "<a href='".$url->expanded_url."' target='_blank'>".$url->display_url."</a>"
	}  // end foreach

	// add each user mention to the array
	foreach ( $tweet->entities->user_mentions as $mention ) {
		$string = substr( $tweetText, $mention->indices[0], ( $mention->indices[1] - $mention->indices[0] ) );
		$tweetEntites[] = array (
				'type'    => 'mention',
				'curText' => substr( $tweetText, $mention->indices[0], ( $mention->indices[1] - $mention->indices[0] ) ),
				'newText' => "<a href='".$mention->screen_name."' target='_blank'>".$string."</a>"
	}  // end foreach

	// add each hashtag to the array
	foreach ( $tweet->entities->hashtags as $tag ) {
		$string = substr( $tweetText, $tag->indices[0], ( $tag->indices[1] - $tag->indices[0] ) );
		$tweetEntites[] = array (
				'type'    => 'hashtag',
				'curText' => substr( $tweetText, $tag->indices[0], ( $tag->indices[1] - $tag->indices[0] ) ),
				'newText' => "<a href='".$tag->text."&src=hash' target='_blank'>".$string."</a>"
	}  // end foreach

	// replace the old text with the new text for each entity
	foreach ( $tweetEntites as $entity ) {
		$tweetText = str_replace( $entity['curText'], $entity['newText'], $tweetText );
	} // end foreach

	return $tweetText;

} // end addTweetEntityLinks()

Note that the JSON response does not have urls for any mentions or hashtags so the function builds those urls.

All that’s left is to loop through the JSON response and process each tweet:

// $data = Twitter JSON response...

if ( !empty( $data ) ) {
    foreach ( $data as $tweet ) {
        $tweetText = addTweetEntityLinks( $tweet );
        // use $tweetText however you want to

See my article on formatting the date in the Twitter REST API.

Also see my PHP Twitter class on Github that extends Abraham’s Twitter OAuth library and adds date formatting and entity links automatically.


5 comments on “Add Links to Twitter Mentions, Hashtags, and URLs with PHP and the Twitter 1.1 oAuth API

  1. negletios says:


    Nice work but I have to mention some problems of your script. I believe you are using the indices in the wrong way. I will give you an example:

    If someone tweets the following : ”Best drink ever #coca #cocacola”

    With your script in this case, it will change the first hashtag, but it will also change the four characters of the second hashtag.

    You should use substr_replace and use the exact indices every time you change an entity. You should also keep a counter of the extra characters you add to the html of the tweet, so you can track the next entity.

    That’s it 🙂 Thanks for the free script 🙂

  2. Jonathan says:

    I was just about to write a function for this and stumbled across yours. Works like a dream!


  3. Tim Parker says:

    Thanks for the script – it was great but I ran into some trouble with cut off characters which I fixed by changing substr to mb_substr and specifying UTF-8, e.g.

    ‘curText’ => mb_substr( $tweetText, $mention[‘indices’][0], ( $mention[‘indices’][1] – $mention[‘indices’][0] ), ‘UTF-8’ ),

    Thanks again!

  4. hashim says:

    Thank for this scirpt how do i display images as attachments?

  5. KJ says:

    display images
    // add img to the array
    foreach( $tweet->entities->media as $url ) {
    $tweetEntites[] = array (
    ‘type’ => ‘photo’,
    ‘curText’ => substr( $tweetText, $url->media[0], ( $url->indices[1] – $url->indices[0] ) ),
    ‘newText’ => “expanded_url.”‘ target=’_blank’>media_url.”‘>
    } // end foreach

Comments are closed.