Tips for getting your blog post/podcast/video promoted for you

I’ve been doing a lot of link sharing lately through Managed Assembly, @dotnetlinks, @dotnetpodcasts, @dotnetvideos and the recently redesigned DotNetKicks. There’s numerous other .NET link sharing sites, linkblogs and other methods so if you want to take advantage of these resources to get your stuff out there, here’s a list of things to make it easier for those doing the promoting on your behalf.

0. Make good content

Enough said.

1. Use accurate but short titles

When possible, keep your titles under 140 characters (less than 100-120 if you want retweets or your username included) so they can be easily tweeted. Make sure your title is a good summary of the content so the promoter doesn’t have to rewrite it. If you use a title that doesn’t reflect the contents or is intentionally inflammatory (say, if you’re writing about exception handling and your post makes good points but uses a linkbait title, I won’t promote it. Not that you care Karl).

2. Use proper HTML titles

A lot of sites automatically grab the title from the <title> tag on your posts. Bookmarklets (in use on MA and DNK) also grab this. If it’s just the name of your site without the post title, it takes extra work for the promoter to copy and paste it. Don’t clutter up your title either. Site name and post name is all you need. All that extra stuff isn’t giving you the SEO benefit you think it is for starters, but it also requires a lot of clean up when posting.

3. Make your Twitter name easily discoverable

I always try to include the Twitter name of an article’s author (and I’m not the only one) when posting links to Twitter. If I don’t have time to go search for you on Twitter it won’t be included so make sure it’s visible on every page of your web site. Unless you don’t care, which is fine too.

4. Include video/podcast length

If you post videos, screencasts or podcasts be sure to include the length of the video or podcast in the post. The length of a video can be as equally important as the title when determining whether or not to watch it.

5. Space out your posts

Maximize your exposure by spacing out your posts. If you write a whole bunch at once, schedule them out to be posted over a few days. Don’t clog up link sharing sites with a bunch of your posts at once as it will dilute the votes you receive.

6. Post stories yourself, but don’t use Twitterfeed

On most link sharing sites, including MA and DNK, self-submitting is allowed. Just don’t ask your friends to vote it up every thing you post. Also, if you have a link you want shared on @dotnetlinks, just do an @reply to either @johnsheehan or @dotnetlinks and I’ll review it.

The Twitterfeed thing is a personal pet peeve. I’m probably already subscribed to your blog if I’m following you on Twitter. I don’t need to know about it twice. If you really, really want to have your feed reposted on Twitter for the people that don’t like RSS, set up a dedicated account for it. Which reminds me, time to go set up @JohnSheehanBlog.

Posted January 26th, 9:00 PM
Read more posts about Boring Meta, Managed Assembly, Tips.

View Comments
Link

Announcing the Managed Assembly TekPub Giveaway

I’m excited to announce that Managed Assembly is now sponsored by TekPub. To celebrate, TekPub has given me two 1-year subscriptions (each a $200 value) to give away. You can learn more about the contest and how to enter on the contest page.

I’m a big fan of what TekPub is doing and the video quality is top notch. While watching the Git series the other day I was thinking, "I wish I could buy this for everyone." While I can’t afford to do that, giving away some subscriptions is the next best thing.

If you recall back to almost a year ago when I launched the site I stated that one of my goals was no ads on the site. I’m compromising on this stance for a couple reasons:

  • The sponsorship rewards users of the site and not my pocketbook.
  • The contest will (hopefully) help bring in new users and content to the site, increasing its value to everyone.
  • The sponsorship doesn’t interfere with the site in any way. I’ve only added a small graphic/link to the footer.

I think this is really an ideal arrangement for the site. Don’t worry, I won’t be adding any big ads to the front page.

Go check out the contest page and win yourself a subscription!

Posted January 26th, 10:00 AM
Read more posts about Managed Assembly.

View Comments
Link

@dotnetlinks, @dotnetpodcasts and @dotnetvideos

A few months back I started a Twitter account called @dotnetlinks to share the most recent interesting links that I came across. It was a combination of my submissions to ManagedAssembly and a couple other ad-hoc submissions I made via Delicious. The response has been good so far. Today a couple folks on Twitter were looking for a tech video aggregator and I thought that a .NET-specific one would be a good feature for ManagedAssembly. If you’ll recall, in my www.asp.net series I mentioned a a similar idea for a podcast directory as well. I haven’t built those yet, but as a start I’ve created @dotnetvideos and @dotnetpodcasts that I’ll start feeding with content as I come across it.

For @dotnetlinks, I don’t post every link I come across. I try to pick the cream of the crop to provide a higher signal-to-noise ratio. For videos and podcasts that won’t be as easy to do since I don’t have time to watch/listen to them all. We’ll see how it goes. It would be cool to integrate those feeds with an aggregator on ManagedAssembly to help promote the quality videos/podcasts and ignore the bad ones.

Give them a follow and if you have any ideas or feedback, let me know what you think.

Posted December 13th, 4:38 PM
Read more posts about .NET, Managed Assembly, Twitter.

View Comments
Link

Another ASP.NET MVC custom ActionResult example

If you’re familiar with ASP.NET MVC this is probably not news to you. If you’re coming from Webforms, you might find this tip helpful.

One of my favorite things about MVC is how easy it is to build custom ActionResults. I previously wrote about building one for returning RSS feeds and this post expands on that idea a little bit.

When I originally set up the ManagedAssembly.com RSS feeds, I added some caching so that the feed would only be generated every 30 minutes resulting in a snapshot of the current set of popular stories. Unfortunately since I hosted the feeds directly, I have very little useful info about how much they’re being used. FeedBurner (when it works) is much better at that so I wanted to switch the feeds over to that but without changing the URLs so I don’t break any existing subscriptions. I had recently read in a post to Twitter by Scott Watermasyk that Graffiti CMS supports FeedBurner by sniffing the user agent and serving the live feed to FeedBurner but at the same URL, redirects real visitors to FeedBurner. This was the perfect solution to my problem, so here’s how I went about implementing it.

First I added a PermanentRedirectResult class that inherits from ActionResult to handle generating the 301 redirect. The built-in RedirectResult uses Response.Redirect which only is capable of issuing a 302 redirect (until ASP.NET 4.0 is out).

public class PermanentRedirectResult : ActionResult
{
    private string _url;
 
    public PermanentRedirectResult(string url) {
        _url = url;
    }
 
    public override void ExecuteResult(ControllerContext context) {
        context.HttpContext.Response.StatusCode = 301;
        context.HttpContext.Response.RedirectLocation = _url;
    }
}

Then in my ControllerBase class that all my controllers inherit from, I added a helper method to simplify calling the result:

public abstract class ControllerBase : Controller
{
    public PermanentRedirectResult PermanentRedirect(string url) {
        return new PermanentRedirectResult(url);
    }
}

Then in the action method I do a simple check for FeedBurner and if it’s not found, issue the redirect. Otherwise return the live feed.

public ActionResult Popular() {
    bool isBot = Request.UserAgent.Contains("FeedBurner");
 
    if (!isBot) {
        return PermanentRedirect(Settings.Feed.PopularFeedUrl);
    }
 
    // *snip* build and return live feed
}

Nice and straightforward and doesn’t break any existing subscriptions. Now with FeedBurner’s stats I can tell exactly how few of you are subscribing to the feed :)

Posted July 14th, 10:43 PM
Read more posts about ASP.NET MVC, Managed Assembly, Tips.

View Comments
Link

Building a mostly real-time web-based Twitter client with ASP.NET MVC, jQuery and TweetSharp, Part 2

In Part 1 I showed how the server-side tweet retrieval and caching mechanism works for the .NET Twitter Stream on Managed Assembly. In this post, I’ll show you how the UI pulls the items and updates the list live.

Twitter streams tend to show the newest items at the top of the page which I don’t think is ideal for reading a live stream of tweets. I decided to build an interface that showed the newest tweets at the bottom of a list so that it would read more like a chat window. I wanted the list to automatically scroll to keep up with the new items, but if you scrolled up to start reading older items autoscoll would be disabled so you wouldn’t be constantly losing your place.

When a visitor hits the page a call is made to the /Refresh method described in Part 1. On the first request the last 100 cached tweets on the server are returned along with the ID of the most recent tweet, which is stored in a hidden field. On subsequent requests the value from the hidden field is included in the request to /Refresh so that only new tweets are returned and appended to the list. This minimizes the amount of traffic sent back and forth and the amount of work needed to append items to the list (just loop through the returned items and append the generated HTML).

Let’s start with the HTML needed:

<div id="statusContainer">
    <table id="statuses" style="table-layout: fixed; overflow: hidden;">
    </table>
</div>
<p>
    Updates automatically every 60 seconds. 
    Auto scroll is 
    <span id="autoScrollOn">ON</span> 
    <span id="autoScrollOff" style="display: none;">OFF</span>
</p>
<%=Html.Hidden("lid", "") %>

There’s a couple inline styles but bare with me. There’s a certain amount of “get-it-done”-ness to the code (particularly the HTML generation later) but it’s not important, this is just an example that happens to be running great live :-)

There’s a lot of CSS involved, but the important style definitions to make the autoscrolling work are as follows:

#autoScrollOff { color: #f00; }
#autoScrollOn { color: #0f0; }
#statusContainer
{
    height: 525px;
    overflow: auto;
    border: 1px solid #ccc;
    margin: 5px 0;
}
#statuses
{
    width: 100%;
    border: none;
    border-collapse: collapse;
}

There are two functions that make it happen. The first calls for the JSON and schedules itself to be called again in 60 seconds:

function getUpdates() {
    $.getJSON('<%=Url.Action("Refresh")%>', { lid: $("#lid").val() }, refreshList);
    window.setTimeout(getUpdates, 60000);
}

The other function is called when the JSON is retrieved (again, this should use client-side templating or something better than just string concats, but it doesn’t so get over it):

function refreshList(json) {
    if (json.max_id != 0)
        $("#lid").val(json.max_id);
 
    $.each(json.results.reverse(), function(i, result) {
        var html = "<tr class=\"status new\"><td class=\"avatar\"><a target=\"_blank\" href=\"http://twitter.com/" + result.UserName + "\"><img height=\"48\" width=\"48\" src=\"" + result.ProfileImageUrl + "\" align=\"left\" /></a></td>";
        html += "<td><p><span class=\"user\"><a target=\"_blank\" href=\"http://twitter.com/" + result.UserName + "\">" + result.UserName + "</a></span> ";
        html += result.Text + "</p><p class=\"meta\">" + result.RelativeTime + " from " + result.ClientLink;
        if (result.IsReply) {
            html += " <a href=\"http://twitter.com/" + result.InReplyToUser + "/statuses/" + result.InReplyToId + "\">in reply to " + result.InReplyToUser + "</a>"
        }
        html += "</p></td></tr>";
 
        $("#statuses").append(html);
    });
 
    if (autoScroll) $("#statusContainer").scrollTo(99999);
}

We store the max ID returned to use for the next call. The tweet list is reversed to put new ones at the bottom (this could be handled server-side too), the HTML is built and appended to the table. At the end of the function we check to see if autoScroll is set and if so, scroll to a big number using the jQuery ScrollTo plugin.

To start things in motion, we call the getUpdates() function in document.ready() and attach the handlers to manage autoScroll state:

var autoScroll = true;
 
$(function() {
    getUpdates();
 
    $("#statusContainer").scroll(function() {
        autoScroll = this.scrollTop + this.clientHeight == this.scrollHeight;
        if (autoScroll) {
            $("#autoScrollOn").show();
            $("#autoScrollOff").hide();
        }
        else {
            $("#autoScrollOff").show();
            $("#autoScrollOn").hide();
        }
    });
});

autoScroll is defined outside of document.ready() so that’s available in all the calls to getUpdates().

There’s a similar method for retrieving the list of users currently being followed for the stream. Since the time I originally wrote this code I’ve started reading JavaScript: The Good Parts by Douglas Crockford so I’m a little ashamed of the JavaScript, but it does work well and that’s all I need for now.

Posted June 3rd, 10:08 PM
Read more posts about .NET, ASP.NET MVC, Managed Assembly, Programming, Tips, jQuery.

View Comments
Link

Building a mostly real-time web-based Twitter client with ASP.NET MVC, jQuery and TweetSharp, Part 1

Back when I launched ManagedAssembly.com around MIX I put together a page to show real-time Twitter search results for ‘MIX09’. On the heels of the dvplrs.com launch this week and the lack of .NET representation, I decided to update my live Twitter page to show live updates of notable people in the .NET community. This is how I built it. You can see the finished product over at ManagedAssembly.com/Twitter.

Since I decided to use the standard Twitter API instead of the Search API, I had to build a server-side cache of updates so that I wasn’t hitting the API on every view of the page which would quickly exhaust the API rate limits. Using a database was overkill so I decided to use ASP.NET’s built-in cache mechanism to store new updates. When the client makes a request for the latest updates, the items will be served out of the cache. The cache is updated every so often with new items.

The workflow when the client makes a request for updates (via jQuery) is like so:

  • Grab the date of the last cache update from the cache. If the value doesn’t exist or it’s older than the cache expiration threshold, it’s time to get more statuses from Twitter.
  • When new updates are pulled from Twitter, store the highest ID in the cache as well so that for future requests we can get only the new tweets.
  • Pull the currently cached items and add the new items from the Twitter API call to it.
  • When updates are sent to the browser, the client-side code stores the latest ID. For future requests this ID is passed back to the server which returns items from the cached list that are greater than the locally-stored ID.

What this gives us is the ability to only call Twitter once a minute no matter how many times the site is hit. It also allows for each visitor to only get back tweets that are new to them (which is different for every visitor depending on when the first hit the page) whenever they make a request. Since the tweets are being pulled from memory on almost every request there’s no delay while the API call is being made. If the application restarts and we lose the cache it won’t really matter because we just grab all the updates it will send and repopulate the cache. It also means that the amount of data being sent to the client via JSON is the bare minimum needed. That makes updating the UI simple (just append the new ones without having to process the full list) and keeps the bandwidth requirements low.

That was a lot of words, so time for some code. We start by determining if we need to get new items from Twitter and if so, grabbing them. This uses the CacheManager from a previous post.

CacheManager cache = new CacheManager(new ShortTermProvider());
 
IEnumerable<TwitterStatus> statuses = new List<TwitterStatus>();
long maxId = 0;
 
DateTime lastFetch = cache.Get<DateTime>("last_fetch");
if (lastFetch < DateTime.Now.AddMinutes(-1)) {
    long lastCacheId = cache.Get<long>("last_id");
    if (lastCacheId == 0)
        lastCacheId = 1; // TweetSharp/twitter returns null if you use Since(0)
 
    var request = FluentTwitter.CreateRequest().AuthenticateAs("UserGoesHere", "passwordgoeshere");
    request.Configuration.UseGzipCompression();
    request.Statuses().OnFriendsTimeline().Since(lastCacheId).AsJson();
 
    statuses = request.Request().AsStatuses();
}

The first time this is hit lastFetch doesn’t exist in cache and the cache manager returns the default for DateTime (DateTime.MinValue) which I’m pretty sure is always more than a minute ago. We’ll update the fetch time in cache later. Once we’ve determined that the cache is empty or expired, we grab the last_id value from cache (which is 0 if it doesn’t exist). We then use TweetSharp to get the latest updates and store them in a list.

We also make sure that statuses isn’t null because if you’ve exceed the Twitter API rate limit, TweetSharp returns a null collection. Once we have the updates we have to process them since the text doesn’t include links for @mentions, #hashtags or links. When deciding how to approach this part I contacted Jon Galloway and he pointed me to the code in Witty which handles creating links, which I borrowed heavily from (thanks Jon!). MA also uses SubSonic 2.2 for the DAL and I used one of the methods provided in that library for determining if a word is a URL.

if (statuses != null) {
    foreach (var status in statuses) {
        string raw = status.Text;
        string[] words = Regex.Split(raw, @"([ \(\)\{\}\[\]])");
        StringBuilder output = new StringBuilder();
        foreach (string word in words) {
            if (word.StartsWith("#")) {
                string hashtag = String.Empty;
                Match foundHashtag = Regex.Match(word, @"#(\w+)(?<suffix>.*)");
                if (foundHashtag.Success) {
                    hashtag = foundHashtag.Groups[1].Captures[0].Value;
                    output.Append(string.Format(@"#<a href=""http://search.twitter.com/search?q=%23{0}"" target=""_blank"">{0}</a>", hashtag));
                }
            }
            else if (word.StartsWith("@")) {
                string userName = String.Empty;
                Match foundUserName = Regex.Match(word, @"@(\w+)(?<suffix>.*)");
                if (foundUserName.Success) {
                    userName = foundUserName.Groups[1].Captures[0].Value;
                    output.Append(string.Format(@"@<a href=""http://twitter.com/{0}"" target=""_blank"">{0}</a>", userName));
                }
            }
            else if (SubSonic.Sugar.Validation.IsURL(word)) {
                output.Append(string.Format(@"<a href=""{0}"" target=""_blank"">{0}</a>", word));
            }
            else {
                output.Append(word);
            }
        }
 
        status.Text = output.ToString();
    }
 
    cache.Store("last_fetch", DateTime.Now);
}

OK now that we’ve got the latest updates from the API and added the links, it’s time to store them in the cache.

var cachedStatuses = cache.Get<List<TwitterStatus>>("cachedStatuses");
if (cachedStatuses == null) {
    cachedStatuses = new List<TwitterStatus>();
}
 
if (statuses != null) {
    cachedStatuses.AddRange(statuses);
}
 
if (cachedStatuses.Count > 0) {
    maxId = cachedStatuses.Max(s => s.Id);
    cache.Store("last_id", maxId);
}
 
cache.Store("cachedStatuses", cachedStatuses);

Last but not least we build a JSON return value for all the tweets in the cache with an ID higher than the one sent by the client (lid is a parameter passed from the client).

long lastClientId = lid ?? 1;
 
var data = from s in cachedStatuses
           where s.Id > lastClientId
           orderby s.Id descending
           select new {
               Id = s.Id,
               ProfileImageUrl = s.User.ProfileImageUrl,
               Text = s.Text,
               Source = s.Source,
               UserName = s.User.ScreenName,
               RelativeTime = s.CreatedDate.ToRelativeTime(false),
               ClientLink = s.Source,
               IsReply = s.InReplyToStatusId != 0,
               InReplyToId = s.InReplyToStatusId,
               InReplyToUser = s.InReplyToScreenName
           };
 
return Json(new { results = data.Take(100), max_id = maxId });

In Part 2 I’ll cover building the UI.

Posted May 25th, 6:51 PM
Read more posts about .NET, ASP.NET MVC, C#, Managed Assembly, Programming, SubSonic, Tips, jQuery.

View Comments
Link

Introducing ManagedAssembly.com

My two favorite programming-related sites right now are StackOverflow and Hacker News. StackOverflow suffers from a lot of “off-topic” questions that are really fit for a discussion group more than a question/answer site. Hacker News is a discussion and social news site for hackers and startups (with a programming bent) hosted on an extremely well-built web app. While the app itself is nice, the community there is even better.

With a site like Hacker News, the value is the community. They act as a filter. I rarely visit a link that comes across the HN front-page news feed without first reading the comments to see if its worth my time. There’s also value in building a place where you can converse with the same group of people for each new topic that comes up. This sounds obvious, but a common question that comes up on HN is “Does Hacker News steal the discussion from the original site/blog/etc that was submitted?” I don’t see a discussion on Hacker News with people who’s viewpoints I’ve become familiar with the same as a discussion with random people in blog comments. I think this is a big part of what keeps people coming back to HN: quality discussion with people you share a collective context with.

Hacker News also succeeds because of the moderation policies, which I won’t cover in any detail since Atwood did plenty of that recently.

Another site I frequent is DotNetKicks.com to get the latest .NET news. Unfortunately, DNK was not built around facilitating discussions which is apparent from the extremely low number of contents posted there. The ‘Shoutbox’ shows hints of “life” (in a discussion sense), but in recent times it’s gone nearly dormant (possible defection to DotNetShoutOut?).

DNK also suffers from an antiquated ranking system that was borrowed from Digg. Stories need a set number of votes to be published to the home page and there they sit until the next story with enough votes knocks them down a notch and eventually off the home page. It’s really easy to game (although, most social news sites are) and there’s not a culture of moderation that can handle the gaming.

I wondered how I could combine the good parts from these three sites into a site targeted to passionate .NET devs where they could get the latest information, take part in quality discussion and be interesting enough to keep them coming back. Building it myself sounded fun, and so ManagedAssembly.com was born.

Managed Assembly was borne out of my desire to combine the best of the aforementioned sites into a site I would actually want to use every day. It borrows heavily from these sites in the following ways:

Hacker News

It won’t take you long to see the inspiration of Managed Assembly in Hacker News. I started with their UI, jQuery-ed it up a little bit, changed some colors, made some other minor changes and off we go. I did this because I think Paul Graham built Hacker News for the right reasons, to build a community for the sake of the community, not for something else. DNK and other link sharing sites fail at building quality communities because they’re designed to generate page views and ad revenue. Paul took his own advice and built HN the way you would build a social news/discussion site if you were doing it for charity. There are no ads. There’s no frills. It just works.

There will be no ads for registered users on Managed Assembly as long as I can sustain it without it. Freeloaders (those not contributing to the community) and RSS feeds may get ads, but not for awhile.

The other big way MA is borrowing from HN is philosophically. HN believes in the ‘Broken Windows Theory’. HN kills off-topic posts with no remorse. Submission titles are cleaned up. Basically, they keep the house in order. Managed Assembly will do the same.

Stack Overflow

While the rep system on Managed Assembly works pretty closely to how Hacker News does, I want it to be more like the Stack Overflow system when it grows up. Users with more rep will get more capabilities. This has not been hashed out yet.

What I’d really like to borrow from Stack Overflow is the user base. I’ve got some ideas in this area, so stay tuned. We’re also going OpenID-only, following SO’s lead.

DotNetSomethingOrOtherLinkSharingSite.com

The only thing we’re copying from DNK and DNSO(?) is the types of links I want to see submitted: anything interesting to the intermediate-to-advanced .NET developer. This includes general-interest programming subjects. Topics that are too basic will be killed at the discretion of the mod(s).

New Stuff

The site isn’t completely a rip off of the others mentioned. I want to add a live aspect to it for events like MIX and PDC. My first attempt at this is a live Twitter tracker for MIX09 content. I wanted to implement a chat so you could have a place to go to discuss the keynotes as they happen, but I ran out of time before MIX. Chat in some form will be integrated in the future.

I also want to integrate Twitter, both into the Live section and other parts of the site. I find Twitter invaluable for keeping up with the latest .NET happenings as they happen, and that gels nicely with the MA MO.

There Will Be Bugs

The site was made by a single developer (me) in ASP.NET MVC. Since I’ve been putting this together in my spare time, I haven’t had the chance to stress test it the way I would like, so you will find bugs. Email me and let me know about them. I’ve also set up UserVoice for handling feature requests.

Check it out, let me know what you think and let’s build a community.

Posted March 18th, 4:18 AM
Read more posts about .NET, ASP.NET MVC, Managed Assembly, Programming.

View Comments
Link

My Projects

ManagedAssembly

RestSharp

jQuery Snippets for Visual Studio 2010

@dotnetlinks on Twitter

SnapLeague