Social Linking to Post Pages

Jan 22, 2014 at 2:58 AM
I am having a pretty big problem in that when I post the URL of post-specific pages to social networks it is pulling the page name, title, description, and images from the home page of the blog, not the page I'm trying to share.

I just upgraded to 2.8 and I'm fairly certain this wasn't an issue in 2.7 as I've never seen this problem before.

I can only assume this is a bug? Or has this been possibly moved to a setting somewhere?
Jan 22, 2014 at 3:17 PM
Not sure why it worked in 2.7 and not thereafter, but if that's the case then you probably need to specifically add meta tag cues to post headers.

The Facebook debugger is pretty good at revealing where problems lie and if the necessary tags are present.

Here's some code that can be used to add the meta tags, there are some dependencies, so you won't be able to drop this into the extensions folder without some modification.

SocialFormatCues.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.Linq;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using BlogEngine.Core.Web.Extensions;
using MyThemeTools;

/// <summary>
/// Summary description for SocialFormatCues
/// </summary>
[Extension("Adds meta tags to control appearance of post previews in share boxes for social networking services", "1.0", "Contact")]
public class SocialFormatCues
{
    /// <summary>
    /// Constructor, add Post.Serving handler.
    /// </summary>
    public SocialFormatCues()
    {
        Post.Serving += AddMetaTags;
    }

    // First post image src as absolute URL.
    private string imgPath = string.Empty;

    /// <summary>
    /// Adds meta tags to page header.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void AddMetaTags(object sender, ServingEventArgs e)
    {
        if (!ExtensionManager.ExtensionEnabled("SocialFormatCues"))
        {
            return;
        }
        if (e.Location == ServingLocation.SinglePost)
        {
            Post p = (Post)sender;
            HttpContext context = HttpContext.Current;

            imgPath = cUtils.GetFirstImgSrc(p.Content, false);

            if (!string.IsNullOrEmpty(imgPath))
            {
                if (!imgPath.ToLower().StartsWith("http"))
                {
                    imgPath = context.Request.Url.GetLeftPart(UriPartial.Authority) +
                        (imgPath.StartsWith("../") ? "/" + imgPath.Replace("../", "") : imgPath);
                }
            }

            if (context.CurrentHandler is System.Web.UI.Page)
            {
                System.Web.UI.Page page = (System.Web.UI.Page)context.CurrentHandler;

                AddOpenGraphTags(page, p);
                AddTwitterTags(page, p);
            }
        }
    }

    private void AddMetaTag(System.Web.UI.Page page, string propertyName, string property, string content)
    {
        if (!string.IsNullOrEmpty(property) && !string.IsNullOrEmpty(content))
        {
            HtmlMeta metaTag = new HtmlMeta();
            metaTag.Attributes.Add(propertyName, property);
            metaTag.Attributes.Add("content", content);
            page.Header.Controls.Add(metaTag);
        }
    }

    // og tags will be recognized by Facebook, Google, LinkedIn, Pinterest and many others.
    private void AddOpenGraphTags(System.Web.UI.Page page, Post p)
    {
        string propertyName = "property";
        AddMetaTag(page, propertyName, "og:url", p.AbsoluteLink.AbsoluteUri);
        AddMetaTag(page, propertyName, "og:title", p.Title);
        AddMetaTag(page, propertyName, "og:description", cUtils.CropContent(p, 200));

        if (!string.IsNullOrEmpty(imgPath))
        {
            AddMetaTag(page, propertyName, "og:image", imgPath);
        }
    }

    private void AddTwitterTags(System.Web.UI.Page page, Post p)
    {
        string propertyName = "name";
        AddMetaTag(page, propertyName, "twitter:card", "summary");
        AddMetaTag(page, propertyName, "twitter:site", "@" + BlogSettings.Instance.Name.ToLower());
        AddMetaTag(page, propertyName, "twitter:url", p.AbsoluteLink.AbsoluteUri);
        AddMetaTag(page, propertyName, "twitter:title", p.Title);
        AddMetaTag(page, propertyName, "twitter:description", cUtils.CropContent(p, 140));

        if (!string.IsNullOrEmpty(imgPath))
        {
            AddMetaTag(page, propertyName, "twitter:image", imgPath);
        }
    }

}
Dependencies
Methods beginning cUtils. are kept in a helper file.

cUtils.GetFirstImgSrc gets the first image source or default image.
cUtils.CropContent truncates post or page content to the specified number of chars and adds ellipses.
Content is considered to be the post/page description or the body striped of HTML.
Jan 22, 2014 at 7:17 PM
I'm a bit lost. I assume you want me to put this in the /extensions folder, but where is this helper file you speak of? Is there anything else required to make an extension work?

Thanks.
Jan 22, 2014 at 10:18 PM
The helper file is just something I use for commonly used utilities, wasn't sure if maybe you already had something like that and might have wanted to use your own methods for getting an image source and cropping content. Anyway, here's the code with required helper methods appended. If you create a new class file and save it as "SocialFormatCues" and paste over with the following you can drop the file into your extensions folder. Probably worth checking a post with the Facebook debugger before and after.
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.Linq;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using BlogEngine.Core.Web.Extensions;
using System.Text.RegularExpressions;

/// <summary>
/// Summary description for SocialFormatCues
/// </summary>
[Extension("Adds meta tags to control appearance of post previews in share boxes for social networking services", "1.0", "Contact")]
public class SocialFormatCues
{
    /// <summary>
    /// Constructor, add Post.Serving handler.
    /// </summary>
    public SocialFormatCues()
    {
        Post.Serving += AddMetaTags;
    }

    // First post image src as absolute URL.
    private string imgPath = string.Empty;

    /// <summary>
    /// Adds meta tags to page header.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void AddMetaTags(object sender, ServingEventArgs e)
    {
        if (!ExtensionManager.ExtensionEnabled("SocialFormatCues"))
        {
            return;
        }
        if (e.Location == ServingLocation.SinglePost)
        {
            Post p = (Post)sender;
            HttpContext context = HttpContext.Current;

            imgPath = GetFirstImgSrc(p.Content);

            if (!string.IsNullOrEmpty(imgPath))
            {
                if (!imgPath.ToLower().StartsWith("http"))
                {
                    imgPath = context.Request.Url.GetLeftPart(UriPartial.Authority) +
                        (imgPath.StartsWith("../") ? "/" + imgPath.Replace("../", "") : imgPath);
                }
            }

            if (context.CurrentHandler is System.Web.UI.Page)
            {
                System.Web.UI.Page page = (System.Web.UI.Page)context.CurrentHandler;

                AddOpenGraphTags(page, p);
                AddTwitterTags(page, p);
            }
        }
    }

    private void AddMetaTag(System.Web.UI.Page page, string propertyName, string property, string content)
    {
        if (!string.IsNullOrEmpty(property) && !string.IsNullOrEmpty(content))
        {
            HtmlMeta metaTag = new HtmlMeta();
            metaTag.Attributes.Add(propertyName, property);
            metaTag.Attributes.Add("content", content);
            page.Header.Controls.Add(metaTag);
        }
    }

    // og tags will be recognized by Facebook, Google, LinkedIn, Pinterest and many others.
    private void AddOpenGraphTags(System.Web.UI.Page page, Post p)
    {
        string propertyName = "property";
        AddMetaTag(page, propertyName, "og:url", p.AbsoluteLink.AbsoluteUri);
        AddMetaTag(page, propertyName, "og:title", p.Title);
        AddMetaTag(page, propertyName, "og:description", CropContent(p, 200));

        if (!string.IsNullOrEmpty(imgPath))
        {
            AddMetaTag(page, propertyName, "og:image", imgPath);
        }
    }

    private void AddTwitterTags(System.Web.UI.Page page, Post p)
    {
        string propertyName = "name";
        AddMetaTag(page, propertyName, "twitter:card", "summary");
        AddMetaTag(page, propertyName, "twitter:site", "@" + BlogSettings.Instance.Name.ToLower());
        AddMetaTag(page, propertyName, "twitter:url", p.AbsoluteLink.AbsoluteUri);
        AddMetaTag(page, propertyName, "twitter:title", p.Title);
        AddMetaTag(page, propertyName, "twitter:description", CropContent(p, 140));

        if (!string.IsNullOrEmpty(imgPath))
        {
            AddMetaTag(page, propertyName, "twitter:image", imgPath);
        }
    }

    /// <summary>
    /// Find first image tag in a given string and return source if found.   
    /// </summary>
    /// <param name="content">The input string.</param>    
    /// <returns>A source path or empty string.</returns>
    private static string GetFirstImgSrc(string content)
    {
        if (!String.IsNullOrEmpty(content))
        {
            Regex tagRegex = new Regex(@"<img\b[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Multiline);
            Match tagMatch = tagRegex.Match(content);
            if (tagMatch.Success)
            {
                Match matchSrc = Regex.Match(tagMatch.Value, "src=[\'|\"](.+?)[\'|\"]", RegexOptions.IgnoreCase | RegexOptions.Multiline);
                if (matchSrc.Success)
                {
                    return matchSrc.Groups[1].Value;
                }
            }
        }
        return string.Empty;
    }


    /****** Helpers *********/

    /// <summary>
    /// Checks the given string for null, spaces or literal null text.
    /// Useful for checking data entry fields
    /// </summary>
    /// <param name="s">The input string.</param>
    /// <returns>Bool value indicating valid entry.</returns>
    private static bool HasValidEntry(string s)
    {
        return s != null && s.Trim().Length > 0 && s != "null";
    }

    /// <summary> 
    /// Truncates post or page content to the specified number of chars and adds ellipses.
    /// Content is considered to be the post/page description or the body striped of HTML.
    /// </summary>
    /// <param name="item">The content to crop.</param>
    /// <param name="maxChars">The truncated length, zero means no cropping.</param>
    /// <returns>The sized content stripped of any HTML or an empty string.</returns>
    private static string CropContent(IPublishable item, int maxChars)
    {
        string content = HasValidEntry(item.Description) ? CleanContent(item.Description) : CleanContent(item.Content);
        if (maxChars > 0 && content.Length > maxChars)
        {
            content = content.Substring(0, maxChars) + "&nbsp;&hellip;&nbsp;";
        }
        return content;
    }

    // Strip common MS Word style tags that won't be caught by Utils.StripHtml(content) - run this before Utils.StripHtml(content).
    private static readonly Regex regWordStuff = new Regex("<style>[^>]*>|<w:.*</w:.*?>", RegexOptions.Compiled);
    // Square brackets and what's between, commonly used in BE for extensions and usercontrols. 
    private static Regex regSqBrkContent = new Regex(@"\[[^\]]*\]", RegexOptions.Compiled);

    /// <summary> 
    /// Strips out common MS Word tags, HTML and any leading or trailing space from a string.     
    /// </summary>
    /// <param name="content">The content to clean.</param>        
    /// <returns>The content stripped of markup and space trimmed.</returns>
    private static string CleanContent(string content)
    {
        if (string.IsNullOrEmpty(content))
        {
            return string.Empty;
        }       

        string cleaned = content.Replace("&nbsp;", " ");
        cleaned = regWordStuff.Replace(cleaned, string.Empty).Trim();
        cleaned = Utils.StripHtml(cleaned);
        cleaned = regSqBrkContent.Replace(cleaned, string.Empty);
        return cleaned;
    }

}
Jan 24, 2014 at 2:14 PM
Edited Jan 24, 2014 at 2:14 PM
I have done this extension, that may need some improvement and customization http://dnbegallery.org/cms/List/Extensions/SocialCards
Have a nice day :-)

I forgot, it works with facebook, twitter and google plus
Jan 29, 2014 at 8:40 PM
Just a FYI that this seemed to resolve itself without any code modifications, so the issue was temporary whatever it was.

Thanks for the link ildrago. Does this extension work in 2.9?

I have noticed when researching social tags that you sometimes want them to differ from the default meta tags. For instance, in your metadescription you generally try to squeeze in a bunch of keywords into a coherent call to action. But on social sites this isn't necessary and you can pay more attention to the message and less attention to the keywords.

I've never tried to create an extension, but could you theoretically design one which would allow the user to modify the various social tags with the 'Edit Post' interface so that they could be customized independent of the standard meta tags?