Adding Site Filters to DMS Reports with Sitecore 6.5


DMS does not have a Site Filter for multi site enviornments in the latest version of Sitecore 6.5 but I found adding one is quite easy so here is a little tutorial.

Step 1 Create a Filter

  • Browse to /sitecore/system/Marketing Center/Analytics Filters/Report Filters.
  • Insert a new ‘Filter’ named Site.
  • Set the Type to ‘Sitecore.Analytics.Reports.Filters.ValueListFilter, Sitecore.Analytics’
  • Set the ColumnName to ‘MultiSite’

Step 2 Add Filter Values

Select your new new ‘Site’ Filter and insert two new ‘Predefined Filter Values’ name them ‘website’ and ‘site2’ and set their values accordingly.

Step 3  Add the Filters to each Report

  • Browse to the reports in /sitecore/system/Settings/Analytics/Reports SQL Queries.  ( for this example I used ‘Latest Visits’ )
  • Add the column [Visits].[MultiSite] to the SQL fields.
  • Add the new ‘Site’ filter to the Filter field.

Step 4 Go Test the filter on your report

  • Open the Report from the ‘Engagement Analytics’ Menu Option.
  • Test your new filter with the Filter Button up top.
Advertisements

Multiple Sites with One Authoring Domain in Sitecore


Sitecore allows you to manage multiple sites by binding a hostname to a root content item via the webconfig. This works fine in the delivery enviornment but for authoring you would also now need a seperate entry for each site if your site is dependent on the Sitecore.Context.Site at all.

Adding the configs settings isn’t a huge deal but if you have 50 sites you now need 50 config settings for the authoring urls, 50 for the editing urls, 100 dns entries and 50 authoring bookmarks for your authors.

I came up with the following approach to resolving the site in page editor and preview modes to help alleviate some of this pain. I can now use one authoring domain and when I push the Page Editor or Preview buttons Sitecore will resolve the site for the item.

To achieve this I did four things on top of the normal setup :

  • Added SiteResolver to resolve site based on Item if sc_itemid is in the Query String and we are in Page Editor or Preview mode.
  • Override the LinkProvider to include the sc_site in the Query String when in Page Editor or Preview Mode.
  • Turn Off Rendering.SiteResolving
  • Add A Site Entry for the “Authroing” site with the root of the home node.

Normal Setup
Root Content Items

IIS Site Bindings

Added Sites to web.config (notice host names and start items)


      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

With the setup above and I log into each domain and author the sites but I can’t author from one site and have the Site resolve correctly.
Authoring Multiple Sites from One Domain
First I add my new authoring site below my last site because I want the others to resolve first on the delivery side.

      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="authoring" hostName="" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now if I login to editing.training domain it resolves to this authoring site that doesn’t contain a domain name. I can log in and Edit all three of my sites but if the sites are relying on anything contained in the Sitecore.Context.Site this information will be pointing to the Authoring Site and it will be wrong.

I remembered how the cross site links resolve to the correct domain when Rendering.SiteResolving is set to true so I reflected on that code and came up with a class (mostly copy pasta’d….including the code with the Go To Label) to resolve the Site for an Item without looking at the host name.

This first class is the Link Helper Class and it’s ResolveTargetSite method is what we need.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web;
using Sitecore.IO;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Prototypes.Links
{
    public static class LinkHelper
    {
        private static Dictionary<string, SiteInfo> _siteResolvingTable;
        private static List<SiteInfo> _sites;
        private static readonly object _syncRoot = new object();
        private delegate string KeyGetter(SiteInfo siteInfo);

        public static SiteInfo ResolveTargetSite(Item item)
        {
            SiteContext site = Context.Site;
            SiteContext context2 = site;
            SiteInfo info = (context2 != null) ? context2.SiteInfo : null;
            if ((context2 != null) && item.Paths.FullPath.StartsWith(context2.StartPath))
            {
                return info;
            }
            Dictionary<string, SiteInfo> siteResolvingTable = GetSiteResolvingTable();
            string key = item.Paths.FullPath.ToLowerInvariant();
        Label_00AE:
            if (siteResolvingTable.ContainsKey(key))
            {
                return siteResolvingTable[key];
            }
            int length = key.LastIndexOf("/");
            if (length > 1)
            {
                key = key.Substring(0, length);
                goto Label_00AE;
            }
            return info;
        }

        private static Dictionary<string, SiteInfo> GetSiteResolvingTable()
        {
            List<SiteInfo> sites = SiteContextFactory.Sites;
            if (!object.ReferenceEquals(_sites, sites))
            {
                lock (_syncRoot)
                {
                    if (!object.ReferenceEquals(_sites, sites))
                    {
                        _sites = sites;
                        _siteResolvingTable = null;
                    }
                }
            }
            if (_siteResolvingTable == null)
            {
                lock (_syncRoot)
                {
                    if (_siteResolvingTable == null)
                    {
                        _siteResolvingTable = BuildSiteResolvingTable(_sites);
                    }
                }
            }
            return _siteResolvingTable;
        }

        private static Dictionary<string, SiteInfo> BuildSiteResolvingTable(IEnumerable<SiteInfo> sites)
        {
            Dictionary<string, SiteInfo> dictionary = new Dictionary<string, SiteInfo>();
            List<string> list = new List<string>();
            KeyGetter[] getterArray = new KeyGetter[] { info => FileUtil.MakePath(info.RootPath, info.StartItem).ToLowerInvariant() };
            foreach (KeyGetter getter in getterArray)
            {
                foreach (SiteInfo info in sites)
                {
                    if (!string.IsNullOrEmpty(info.HostName) && string.IsNullOrEmpty(GetTargetHostName(info)))
                    {
                        Log.Warn(string.Format("LinkBuilder. Site '{0}' should have defined 'targethostname' property in order to take participation in site resolving process.", info.Name), typeof(LinkProvider.LinkBuilder));
                    }
                    else if ((!string.IsNullOrEmpty(GetTargetHostName(info)) && !string.IsNullOrEmpty(info.RootPath)) && !string.IsNullOrEmpty(info.StartItem))
                    {
                        string key = getter(info);
                        if (!list.Exists(new Predicate<string>(key.StartsWith)))
                        {
                            dictionary.Add(key, info);
                            list.Add(key);
                        }
                    }
                }
            }
            return dictionary;
        }

        private static string GetTargetHostName(SiteInfo siteInfo)
        {
            if (!string.IsNullOrEmpty(siteInfo.TargetHostName))
            {
                return siteInfo.TargetHostName;
            }
            string hostName = siteInfo.HostName;
            if (hostName.IndexOfAny(new char[] { '*', '|' }) < 0)
            {
                return hostName;
            }
            return string.Empty;
        }

    }
}

Next I create a custom SiteResolver and place it after the normal SiteResolver in the begin request pipeline.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class SiteResolver : Sitecore.Pipelines.HttpRequest.SiteResolver
    {

        public override void Process(Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if ((Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsPageEditor) 
                && HttpContext.Current.Request.QueryString["sc_itemid"] != null)
            {
                Database db = Sitecore.Context.Database;
                if (db != null)
                {
                    Item item = db.SelectSingleItem(HttpContext.Current.Request.QueryString["sc_itemid"]);
                    if (item != null)
                    {
                        SiteInfo site = LinkHelper.ResolveTargetSite(item);
                        SiteContext siteContext = SiteContextFactory.GetSiteContext(site.Name);
                        UpdatePaths(args, siteContext);
                        Sitecore.Context.Site = siteContext;
                    }
                }
            }          
        }
    }
}

Add that to the httpRequestBeing pipeline right after the normal site resolver

        <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
        <processor type="Sitecore.Prototypes.Links.SiteResolver, Sitecore.Prototypes.Links"/>

Now when I hit the Page Editor or Preview buttons the site is displayed using the correct Sitecore.Context.Site, but if I click on a link to another page it loses the context….

To fix this I added a custom link provider that appends the sc_site to the query string if we are in preview or pageditor mode.

Here is the class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class LinkProvider : Sitecore.Links.LinkProvider
    {

        public override string GetItemUrl(Data.Items.Item item, Sitecore.Links.UrlOptions options)
        {
            string url = base.GetItemUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public override string GetDynamicUrl(Data.Items.Item item, Sitecore.Links.LinkUrlOptions options)
        {
            string url  = base.GetDynamicUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public string AppendOption(string url, string option)
        {
            return url.Contains("?") ? url + option : url + "?" + option;
        }
    }
}

And here is where I overrode it in the config section

		<linkManager defaultProvider="authoring">
			<providers>
				<clear />
				<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false" />
        <add name="authoring" type="Sitecore.Prototypes.Links.LinkProvider, Sitecore.Prototypes.Links" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false"  />
			</providers>
		</linkManager>

Now my last problem is that because Rendering.SiteResolving is set to true so my domain name is switching so we need to set that to false in the Authoring Environment while we probably want it true on the delivery side.

       <setting name="Rendering.SiteResolving" value="false" />

Notes

  • I wouldn’t expect this to work with the Shared Source DB Site Resolver and I’m not sure about the other Shared Source Multi-Site piece.
  • Posting code in wordpress is painfull.
  • So is copying from it so I’ll try to get a download up

Adding an Item Informaton button to the Sitecore Page Editor


I had a request to see Item information from the Page Editor without going into the split screen edit item window so I created an Item Information button and corresponding page.

It currently display the following

  • Item ID
  • Path
  • Latest Version
  • Current Workflow
  • Workflow State
  • References
  • Referrers
  • Published State in all Target Databases

One thing to note is it’s for Content Authors and not Dev’s so it’s does’t report on References or Referrers unless there path is one of a Content Item or Media Item.

Here’s the Code for the Command

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Shell.Applications.WebEdit.Commands;
using Sitecore.Web.UI.Sheer;
using Sitecore.Data;
using Sitecore.Data.Items;

namespace Sitecore.Prototypes.PageEditor.WebEdit.Commands
{
public class Info : WebEditCommand
{
public override void Execute(Shell.Framework.Commands.CommandContext context)
{
Item item = context.Items[0];

SheerResponse.ShowModalDialog("/MyApplications/PageEditorInfo/Info.aspx?ItemId=" + item.ID.ToString(), "640px", "480px", string.Empty, false);
return;
}
}
}

Here’s the Code for the Page located in /MyApplications/PageEditorInfo/Info.aspx (yeah lots of literals and inlines CSS)


<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Info.aspx.cs" Inherits="Sitecore.Starterkit.MyApplications.PageEditorInfo.Info" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><asp:Literal runat="server" ID="litItemTitle" /></title>
<link rel="stylesheet" href="/sitecore/shell/themes/standard/default/Default.css"/>
<link rel="stylesheet" href="/sitecore/shell/themes/standard/default/Dialogs.css"/>
</head>
<body style="background: none repeat scroll 0% 0% rgb(233, 233, 233);" scroll="no">

<form id="form1" runat="server">
<div style="background-color: White; border-bottom: 1px solid black;padding: 6px;">
<div style="color: black; padding: 0px 0px 4px; font: bold 9pt tahoma;">Item Information for : <asp:Literal runat="server" ID="itemDisplayName" /></div>
<div style="color: rgb(51, 51, 51);">Displays Item Information from the Page Editor</div>
</div>
<div style="padding:6px;">
<table>
<tbody>
<tr>
<td align="right">Id : </td><td><asp:Literal runat="server" ID="itemId" /></td>
</tr>
<tr>
<td align="right">Path : </td><td><asp:Literal runat="server" ID="itemPath" /></td>
</tr>
<tr>
<td align="right">Latest Version : </td><td><asp:Literal runat="server" ID="itemVersion" /></td>
</tr>
<tr>
<td align="right">Workflow : </td><td><asp:Literal runat="server" ID="itemWorkflow" /></td>
</tr>
<tr>
<td align="right">Worfklow State : </td><td><asp:Literal runat="server" ID="itemWorkflowState" /></td>
</tr>
<tr>
<td align="right" valign="top">Referrers : </td><td><asp:Literal runat="server" ID="itemReferrers" /></td>
</tr>
<tr>
<td align="right" valign="top">References : </td><td><asp:Literal runat="server" ID="itemReferences" /></td>
</tr>
<tr>
<td align="right" valign="top">Publishing Information : </td><td><asp:Literal runat="server" ID="itemPublishingInformation" /></td>
</tr>
</tbody>
</table>
</div>
</form>
</body>
</html>

and the code behind


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Workflows;
using Sitecore.Data;

namespace Sitecore.Starterkit.MyApplications.PageEditorInfo
{
public partial class Info : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Assert.IsNotNullOrEmpty(Request.QueryString["ItemId"],"ItemId must not be null");

Item item = Sitecore.Context.Database.SelectSingleItem(Request.QueryString["ItemId"]);

Assert.IsNotNull(item,"Item must not be null");
Assert.IsTrue(item.Access.CanRead(), "Read Access Denied");

itemId.Text = item.ID.ToString();
itemPath.Text = item.Paths.FullPath;
litItemTitle.Text = item.DisplayName;
itemDisplayName.Text = item.DisplayName;
itemVersion.Text = item.Versions.GetLatestVersion().Version.Number.ToString();

IWorkflow workflow = Sitecore.Context.Database.WorkflowProvider.GetWorkflow(item);
if (workflow != null)
{
WorkflowState state = workflow.GetState(item);
itemWorkflow.Text = workflow.Appearance.DisplayName;
itemWorkflowState.Text = state.DisplayName;

}
var refrences = Globals.LinkDatabase.GetReferences(item);
foreach (var link in refrences)
{
var linkItem = link.GetTargetItem();
if (linkItem != null && (linkItem.Paths.IsContentItem || linkItem.Paths.IsMediaItem))
itemReferences.Text += linkItem.Paths.FullPath + "<br/>";
}
var referrers = Globals.LinkDatabase.GetReferrers(item);
foreach (var link in referrers)
{
var linkItem = link.GetSourceItem();
if (linkItem != null && (linkItem.Paths.IsContentItem || linkItem.Paths.IsMediaItem))
itemReferrers.Text += linkItem.Paths.FullPath + "<br/>";
}

ShowPublishingInfo(item);
}
void ShowPublishingInfo(Item currentItem)
{
var publishingTargets = Sitecore.Context.Database.SelectSingleItem("/sitecore/system/Publishing targets").Children;

foreach (Item publishingTarget in publishingTargets)
{
string dbName = publishingTarget.Fields["Target database"].GetValue(true);
if (!string.IsNullOrEmpty(dbName))
{
var database = Sitecore.Configuration.Factory.GetDatabase(dbName);

Item item = database.SelectSingleItem(currentItem.ID.ToString());

string htmlOutput = string.Empty;
if (item != null)
{

foreach (var language in item.Languages)
{
Item languageVersion = item.Versions.GetLatestVersion(language);
if (languageVersion != null && languageVersion.Versions.Count > 0)
{
htmlOutput += string.Format("<div>{0} - {1}</div>", languageVersion.Version.Number.ToString(), languageVersion.Language.GetDisplayName());
}
}

}
if (htmlOutput == string.Empty)
{
htmlOutput = "This item is not currently published to " + publishingTarget.DisplayName + ".";
}

htmlOutput = string.Format("<div><div style='font-weight:bold'>Published to {1}</div>{0}</div>", htmlOutput,publishingTarget.DisplayName);

itemPublishingInformation.Text += (htmlOutput);
}
}
}
}
}

You have to add the command to the commands.config file under the app_config folder

<command name=”webedit:info” type=”Sitecore.Prototypes.PageEditor.WebEdit.Commands.Info,Sitecore.Prototypes.PageEditor”/>

Add add the Item to the Core Database

Path : /sitecore/content/Applications/WebEdit/Common Field Buttons/Info

Template : /sitecore/templates/System/WebEdit/WebEdit Button

Header : Item Info

Click : chrome:common:edititem({command:”webedit:info”})

Type : common

Icon : Applications/16×16/information.png

I’ll try to get in a nice downloadable zip format.

Update : I fixed some bugs in the code and created a Sitecore Package available here

SEO Tips for 2011


SEO strategy changes all the time so I thought I’d post an article that is relevant to 2011. The purpose of this post is get a list of some of the things I think people should be focusing on outside of link building and external marketing.

1. Use Auto Suggestions to build your title and url.

Here’s how I came up with the title for this post.

I opened google, I typed ‘SEO Tips’ without pressing return and looked at the suggestions it gave me. Out of the list I thought ‘SEO Tips 2011’ gave me a topic I could write about that would be valuable to it’s reader.

let Google Suggestions be your guide

2. How your HTML is Marked up is more important than how your web page looks.

Fast clean pages with fewer images and fast load times do better with bots and people. Notice Google and Amazon are heavily text based and not very pretty. That’s because they get it.

Marketing text inside of images doesn’t reach the bots.
Content inside of flash files isn’t helping people find your site.
Depending on the type of content you are working with there are different best practices for meta tags and markup. You don’t mark up a product site the same way you mark up a news site or a video site.

Here’s a markup article I wrote on MetaTags.

3. Ajax and Popups are nice for functionality but bad for SEO.

The bot is just going to parse the HTML as it renders, it doesn’t care about your ajax events so if there is content on your page that isn’t there on load from an bot perspective it’s not there. The most common example of this is ‘click here to see more info’ and it does it in a modal window with AJAX.  Also tabbed data that uses AJAX has this problem.

If you have terms on a page put them in google and make sure you get a long tail hit on them.

4. PDF and Video’s are bad if there isn’t a corresponding page with lots of data about the PDF or Video

PDF material is nice for technical information and printing but having a html version of that pdf with a download link to the pdf is better for SEO.  At the least make sure there is text on the page describing your multimedia content.

For example lets say I have a video on how to build a ‘foo’. The page has some basic text describing the video as ‘how to build a foo’ with very little detail. The video is an hour long.  The video covers everything from ‘converting a ‘thingamajig’ to a ‘whatsit’ as part of building a foo.

You search for “building a foo” your video comes up.

You search for “converting a ‘thingamajig’ to a ‘whatsit'” and your video doesn’t come up. You have content for this but it’s not indexed on that term because the term is only spoke and not written.

Having Video files and PDF’s are good but you need to surround it with the proper copy in html to maximize it’s value.

5. Measure often and react

Google and yahoo have tools for optimizing SEO, have a strategy that involves implementing, learning them and checking things on a daily basis. Content authors need the ability to see how their content is doing so they can measure, tweak, and improve things and someone should be able to rattle off rough stats on how your doing on different search terms.  For example on my blog my stuff on Razor gives me the most search based traffic.

6. Find an SEO Resource you can afford and ‘Learn to Fish’

Read a book, some articles, find a forum, bring in a consultant or company and have them teach you what they know about SEO and apply it.

7. Google has previews

This is another spot where AJAX can give you problems. Make sure the previews of your pages in ‘google preview’ are acceptable.

8. Content and Timing are key
Your site ranking and inlinks are important but the content,q uality, timing and bot readability of it rules. I’m a one person blog with very few links to it but on terms you would expect to bring up a microsoft page or stackoverlow page I often see my pages above them on terms we are sharing.

Timing is also very important, a will written article on a subject that shows up first will often remain at the top of the rankings.  If you search for a subject you should notice there is a relation between ranking and when they showed up on the market for equally well tagged and written content.

9. It’s a game play and have fun.

If you think you should rank higher on a subject than some other site put the time in to figure out why they are better, come up with a plan to pass them and win! Don’t settle for anything.

Authors note : If in an effort to improve things you mess up and actually bring things down on a major site you are probably going to be in some trouble but you can’t win if you don’t try so have back up plans and CYA.

Meta Tags for public facing websites


I recently took an attempt at writing a web service that allowed me to get details about a web page similar to what you see when you paste a link in face book. In doing this I learned that there are quite a few meta tags a publicly facing site should have that go beyond the basic SEO tags we’ve been putting in for years.

The SEO tags are Title, Description, Keywords and Canonical.

Title is the title you see in the top of the browser window as well as the title search engines give to your page.

<title>My Page Title Goes Here</title>

Description
is not visible in the browser window but it used below the title of your page in search engines to describe your page.

<meta name=”description” content=”This is the best page in the world, you should visit it so you know what everyone else is talking about”>

Keywords keywords should contain a comma seperated list of the search terms you are targeting. They say the search engines no longer use this so you may want to save yourself some maintenance time and stop supporting this term.

<meta name=”keywords” content=”my keyword1, my keyword 2″/>

Canonical is a relatively new one that is for search engines. If you can access a page from multiple URL’s the canonical tag should be the most human readable version that you want the search engines to display.

If you support friendly urls

something like http://www.mywebsite.com/Articles/1/Title-Of-My-Story

is preferred over http://www.mywebsite.com/articles?id=1

In this case you would place the tag below in both versions of the page.

<link rel="canonical" href="http://www.mywebsite.com/Articles/1/Title-Of-My-Story">

Now the new stuff I’m seeing is used to describe your page to non search engines but things like Browsers, IPhones, Mashups and Facebook.

Shortcut_Icon used in favorites/bookmarks
sample from cnn.com
<link type=”image/x-icon” href=”http://i.cdn.turner.com/cnn/favicon.ico&#8221; rel=”shortcut icon”>

Apple Touch Icon points to hi-res icon for Apple iTouch Devices.
sample from stackoverlfow.com
<link href=”http://sstatic.net/stackoverflow/img/apple-touch-icon.png&#8221; rel=”apple-touch-icon”>

image_src is picked up by things like facebook and google news to show the image associated with your pages content.
sample from cnn.com article
<link rel=”image_src” href=”http://i.cdn.turner.com/cnn/.element/img/3.0/newsscanner/no_image_cnn_90x51.jpg“/>

Open Graph tags for facebook
If you care about what a link to your site looks like when someone puts a link up in facebook you can control it with open graph tags.
There is more info on open graph here on facebook.

Here’s a youtube video sample using open graph

    <meta property="og:url" content="http://www.youtube.com/watch?v=i4SkO3ypCXw" />
    <meta property="og:title" content="Drunken Master Trailer" />
    <meta property="og:description" content="Wong Fei Hong (Jackie Chan) is sent to train under his uncle, a wine guzzling master famous for crippling his students. Desperate to escape this brutal teach..." />
    <meta property="og:type" content="video" />
    <meta property="og:image" content="http://i2.ytimg.com/vi/i4SkO3ypCXw/default.jpg" />
    <meta property="og:video" content="http://www.youtube.com/e/i4SkO3ypCXw" />
    <meta property="og:video:type" content="application/x-shockwave-flash" />
    <meta property="og:site_name" content="YouTube" />

Driving Traffic and Sales with an API


10 years ago there were two reasons I would use an API (Advanced Programmable Interface).
1. To talk to hardware from a software interface.
2. To leverage third party software inside of an application.

Now I see Google, Amazon and Facebook as leaders in their respective industries. They all have API’s that allow developers to send them traffic and information as well as advertise their site. Lets take a look at the three companies and what they’re doing to expand their business with API’s.

Google

Google has a lot of API’s as well as a full blown SDK (Software Developers Kit) on the android. I’ll focus on their web based feeds and services for this article and skip the Android SDK. A quick look at the google code page shows something like 50 to 70 projects, most of which can be considered an API.

Google makes most of it’s money on advertising and they’re able to successfully advertise based on content and data they have mined about you combined with data mined from others.

They have 8 Advertising API’s that allows developers to make use of AdSense in their software.

There are 5 or 6 around geolocating and maps. When you build apps using these api’s and combine them with AdSense, you have location based  marketing tools.

There’s a YouTube api which should ultimately drive traffic to a site they own.

There’s commerce and product search API’s you can use to search your own products and implement their checkout system at a cost. Not only can you read the commerce feed but you can submit your companies product data to it.

There are API’s for GMail and Google Docs which google hopes to compete with Microsoft in those spaces.

I’ll admit there’s some API’s that I have no clue how their making money with but they’re getting developers to use their brand and display their data from their data feeds so there has to be some value in that.

Facebook

We’ve all seen the Facebook like buttons all over the web. That’s an example of an API being implemented. It gets facebooks logo on your webpage and gives your users a way to share your content on facebook. It’s win/win for both Facebook and the company implementing it.

A look at the Facebook developers page gives you api’s for building facebooks apps and games as well as integrating facebook into a mobile device. They’re giving developers tools to build up the facebook brand and they provide the tools for doing so.

They have a link to a showcase page showing how businesses are leveraging facebook. There are people both building widgets in their website that integrate into facebook as well as full blown applications running inside of facebook.

Amazon

Amazon has a bunch of API’s for their affiliates, both advertisers and suppliers. There are tools for building lists of products to display on your site, tools for adding products to amazon and tools for building an entire commerce system in your site that are all built on Amazon API’s. Because of these API’s Amazon has sites all over the internet linking to products on their site and complete checkout systems that might not say amazon on them but Amazon is under the covers.

Compare Amazon to Walmart. Walmart has some datafeeds with product info for their advertising programs but that’s about it. They’re not building an API that allows developers to build a business. So they’re raking in the doe with their brick and mortar sites, but Amazon is king of the internet.

My Overall Thought

If you’re an internet based company or a company trying to build it’s internet presence maybe it’s not enough to focus on SEO and online advertising programs because the kings of the internet are building API’s that allow developers to drive traffic and sales to them.

Rendering an Item with a XSLT stored in sitecore or a database.


I work in an environment where getting something like an XSLT file onto a server requires a full QA test cycle of the whole system so I played around with rendering a sitecore item with XSLT stored inside of a sitecore item. I don’t know if I’ll ever do anything with it but here’s a class that allows you to set the path to to a sitecore item that contains a ‘Xsl Transform’ field.

    public class XslItem : XslFile
    {
        protected override void LoadXsl(System.Xml.XmlDocument xsl, string path)
        {
            Item item = Sitecore.Context.Database.GetItem(path);
            if (item != null)
            {
                if (!item.Fields.IsNullOrEmpty("Xsl Transform"))
                {
                    string xslField = item.Fields["Xsl Transform"].GetValue(true);
                    xsl.LoadXml(xslField);
                    return;
                }
            }
            base.LoadXsl(xsl, path);
        }
    }

Then to use it you’ll want something like this :


  XslItem file = new XslItem();
  file.Path = "/sitecore/content/SomeItemWithXslTransformField";
  file.DataSource = "/sitecore/content/theDataItem");
  HtmlTextWriter writer = new HtmlTextWriter(new StringWriter());
  file.RenderControl(writer);
  string htmlOutput = writer.InnerWriter.ToString();
  return new LiteralControl(htmlOutput);

And you can place your XSL inside of sitecore

public class XslItem : XslFile
{
protected override void LoadXsl(System.Xml.XmlDocument xsl, string path)
{
Item item = Sitecore.Context.Database.GetItem(path);
if (item != null)
{
if (!item.Fields.IsNullOrEmpty(“Xsl Transform”))
{
string xslField = item.Fields[“Xsl Transform”].GetValue(true);
xsl.LoadXml(xslField);
return;
}
}
base.LoadXsl(xsl, path);
}
}