SEO Related Posts by ThinqLinq

Implementing Pingbacks

Recently, I discussed Sending and Receiving TrackBacks on this blog. The TrackBack API is not the only mechanism which blog engines use to communicate between each other about the links that are included in individual posts. Wikipedia lists three methods to keep track of which articles are cross linked: Refback, Trackback, and Pingback. Of these, perhaps the trickiest to implement is the Pingback because it uses the xml-rpc style of communication rather than SOAP or REST that most .Net programmers are familiar with.

Thankfully, Cook Computing has released an open source implementation for xml-rpc which makes programming for it similar to programming .Net services by relying on attributes to specify the methods and contracts that a class consumes. The source and documentation can be found at http://www.xml-rpc.net/. Once we add a reference to the CookComputing.XmlRpcV2.dll, we can begin coding our Pingback implementation.

Unlike WCF, we don't need to perform a lot of configuration steps. We simply add a Generic Handler (.ashx) file and point it to the class that will perform the implementation. In this case, our handler will be called PingbackService.ashx and consists of the following:

<%@ WebHandler Language="VB" Class="LinqBlog.BO.Services.PingbackService" %>

As they say in the Staples advertisement, "That was easy!" Next, we implement the PingbackService class in our business tier. Similar to the Trackback, we'll separate this process into the sending operation and the receiving operation.

Sending Pingbacks

The steps to send a pingback are similar to those for a Trackback:

  • Find the links in our post.
  • Check the post's to see if the hosting server supports the Pingback API
  • Send the pingback to the hosting server's URI

The first step is identical to the TrackBack, so refer to the ParseAndSendTrackbacks method from my previous post for that code. Before we can send the ping, we need to check the server for our link to see if it supports Pingbacks. The Pingback Specification allows for two options for server discovery: based on the presence of a X-Pingback HTTP header looking like:

X-Pingback: http://www.ThinqLinq.com/Api/PingbackService.ashx

Or a <link> tag in the web page as follows:

<link rel="pingback" href="http://www.ThinqLinq.com/Api/PingbackService.ashx" />

Thus we need to check for the presence of either of these auto discovery mechanisms in our check to find the URI for the pingback server. Here's a sample implementation which takes a post's url and returns the url of the server's service (or an empty string if the server doesn't support Pingbacks).

Private Function GetPingbackServer(ByVal destination As String) As String
    Dim destUri As Uri = Nothing
    If Not Uri.TryCreate(destination, UriKind.Absolute, destUri) Then
        'Make sure we have a valid uri and that it isn't from this site (relative uri)
        Return ""
    End If

    Dim server As String = ""
    Dim req = DirectCast(WebRequest.Create(destination), HttpWebRequest)
    req.Referer = "http://www.thinqLinq.com"
    Using resp = DirectCast(req.GetResponse, HttpWebResponse)
        'Check headers for x-Pingback
        server = resp.Headers.Get("x-Pingback")
        If server <> "" Then Return server

        'Check for link element
        If resp.Headers.AllKeys.Contains("Content-Type") AndAlso _
            resp.Headers("Content-Type").StartsWith("text/html") Then

            Dim client As New WebClient()
            client.UseDefaultCredentials = True
            Dim page = client.DownloadString(destination)
            Dim regexString = _
String.Format("<link rel={0}pingback{0} href={0}[a-z0-9:\.\/_\?\-\%]*{0}", _
ControlChars.Quote) Dim match = Text.RegularExpressions.Regex.Match(page, regexString, _
RegexOptions.IgnoreCase).Value If Not String.IsNullOrEmpty(match) Then Dim startIndex As Integer = match.IndexOf("href=") + 6 Dim ret = match.Substring(startIndex, match.Length - startIndex - 1) Return ret End If End If End Using Return "" End Function

In this case, checking the headers is easy. Finding the <link> link tag takes a combination of Regular Expressions and string parsing since the <link> tag can either be HTML or XHTML compliant (and thus we can't use XML parsing on it. Now that we know the address of our post, the address that we're linking to and the address of the linking site's pingback server, we can issue the request to the server using Xmlrpc.Net. Here's the code:

Public Function SendPing(ByVal source As String, ByVal destination As String) As String

    Dim server = GetPingbackServer(destination)
    If Not server = "" Then
        Dim proxy As IPingbackPing = _
DirectCast(XmlRpcProxyGen.Create(GetType(IPingbackPing)), _
IPingbackPing) proxy.Url = server Return proxy.Ping(source, destination) End If Return "" End Function

Typically with the XMLRPC.Net, we specify the server's address in a static attribute on the service type. However, in our case the URL isn't known at compile time. As a result, we use the XmlRpcProxyGen.Create method to create a proxy for the RPC service at runtime. The Create method takes a type as an interface. We define the type with the required attributes as follows:

<XmlRpcUrl("http://ThinqLinq.com/SetAtRuntime.aspx")> _
 Public Interface IPingbackPing
    Inherits IXmlRpcProxy

    <XmlRpcMethod("pingback.ping")> _
    Function Ping(ByVal source As String, ByVal destination As String) As String

End Interface

Notice that the interface does specify a XmlRpcUrl. This is just a place holder which we replace in the SendPing method by setting the proxy.Url to the actual server's address. The act of calling the Ping method is trivial thanks to the generated proxy.

Receiving a Pingback

Switching gears to the server side now, receiving a ping is actually easy to do with the xml-rpc.net implementation. On our class, we inherit from the CookComputing.XmlRpc.XmlRpcService. This takes care of the core handler code and wiring up our method with the RPC call. To associate our method with the RPC method name, we add the CookComputing.XmlRpc.XmlRpcMethod specifying a method name of "pingback.ping" as required by the Pingback specification. This method takes two string parameters: source url and destination url; and returns a message indicating the outcome of the request.

      <XmlRpcMethod("pingback.ping")> _
      Public Function Ping(ByVal source As String, ByVal destination As String) As String
          Using dc As New LinqBlogDataContext
              'Get the post's ID based on the destination url from 
              'the custom URL Rewriting scheme.
              Dim postId As Integer = GetPostId(destination, dc)
              If postId > 0 Then
                  'Make sure we haven't already added this pingback
                  If (From c In dc.CommentBases _
                      Where c.CreatorLink = source And _
                          c.PostId = postId).Count = 0 Then

                      dc.CommentBases.InsertOnSubmit( _
                          New Pingback With {.CreatorLink = source, _
                                             .Description = "Pingback from " & source, _
                                             .EnteredDate = Now, _
                                             .PostId = postId, _
                                             .Creator = "Pingback", _
                                             .CreatorEmail = ""})
                      dc.SubmitChanges()

                      Return "pingback registered successfully"
                  Else
                      Return "pingback already registered"
                  End If
              Else
                  Return "pingback not registered, no post found"
              End If
          End Using
End Function

 

In this case, we need to make sure that a) the destination location does include a reference to a post (through the id in the querystring or via URL Rewriting). If we have a valid ID, we then check to see if there is already a linkback associated with this post and the given source address. We don't want to register duplicate linkbacks. This is particularly important since we are implementing both Postbacks and Trackbacks and the calling site could send both requests. We only want to register one. Assuming we have a new Pingback for this post, we will create a new Pingback object (which inherits from CommentBase) and set the necessary values. With LINQ to SQL, applying the change is the standard SubmitChanges call on the context. We finish by letting the client know what happened in our call. This is mostly a courtesy as the specification doesn't require a specific response outside of exception codes.

Feel free to pingback (or trackback) to this post to let me know if this series of posts has been helpful to you.

Posted on - Comment
Categories: VB Dev Center - VB - LINQ - SEO -

Sending TrackBacks

Yesterday, I showed how we can receive trackbacks from other sites using the TrackBack API. Today, we'll look at the other side of this picture: sending TrackBacks to other sites based on links in the post. Sending a TrackBack entails several steps:

  1. Parsing the post to find links to other sites.
  2. Checking the target site to see if it supports TrackBacks.
  3. Formatting and sending the TrackBack to the target's service.
  4. Checking for error responses.

Thus with every post I create now, I include a method to parse the post and send the trackbacks as necessary. Parsing the post is a relatively painless process. In it, I query the raw HTML of the post finding anything that looks like href="….". Although I typically use LINQ for querying, in this case the best tool for the job is a regular expression. I admit that I'm not the best at regular expressions, so if anyone has a better alternative, let me know. Here's the method that starts the parsing process:


Public Shared Sub ParseAndSendTrackbacks(ByVal post As PostItem)
    'find references that support trackbacks
    Dim pattern As String = String.Format("href={0}[a-z0-9:\.\/_\?\-\%]*{0}", _
                                          ControlChars.Quote)
    Dim matches = Regex.Matches(post.Description, pattern, RegexOptions.IgnoreCase)
    For Each Link In matches.OfType(Of RegularExpressions.Match)()
        Try
            Dim startIndex = Link.Value.IndexOf(ControlChars.Quote) + 1
            Dim urlPart As String = Link.Value.Substring(startIndex, _
                                        Link.Value.Length - startIndex - 1)
            Dim svc As New TrackbackService
            svc.SendTrackback("http://www.ThinqLinq.com/Default/" & _
                              post.TitleUrlRewrite & ".aspx", post, urlPart)
        Catch ex As Exception
            Trace.Write(ex.ToString)
        End Try
    Next
End Sub

As you can see, this consists mostly of the Regex match and some string parsing to get down to the url that we are referring to. We then send that url into a method which will send the trackback.


Public Sub SendTrackback(ByVal postUrl As String, ByVal post As PostItem, ByVal externalUrl As String)

    Dim server = GetTrackbackServer(externalUrl)
    If server <> "" Then
        'Send the trackback to the sever
    End If
End Sub

Here, the first step is to see if the post that we are referencing in our post supports the trackback API. The trackback API has an auto-discovery mechanism whereby the page needs to include an embedded RDF which specifies the URI that a client should ping when it wants to issue a trackback. On this site, the trackback server URI for my Receiving Trackbacks post is:

<!--
    <rdf:RDF xmlns:rdf=http://www.w3.org/1999/02/22-rdf-syntax-ns#
      xmlns:dc=http://purl.org/dc/elements/1.1/
      xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
      <rdf:Description rdf:about=http://www.ThinqLinq.com/About.aspx
       dc:identifier="http://www.foo.com/Default.aspx" dc:Title="Thinq Linq"
       trackback:ping="http://ThinqLinq.com/Trackback.aspx?id=22050" />
    </rdf:RDF>
-->

In this case, notice that the XML is included inside of a comment block. The TrackBack Technical Specification indicates that this is required by some validators. Since we know that the site needs to include a node like this, we can issue a request to the page of the post that we are wanting to send a post to and see if it includes the appropriate rdf response information. We can't assume that the page will be XHTML compliant, so we will use another regular expression to find the <rdf:RDF></rdf:RDF> node and then use XML literals with VB 9 to parse it to get the value of the trackback:ping. Since the XML is strongly typed with namespaces, we'll start by including the appropriate imports to our class file. If we don't include these imports, our LINQ query will fail because the namespaces are required when querying XML.

Imports <xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
Imports <xmlns:dc="http://purl.org/dc/elements/1.1/">
Imports <xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">

With the namespaces in place, we can proceed with the rest of our code to find the TrackBack server's URI. This method will return the server's address or an empty string if the server is not found for any reason.

Private Function GetTrackbackServer(ByVal externalUrl As String) As String
    Try
        'Make sure that we have a valid external link
        Dim linkUri As Uri = Nothing
        If Not Uri.TryCreate(externalUrl, UriKind.Absolute, linkUri) Then
            Return ""
        End If
        Dim server As String = ""

        'Set up the HTTP request
        Dim req = DirectCast(WebRequest.Create(linkUri), HttpWebRequest)
        req.Referer = "http://www.thinqLinq.com"
        Dim client As New WebClient()
        client.UseDefaultCredentials = True

        'Get the page's contents
        Dim page = client.DownloadString(externalUrl)

        'Find the rdf tag
        Dim regexString = String.Format("<rdf:rdf(.*?)rdf:rdf>")
        Dim match = Text.RegularExpressions.Regex.Match(page, regexString, _
RegexOptions.IgnoreCase Or RegexOptions.Singleline).Value If Not String.IsNullOrEmpty(match) Then 'Use LINQ to XML to fet the trackback:ping attribute's value Dim rdf = XDocument.Parse(match) Dim url = rdf.Root.<rdf:Description>.FirstOrDefault.@trackback:ping If Not String.IsNullOrEmpty(url) Then Return url End If End If Catch ex As Exception Diagnostics.Trace.WriteLine(ex.ToString()) End Try 'Something didn't work right, or the site doesn't support TrackBacks. Return "" End Function

Now that we know the server, we can finish off the process of sending our request to the server that we started earlier. In this case, we create the request setting the method to "POST" and the ContentType to "application/x-www-form-urlencoded". We then build our form's values that we make sure to UrlEncode and place in the request's content stream.

Public Sub SendTrackback(ByVal postUrl As String, ByVal post As PostItem, ByVal externalUrl As String)

    Dim server = GetTrackbackServer(externalUrl)
    If server <> "" Then
        Dim req = DirectCast(WebRequest.Create(server), HttpWebRequest)
        req.Method = "POST"
        req.ContentType = "application/x-www-form-urlencoded"
 
        Dim content As New StringBuilder()
        content.AppendFormat("url={0}", HttpUtility.UrlEncode(postUrl))
        content.AppendFormat("&title={0}", HttpUtility.UrlEncode(post.Title))
        content.Append("&blog_name=ThinqLinq")
        content.AppendFormat("&excerpt={0}", _
                             HttpUtility.UrlEncode(StripHtml(post.Description, 200)))

        Dim contentBytes() = System.Text.Encoding.ASCII.GetBytes(content.ToString())
        req.ContentLength = contentBytes.Length

        Using reqStream = req.GetRequestStream
            reqStream.Write(contentBytes, 0, contentBytes.Length)
        End Using

        Dim resp = req.GetResponse()
        Using respStream = resp.GetResponseStream()
            Dim reader As New StreamReader(respStream)
            Dim response = XDocument.Parse(reader.ReadToEnd())
            'Check for errors
            If CDbl(response...<error>.FirstOrDefault()) > 0D Then
                Throw New InvalidOperationException(response...<message>.FirstOrDefault().Value)
            End If
        End Using
    End If
End Sub

Once we send the request, we make sure to check the response to see if there were any errors. As we mentioned yesterday, the TrackBack Technical Specification requires that the response be XML in the following format:

<?xml version="1.0" encoding="utf-8"?>
<response>
   <error>1</error>
   <message>The error message</message>
</response>

Since it is XML, it is easy to parse this using LINQ to XML. If an error is found, we raise an exception including the contained error message from the server.

Posted on - Comment
Categories: VB - VB Dev Center - LINQ - SEO -

Receiving Trackbacks

If you've been following along, I've been working on enhancing this site a bit recently. A couple of the most recent enhancements can be found in the following posts:

Continuing in this tradition, I wanted to include the ability to be notified when other sites post links to my posts. There are several such API's that support this kind of notification, including Trackbacks and Pingbacks. In this post, we'll look at receiving Trackbacks and saving them to our database (using LINQ of course).

The technical specification for Trackbacks is hosted at http://www.movabletype.org/documentation/developer/callbacks/ . To implement the listener part of the trackback, we simply need to be able to accept a HTTP Post which includes the post id in the URI and the trackback's details as form parameters. For example, if I wanted to send a trackback for my Paging with AJAX post, I would issue the following request:

POST http://ThinqLinq.com/Trackback.aspx?id=22040
Content-Type: application/x-www.form-urlencoded; charset=utf-8

title=Test+Postback&url=http://www.fansite.com/&excerpt=Good+post&blog_name=Fansite

In this request, the URI specifies the name of the trackback server including the ID of the post we are tracking: http://ThinqLinq.com/Trackback.aspx?id=22040. The trackback API specifies that the Content-Type should always be application/x-www.form-urlencoded; charset=utf-8. The body specifies the values we want the trackback server to know about. Each of these are optional. In this case, we want to send the server a trackback for a post called "Test Postback" which can be found at http://www.fansite.com on the blog that is named "Fansite". To be nice, we'll include an excerpt of our post with the value of "Good post". Because the content type needs to be urlencoded, we need to make sure that each value is encoded properly. To summarize, following parameter values would be sent for our test request:

  • title=Test Postback
  • url=http://www.fansite.com/
  • excerpt=Good Post
  • blog_name=Fansite

One tool to issue a raw HTTP request to test this is Fiddler. Inside Fiddler, we can select the Request Builder tab. Select "POST" as the method and enter the URI that we wish to post to (http://ThinqLinq.com/Trackback.aspx?id=22040). We then set the content type in the Request Headers and our post content in the Request Body. Below is a screen shot to send this request using Fiddler.

TrackbackFiddler

We'll discuss what we need to do to send this request from code in a later post. For now, we'll focus on receiving this request. While I wanted to do this with WCF, implementing it as a standard Web page is the easiest. As a result, we'll create a page called Trackback.aspx. Since there is no real UI on this, we'll remove everything from the page except for the pointer to the page's code file:

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Trackback.aspx.vb" Inherits="Trackback" %>

On the page load, we need to clear our buffer because we're going to just send a simple XML response indicating any errors we may experience.

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Response.Buffer = True
        Response.Clear()
        Response.Cache.SetCacheability(HttpCacheability.NoCache)
        Response.ContentType = "text/xml"

Next, we need to create the object that we want to persist in our database. We already have a Comment table which maps to our Comments. In many ways Trackbacks are like Comments, so we'll use the same table. Since they do have different behaviors, we'll use LINQ to SQL's ability to support inheritance. Thus we'll add a discriminator column on our Comment table called CommentType and default the value to "C" since most of the items in that table will be comments.

Next, in our DBML we need to indicate the new mapping. Our existing comment class will now be an abstract base class called CommentBase. We'll change it's Inheritance Modifier to MustInherit as a result. We'll then add three derived types for Comment, Trackback, and Pingback to model the behaviors we'll be implementing now. We'll also need to make sure to Inheritance Discriminator values of each of the inheritance arrows. When we're done, this piece of our model now looks as follows:

TrackbackDbml

Now that we have our new Trackback type, we can instantiate it on our Page_Load with the form values that we get from our page's request fields.

      Dim trackback As New LinqBlog.BO.Trackback
      Dim id As Integer
      If Not Integer.TryParse(Request("id"), id) Then
          Return CreateFailureMessage("Id invalid")
      End If

      trackback.PostId = ID
      trackback.Creator = If(Request.Params("blog_name"), "")
      trackback.CreatorEmail = String.Empty
      trackback.Description = _
          String.Format("Trackback from &lt;a href={0}{1}{0}&gt;{2}&lt;/a&gt", _
                        ControlChars.Quote, _
                        If(Request.Params("url"), ""), _
                        HttpUtility.UrlDecode(If(Request.Params("title"), "")))
      trackback.EnteredDate = Now
      trackback.CreatorLink = If(Request.Params("url"), "")

We do need to decode the Url encoded values. We also need to watch out for missing values because each of the form values are optional. In this case, we'll use VB 9's support for ternary If. Also notice here that we aren't setting the CommentType field. Since we are using the discriminator column and set that in LINQ to SQL's metadata, when we save Trackback objects, it will automatically assign the type to "T" for us.

Now that we have our trackback object, adding it to the database with LINQ is trivial. However, we do need to make one additional check before saving the trackback. We don't want to save multiple trackbacks for a single post. Thus we'll check to see if there are any trackbacks for this post in the database already before saving this one:

            Using dc As NewLinqBlogDataContext
                'Make sure we don't already have a comment for this post from this url
               
If(Aggregatetb Indc.CommentBases.OfType(OfTrackback)() _
                   Wheretb.PostId = id And_
                        tb.CreatorLink = values.Url _
                    IntoCount()) = 0 Then

                   
'Add it
                   
dc.CommentBases.InsertOnSubmit(trackback)
                    dc.SubmitChanges()

                End If
            End Using

We're almost done. The last step is to send the appropriate response to the sending system as required by the Trackback API specification. This is a simple XML response containing any error messages. With XML Literals, this is a simple copy paste operation from the Trackback API:

 

    Private Function CreateSuccessMessage() As String
        Return <?xml version="1.0" encoding="utf-8"?>
               <response>
                   <error>0</error>
               </response>.ToString()
    End Function

    Private Function CreateFailureMessage(ByVal description As String) As String
        Return <?xml version="1.0" encoding="utf-8"?>
               <response>
                   <error>1</error>
                   <message><%= description %></message>
               </response>.ToString()
    End Function

With this in place, we just add the following inside of our If statement after we save the value to the database:

    Response.Write(CreateSuccessMessage)
    Response.End()

If there were any issues in creating or saving our trackback, we can send the Response.Write(CreateFailureMessage(error)).

Feel free to test this by tracking back to this post. Be aware that I am moderating the trackbacks to avoid comment spam that can be inevitable when exposing public known API's like this. Please don't abuse the ability to send trackbacks as I don't want to have to remove the functionality.

Posted on - Comment
Categories: VB - VB Dev Center - LINQ - SEO -

Adding Gravatar support to comments

Having a bit more free time than expected, I thought I would take a bit of time and add some features to this site based on some things I've seen at other sites. A quick one is adding Gravatar support to the comments. If you're not familiar with gravatar's, www.Gravatar.com describes them as a globally recognized avatar, is quite simply an image that follows you from site to site appearing beside your name when you do things.

To get a Gravatar, go to Gravatar.com and let them know your email address and the picture you want associated with your picture. Once you are done, any site that supports gravatar's will then show your picture to the world. That's your task.

My task is to enable this site to access your picture. They have a number of add-ins for various blogging engines, but since this one is custom, we'll have to implement it ourself. Luckly it is as simple as adding a img tag to our site with the source being a URI which includes information about the user that we want to display. Here's a sample URI which would show my picture:

<img src="http://www.gravatar.com/avatar.php?gravatar_id=58d453f6449cc9125948bd153bc4272b&rating=G&size=40" alt="Gravatar" />

Let's break the source attribute down a bit. Essentially it is a URI to a PHP site on the Gravatar server: http://www.gravatar.com/avatar.php. It includes three query string parameters: the gravatar_id, rating and size.

The rating and size are easy enough. For this site, we're going to keep it clean (although we do get Kinq-y at times) so we'll keep the site rated G. Other sites could use one of their other rating levels (g, pg, r, or x).

For the size, you can specify that the image be anywhere between 0 and 512 pixels. To keep the page load small here, I'll ask for images 40 px by 40 px and set the size=40.

With that out of the way, we need to generate the value for the gravatar_id paramter. In a nutshell, the id is just a MD5 hash of the commentor's email address. When we set-up the ability to add comments to this site, we made the email address a required field, so we are already storing that. All we need to do is convert it and bind to it in our custom img tag. To encapsulate that functionality and keep it with the comments themselves, we will add a partial class for Comments and put our new property in there. We don't add it directly to the Comment class that the dbml file generates as it will be deleted in the future if we ever decide to regenerate that file. With partial classes, we can retain parts of the class definition in multiple physical files and the compiler will combine them within the generated assembly. Here's the definition of this class and our new property:


Public Class Comment
    Public ReadOnly Property GravatarSource() As String
        Get
        End Get
    End Property
End Class

Notice here, we don't need to specify that this is a partial class because the one generated by our DBML designer already includes the partial destinction. As long as we're in the same namespace, in VB, we're fine. I should point out however that there are some limitations on how we can use this partial property in LINQ queries (see http://www.thinqlinq.com/Default/Projecting-into-an-unmapped-property-from-a-LINQ-to-SQL-query.aspx).

Now for the getting this value. To make binding simple, we'll just format the entire URI in this method (we're using a property here due to limitations in data binding to methods). Using the String.Format method, we can insert the hash into our uri using the following:

Return String.Format("http://www.gravatar.com/avatar.php?gravatar_id={0}&rating=G&size=40", GetEmailHash())

The body of the GetEmailHash function is where the meat of our work happens. In this, we will encode the value of the Commentor's email address which we can access from the other part of the partial class as the CreatorEmail property. To do that, we need to encode the string into a byte array. Then, using a MD5CryptoServiceProvider instance, we can compute the hash into a new byte array.


Dim enc As New UTF8Encoding()
Dim hashProvider As New MD5CryptoServiceProvider
Dim bytes() As Byte = hashProvider.ComputeHash(enc.GetBytes(Me.CreatorEmail))

Finally, we need to piece the encrypted array back into a string. In the case of the Gravatar system, each byte needs to be converted back to the HEX represntation and lower cased. We could use a for-each loop and iterate over the results building it up dynamically, but this is a great case of using LINQ to Objects to replace an iteration:

From b In bytes Select b.ToString("X2").ToLower()

We can then concatenate the resulting HEX strings using the String.Join. Here's the completed definition of this class:


Imports System.Security.Cryptography
Imports System.Text
Imports System.IO 

Public Class Comment
    Public ReadOnly Property GravatarSource() As String
        Get
            'We need the MD5 hash of the email address
            Return String.Format("http://www.gravatar.com/avatar.php?gravatar_id={0}&rating=G&size=40", GetEmailHash())
        End Get
    End Property
    Private Function GetEmailHash() As String
        Dim enc As New UTF8Encoding()
        Dim hashProvider As New MD5CryptoServiceProvider
        Dim bytes() As Byte = hashProvider.ComputeHash( _
                           enc.GetBytes(Me.CreatorEmail))
        Return String.Join("", _
                           (From b In bytes _
                            Select b.ToString("X2").ToLower()) _
                            .ToArray())
    End Function
 End Class

Now, to add this property to our UI. Since we are already set up to bind to the comment object in our CommentRepeater control, we just add a new line to specify the img tag:

<a href="http://www.gravatar.com" title="Get your avatar"><img width="40" height="40" style="float: right; padding-left: 10px;" src="<%# Eval("GravatarSource") %>" alt="Gravatar" /></a>

That's it. If you want to see the gravatar in action, head on over to their site and sign up. Then come back here and leave a comment on this post. I'd love to see the faces of people who enjoy this site.
Posted on - Comment
Categories: VB Dev Center - LINQ - VB - SEO -

Adding a dynamic SiteMap for search engine optimization using LINQ

A couple months ago I added a feature to this site to build a Site Map for this site dynamically based on the information from the database for posts and files for the downloads. If your not familiar with how Sitemap files can help your site searchability, Google has a good documentation about Sitemaps in their Webmaster tools.

The SiteMap Protocal is a rather simple XML document consisting of a set of url nodes that consist of the following:

  • loc - URL for the page link
  • lastmod - Date the page was last modified
  • changefreq - How often the page is changed
  • priority - How high you think the page should be ranked relative to other pages on your site.

For this site, I decided to index the main (default.aspx) File, about and contact pages. In addition, I indexed each post as a separate url node. If you want to view the resulting data, browse to http://www.thinqlinq.com/sitemap.aspx. To do this, I used LINQ to XML with VB XML Literals. To begin, we need to add the XML Namespaces. At the top of our file, we enter the following imports:

Imports <xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
Imports <xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

We continue by adding the root urlset node and one child node representing the main page:

Dim map = _

    <?xml version='1.0' encoding='UTF-8'?>

    <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9

            http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

        xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

 

        <url>

            <loc>http://www.ThinqLinq.com/default.aspx</loc>

            <lastmod>

               <%= (From p In dc.PostItems _

                    Order By p.PublicationDate Descending) _

            .First.PublicationDate.ToString("yyyy-MM-dd") %>

            </lastmod>

            <changefreq>daily</changefreq>

            <priority>1.0</priority>

        </url>

    </urlset>

 

Most of this is standard XML. The main difference is the use of a LINQ query to show the last modification date based on the most recent post from our database. In this case we just want the First date when the dates are ordered descending. We do need to format it properly so that our search engine (Google) will be able to recognize it.

Next up, we need to add the link for the Downloads page. We'll do this much the same way that we added the url for the default page. However, in this case the modification date won't come from the database, but rather use a LINQ to Objects query to get the most recent file in the downloads directory on this site.

<url>

    <loc>http://www.ThinqLinq.com/Files.aspx</loc>

    <lastmod>

        <%= (From f In New System.IO.DirectoryInfo( _

            Server.MapPath("~/Downloads")).GetFiles _

            Order By f.LastWriteTime Descending) _

            .FirstOrDefault.LastWriteTime.ToString("yyyy-MM-dd") %>

    </lastmod>

    <changefreq>weekly</changefreq>

    <priority>1.0</priority>

</url>

The About and Contact pages are relatively straight forward. The remaining url nodes are generated based on the records in the PostItems from our database. To populate them, we'll create a LINQ query pulling the data from the database using LINQ to SQL and projecting (Select) out individual url nodes for each row in the database:

<%= From p In dc.PostItems.ToList _

    Select <url>

               <loc>http://www.ThinqLinq.com/default/<%= p.TitleUrlRewrite %>.aspx</loc>

               <lastmod><%= p.PublicationDate.ToString("yyyy-MM-dd") %></lastmod>

               <changefreq>daily</changefreq>

               <priority>0.3</priority>

           </url> %>


As you can see, there isn't much here that is overly complex. It's just a series of LINQ queries filling the data from various sources. For reference purposes, Here's the complete code:

Imports <xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

Imports <xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

 

Partial Class SiteMap

    Inherits System.Web.UI.Page

 

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        Response.Buffer = True

        Response.Clear()

        Response.Cache.SetCacheability(HttpCacheability.NoCache)

        Response.ContentType = "text/xml"

        Response.AddHeader("Content-Disposition", "inline;filename=blog.rss")

        WriteRss()

        Response.End()

    End Sub

 

    Private Sub WriteRss()

        Try

            Using dc As New LinqBlog.BO.LinqBlogDataContext

                Dim map = _

                    <?xml version='1.0' encoding='UTF-8'?>

                    <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

                        xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

 

                        <url>

                            <loc>http://www.ThinqLinq.com/default.aspx</loc>

                            <lastmod>

                                <%= (From p In dc.PostItems _

                                    Order By p.PublicationDate Descending) _

                                    .First.PublicationDate.ToString("yyyy-MM-dd") %>

                            </lastmod>

                            <changefreq>daily</changefreq>

                            <priority>1.0</priority>

                        </url>

                        <url>

                            <loc>http://www.ThinqLinq.com/Files.aspx</loc>

                            <lastmod>

                                <%= (From f In New System.IO.DirectoryInfo( _

                                    Server.MapPath("~/Downloads")).GetFiles _

                                    Order By f.LastWriteTime Descending) _

                                    .FirstOrDefault.LastWriteTime.ToString("yyyy-MM-dd") %>

                            </lastmod>

                            <changefreq>weekly</changefreq>

                            <priority>1.0</priority>

                        </url>

                        <url>

                            <loc>http://www.ThinqLinq.com/about.aspx</loc>

                            <lastmod>

                                <%= System.IO.File.GetLastWriteTime( _

                                    Server.MapPath("About.aspx")).ToString("yyyy-MM-dd") %>

                            </lastmod>

                            <changefreq>monthly</changefreq>

                            <priority>1.0</priority>

                        </url>

                        <%= From p In dc.PostItems.ToList _

                            Select <url>

                                       <loc>http://www.ThinqLinq.com/default/<%= p.TitleUrlRewrite %>.aspx</loc>

                                       <lastmod><%= p.PublicationDate.ToString("yyyy-MM-dd") %></lastmod>

                                       <changefreq>daily</changefreq>

                                       <priority>0.3</priority>

                                   </url> %>

 

                        <url>

                            <loc>http://www.ThinqLinq.com/Contact.aspx</loc>

                            <lastmod>2008-02-28</lastmod>

                            <changefreq>never</changefreq>

                            <priority>0.1</priority>

                        </url>

                    </urlset>

                Response.Write(map)

            End Using

        Catch ex As Exception

            Response.Write(<error><%= ex.ToString %></error>)

        End Try

 

    End Sub

End Class

Posted on - Comment
Categories: VB Dev Center - LINQ - VB - VS 2008 - SEO - Linq to XML -

Feedburner access to Wooleys various Wonderings

I have set-up several feedburner syndication options for my postings for your enjoyment. Feel free to move your aggregator over to those versions of the feeds and show me how much you digg what I have to say. Here's the links for you:

Wooley's Wonderings: My main feed
Jim's Samples and Presentations: My downloads and sample applications from my presentations. Subscribe here to get updates whenever I add new samples.

I have a bunch of things I want to discuss from my DevConnections trip, so stay tuned.

 

Updated 1/20/2008:

You can also access the feed at http://feeds.feedburner.com/thinqlinq/rss for the ThinqLinq feed.

Posted on - Comment
Categories: SEO -