In this tutorial I show you how to create your own custom twitter feed for your website. You will have full control over the styling of the feed, like the one I have on my about page. It also pulls through the follower count.
A lot of clients ask for this as they don't like the default styling of the twitter feed plugin.

This tutorial is meant for an MVC website with .NET 4.5 which makes it suitable for Umbraco too.

I've included some functions to help with formatting the tweets and a function to get the relative time since the tweet.

If you already have your twitter API keys you can skip to number 4.

Before you start, you need to have the twitter API keys. To get these, you need to:

  1. sign in to https://apps.twitter.com/ with your twitter account.

  2. Click on the Create New App button and fill out the details. I didn't enter a Callback URL.
    Here's what I put for my site

    If you haven't done already, it will tell you that you need to add your mobile phone number to your twitter profile in the settings part of the main twitter site. I added my mobile number and unticked 'Let others find me by my mobile number' in the Security and privacy tab.

  3. When you have created your app and click on the name to go into the application management screen, you need to click on the tab named Keys and Access Tokens. You will need to copy your Consumer Key and your Consumer Secret. I put these as app settings in my website. You shouldn't have them in your code.

  4. Next you need to add the Skybrud.Social dll to your project. You can install it by using Package Manager Console. 

    PM> Install-Package Skybrud.Social
    If you don't use package manager, you can visit the Skybrud.Social site here http://social.skybrud.dk/

  5. Add a reference to System.Runtime.Caching to your project, because the code I wrote uses caching to reduce the calls to the twitter API.

  6. Add the following code to a partial or a view. I've put it all in one for convenience and for the purpose of this tutorial. Normally I would be passing the collection of tweets in to the model for the view. Also I would have the functions in a class elsewhere but it is very handy for this tutorial and easy for you to get something working.

    @using Skybrud.Social.Twitter
    @using Skybrud.Social.Twitter.Entities
    @using Skybrud.Social.Twitter.OAuth
    @using Skybrud.Social.Twitter.Objects
    @using System.Web.Configuration
    @using System.Runtime.Caching
    @using System.Text.RegularExpressions

    @{
    //get the twitter api keys from the web.config file.
    string consumerKey = WebConfigurationManager.AppSettings["TwitterAPIConsumerKey"];
    string consumerSecret = WebConfigurationManager.AppSettings["TwitterAPIConsumerSecret"];
    string twitterUserName = WebConfigurationManager.AppSettings["TwitterUsername"];
    const int TWITTER_FEED_CACHING_TIME_IN_MINUTES = 10;
    const int NUMBER_OF_TWEETS_TO_SHOW = 3;
    const string TWITTER_DOMAIN_URL = "https://twitter.com/";

    var tweets = GetTweets(consumerKey, consumerSecret, twitterUserName, TWITTER_FEED_CACHING_TIME_IN_MINUTES).Take(NUMBER_OF_TWEETS_TO_SHOW);
    }

    @*These are the styles I used for mine, you can create your own*@
    <style>
    .tweets { background: #f1f1f1; display: block; }

    .tweet { display: block; padding: 10px; }

    .tweets .alt { background: #fff; }

    .tweet-banner { color: #fff; text-align: center; }
    .tweet-banner a { color: #fff; }

    .tweet-banner img { border-radius: 50%; margin: 0 auto; }

    .tweet-banner .shadow {
    padding: 50px;
    content: "";
    position: relative;
    width: 100%;
    bottom: 0;
    height: 100%;
    background: -moz-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
    background: -webkit-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
    background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
    filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#a6000000', GradientType=0);
    }

    span.pretty-date { font-size: 0.6em; }
    </style>


    @if (tweets != null && tweets.Any() && tweets.Count() > 0)
    {
    var user = @tweets.First().User;
    int tweetCount = 0;

    <div class="tweet-banner" style="background:url('@user.ProfileBannerUrl') center / cover">
    <div class="shadow">
    <h2><a href="@(TWITTER_DOMAIN_URL + user.ScreenName)" target="_blank">@user.Name (@@@(user.ScreenName))</a></h2>
    <h3>@user.FollowersCount&nbsp;followers</h3>
    <img src="@user.ProfileImageUrl" />
    <h3>Latest tweets</h3>
    </div>
    </div>
    <div class="tweets">
    @foreach (TwitterStatusMessage tweet in tweets)
    {
    <div class="tweet @(tweetCount % 2 == 0 ? "" : "alt")">
    <a href="@(TWITTER_DOMAIN_URL + tweet.User.ScreenName)"><img src="@tweet.User.ProfileImageUrl" /></a> <a href="@(TWITTER_DOMAIN_URL + tweet.User.ScreenName)" target="_blank">@@@(user.ScreenName)</a> <span class="pretty-date">@GetRelativeTimeBetweenDates(tweet.CreatedAt, DateTime.UtcNow)</span>
    <p>@Html.Raw(FormatAsHtml(tweet))</p>
    </div> tweetCount++;
    }
    </div>
    }
    else
    {
    @Html.Raw("Tweets currently unavailable")
    }

    @functions{

    //These functions are for getting the tweets using Skybrud.Social and for formatting the tweet text.

    /// <summary>
    /// Gets a the last 20 Tweets from a user's timeline. Uses caching to reduce calls to the API.
    /// </summary>
    /// <param name="consumerKey">The consumerKey from your twitter API details</param>
    /// <param name="consumerSecret">The consumerSecret from your twitter API details</param>
    /// <param name="twitterUserName">The twitter name of the user whose timeline you want to get</param>
    /// <param name="cacheTimeInMinutes">Number of minutes before the cache expires</param>
    /// <returns>A collection of tweets from a user's timeline. Adds it to the cache for next time.</returns>
    public static IEnumerable<TwitterStatusMessage> GetTweets(string consumerKey, string consumerSecret, string twitterUserName, int cacheTimeInMinutes)
    {
    ObjectCache cache = MemoryCache.Default;
    var tweets = cache["tweets"] as IEnumerable<TwitterStatusMessage>;
    if (tweets == null)
    {
    var policy = new CacheItemPolicy();
    policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(cacheTimeInMinutes);

    Skybrud.Social.Twitter.OAuth.TwitterOAuthClient client = new Skybrud.Social.Twitter.OAuth.TwitterOAuthClient
    {
    ConsumerKey = consumerKey,
    ConsumerSecret = consumerSecret
    };

    TwitterService service = TwitterService.CreateFromOAuthClient(client);

    tweets = service.Statuses.GetUserTimeline(twitterUserName).Body.ToList();

    cache.Set("tweets", tweets, policy);
    }
    return tweets;
    }

    /// <summary>
    /// Calls the relevant methods to format a tweet as HTML
    /// </summary>
    /// <param name="status">The status object as a TwitterStatusMessage</param>
    /// <returns>string to output as HTML</returns>
    public static string FormatAsHtml(TwitterStatusMessage status)
    {
    var tweetText = ReplaceHashTagsAndUsernamesWithUrls(status.Text);
    foreach (var url in status.Entities.Urls)
    {
    tweetText = ReplaceFlatUrlWithHTMLUrl(tweetText, url.Url);
    }
    foreach (var mediaEntity in status.Entities.Media)
    {
    tweetText = ReplaceMediaUrlWithImgTag(tweetText, mediaEntity);
    }
    return tweetText;
    }

    /// <summary>
    /// Calls the methods for replacing hash tag text and username text with Urls to the hashtag or user on twitter.
    /// </summary>
    /// <param name="tweetText">Twitter status text</param>
    /// <param name="domainUrl">The twitter domain url in case it changes at a later date</param>
    /// <returns>The tweet text with the new formatted text in it</returns>
    public static string ReplaceHashTagsAndUsernamesWithUrls(string tweetText, string domainUrl = "https://twitter.com")
    {
    string newText = ReplaceHashtagsWithUrls(tweetText);
    newText = ReplaceTwitterUsernamesWithUrls(newText, domainUrl);
    return newText;
    }

    /// <summary>
    /// Looks for patterns in the tweet text which look like usernames and replaces them with some HTML markup for linking to the user
    /// </summary>
    /// <param name="tweetText">Twitter status text</param>
    /// <param name="domainUrl">The twitter domain url in case it changes at a later date</param>
    /// <returns>The tweet text with the new formatted text in it</returns>
    public static string ReplaceTwitterUsernamesWithUrls(string tweetText, string domainUrl = "https://twitter.com")
    {
    return Regex.Replace(tweetText, @"@(\w+)", delegate (Match match)
    {
    string userName = match.ToString().Trim();
    return String.Format("<a href=\"{0}/{1}\" target=\"_blank\">{2}</a>", domainUrl.TrimEnd('/'), userName.Substring(1), userName);
    });
    }

    /// <summary>
    /// Looks for patterns in the tweet text which look like hashtags and replaces them with some HTML markup for linking to the hashtag
    /// </summary>
    /// <param name="tweetText">Twitter status text</param>
    /// <returns>The tweet text with the new formatted text in it</returns>
    public static string ReplaceHashtagsWithUrls(string tweetText)
    {
    return Regex.Replace(tweetText, @"#(\w+)", delegate (Match match)
    {
    string hashTag = match.ToString().Trim().Substring(1);
    return String.Format("<a href=\"https://twitter.com/hashtag/{0}?src=hash\" target=\"_blank\">#{0}</a>", hashTag);
    });
    }

    /// <summary>
    /// Looks a url in the tweet and replaces it with HTML markup for the url
    /// </summary>
    /// <param name="tweetText">Twitter status text</param>
    /// <param name="urlText">The Url to replace</param>
    /// <param name="newWindow">Decide whether to open in a new window or not</param>
    /// <returns>The tweet text with the new formatted text in it</returns>
    public static string ReplaceFlatUrlWithHTMLUrl(string tweetText, string urlText, bool newWindow = true)
    {
    return tweetText.Replace(urlText, String.Format("<a href=\"{0}\" {1}>{0}</a>", urlText, newWindow ? "target=\"_blank\"" : ""));
    }

    /// <summary>
    /// Looks for images in a tweet and replaces the url text in the tweet text with the markup for displaying the image
    /// </summary>
    /// <param name="tweetText">Twitter status text</param>
    /// <param name="mediaEntity">Media item containing the details of the image</param>
    /// <param name="newWindow">Decide whether to open in a new window or not</param>
    /// <returns>The tweet text with the new formatted text in it</returns>
    public static string ReplaceMediaUrlWithImgTag(string tweetText, TwitterMediaEntity mediaEntity, bool newWindow = true)
    {
    return tweetText.Replace(mediaEntity.Url, String.Format("<a href=\"{0}\" {1}><img src=\"{0}\" /></a>", mediaEntity.MediaUrl, newWindow ? "target=\"_blank\"" : ""));
    }

    /// <summary>
    /// Returns a string representing the relative time for when the tweet was tweeted.
    /// I started out using this as a base, but rewrote some of it: http://www.dotnetperls.com/pretty-date
    /// </summary>
    /// <param name="startDate">The start date</param>
    /// <param name="endDate">The end date</param>
    /// <returns>A string representing the relative time between the two dates</returns>
    public static string GetRelativeTimeBetweenDates(DateTime startDate, DateTime endDate)
    {
    const int MAXIMUM_DAYS_IN_MONTH = 31;
    const int ONE_MINUTE = 60;
    const int TWO_MINUTES = 120;
    const int ONE_HOUR = 3600;
    const int TWO_HOURS = 7200;
    const int ONE_DAY = 86400;
    const int ONE_WEEK = 7;

    TimeSpan interval = endDate.Subtract(startDate);
    int differenceInDays = (int)interval.TotalDays;
    int differenceInSeconds = (int)interval.TotalSeconds;

    string relativeTime = "";

    if (differenceInDays >= 0 && differenceInDays < MAXIMUM_DAYS_IN_MONTH)
    {
    if (differenceInDays == 0)
    {
    if (differenceInSeconds < ONE_MINUTE)
    {
    relativeTime = "just now";
    }
    else if (differenceInSeconds < TWO_MINUTES)
    {
    relativeTime = "1 minute ago";
    }
    else if (differenceInSeconds < ONE_HOUR)
    {
    relativeTime = string.Format("{0} minutes ago", Math.Floor((double)differenceInSeconds / ONE_MINUTE));
    }
    else if (differenceInSeconds < TWO_HOURS)
    {
    relativeTime = "1 hour ago";
    }
    else if (differenceInSeconds < ONE_DAY)
    {
    relativeTime = string.Format("{0} hours ago", Math.Floor((double)differenceInSeconds / ONE_HOUR));
    }
    }
    else if (differenceInDays == 1)
    {
    DateTime yesterday = endDate.AddDays(-1);
    if (startDate.Date == yesterday.Date)
    {
    relativeTime = "yesterday";
    }
    else
    {
    relativeTime = "2 days ago";
    }
    }
    else if (differenceInDays < ONE_WEEK)
    {
    relativeTime = string.Format("{0} days ago", differenceInDays);
    }
    else if (differenceInDays < MAXIMUM_DAYS_IN_MONTH)
    {
    double numberOfWeeks = Math.Ceiling((double)differenceInDays / ONE_WEEK);
    relativeTime = string.Format("{0} week{1} ago", numberOfWeeks, numberOfWeeks > 1 ? "s" : "");
    }
    else
    {
    DateTime oneMonthAgo = endDate.AddMonths(-1);
    if (startDate.Year == oneMonthAgo.Year && startDate.Month == oneMonthAgo.Month)
    {
    relativeTime = "last month";
    }
    else
    {
    relativeTime = startDate.ToString("dd MMM yyyy");
    }
    }
    }
    return relativeTime;
    }

    }
  7. If you created it as a partial view, you can call it from another view like this:

    @{ Html.RenderPartial("_Tweets"); }

That's it, you can now tweak the markup and styling for how you want it and split the code off into separate classes  etc. If you have any problems, please put comments in the bottom of the page and I will try to help you.

Please share on Twitter, Facebook, LinkedIn Google+ etc to help others.

Want to thank me?

If I've helped you out and you want to thank me, why not buy me a coffee?

About the author

Paul Seal

Umbraco MVP and .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.

Related Posts

Find out how my blog reached 1 million page views

In this post I will tell you how my blog reached 1 million page views.

Read Post

How to include scripts from partial views in MVC and Umbraco

This post will show you how you can include scripts from inside an MVC partial view rather than havi…

Read Post

How to fix the error "Authentication failed because the remote party has closed the transport stream"

This post will show you how to fix the error "Authentication failed because the remote party has clo…

Read Post

How to solve the error Could not load file or assembly System.Security.Principal.Windows

This post help you solve the error System.IO.FileNotFoundException Could not load file or assembly S…

Read Post