In diesem Artikel möchte ich auf das Thema Search Engine Optimization näher eingehen und drei Bereiche aus dieser Thematik vorstellen, die auch im SharePoint-Umfeld eine relevante Rolle spielen.

  1. XML-Sitemap
  2. SEO Ping
  3. Robots.txt

Zu jedem dieser drei Mechanismen möchte ich einen Weg zeigen, wie sich diese in SharePoint integrieren lassen und so den WebCMS-Auftritt in Punkto SEO weit nach vorne bringen.

Die XML Sitemap

In der Regel arbeiten Crawler über Verlinkungen. Ist eine Seite nicht oder mangelhaft verlinkt, dann wird sie kaum gefunden. Eine XML Sitemap besteht aus einer XML-Datei und liegt im Root-Verzeichnis des Internetauftritts. Sie beinhaltet alle Seiten des Auftrittes, die gefunden werden sollen. Zu jeder Seite wird zudem angegeben wann sich dieses zuletzt geändert hat, damit Ihr Inhalt neu erfasst wird. In der XML Sitemap kann dem Crawler auch mitgeteilt werden, welche Seiten ignoriert werden sollen. Mehr zu diesem mächtigen Instrument erfahrt Ihr unter „http://de.wikipedia.org/wiki/Sitemaps“ .

Wie eine XML Sitemap  in SharePoint 2010 umgesetzt werden kann, möchte ich mit dem nachfolgenden, schlanken(image) TimerJob einmal darstellen.

public class XmlSiteMapTimerjob : SPJobDefinition
    {
        internal const string JOBTITLE = "HV xml sitemap job";

        private StringBuilder textWriter = null;
        private int counter;
        public XmlSiteMapTimerjob()
            : base()
        {

        }

        public XmlSiteMapTimerjob(string jobName, SPWebApplication webApplication)
            : base(jobName, webApplication, null, SPJobLockType.Job)
        {
            this.Title = "HV xml sitemap job";
        }

        /// <summary>
        /// Creates a xml stemap over all site collections
        /// </summary>
        /// <param name="targetInstanceId"></param>
        public override void Execute(Guid targetInstanceId)
        {
            counter = 0;
            try
            {
                SPWebApplication webApplication = this.Parent as SPWebApplication;
                textWriter = new StringBuilder();
                // create xml file
                textWriter.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                textWriter.Append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
                SPSite rootSite = webApplication.Sites[0];
                try
                {                
                    foreach (SPSite portal in webApplication.Sites)
                    {
                        BuildSitemap(portal.RootWeb.Url);
                        if (portal.Url.Length &lt; rootSite.Url.Length)
                        {
                            rootSite = portal; // the root site collection has the shortest url
                        }
                        if (portal != null)
                        {
                            portal.Dispose();
                        }
                    }
                }
                finally
                {
                    if (rootSite != null)
                    {
                        rootSite.Dispose();
                    }
                }

                textWriter.Append("</urlset>");
                //store xml sitemap in the root site collection
                using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(textWriter.ToString())))
                {
                    //delete old
                    SPFile sitemapFile = rootSite.RootWeb.GetFile(rootSite.RootWeb.Url + "/sitemap.xml");
                    if (sitemapFile.Exists)
                    {
                        sitemapFile.Delete();
                    }
                    //write new file
                    rootSite.RootWeb.Files.Add("sitemap.xml", stream, true);
                }
            }
            catch (Exception ex)
            {
                Logger.LogToOperations("XmlSiteMapTimerjob->Execute: Anecxeption occured " + ex);
            }
        }

        /// <summary>
        /// Initial function to create the Sitemap.  
        /// Creates and writes the XML file.
        /// Puts Sitemap.xml in the root of the web applications' first site collection.
        /// </summary>
        /// <param name=””></param>
        /// <calledBy>Execute</calledBy>
        private void BuildSitemap(string siteUrl)
        {
            try
            {

                using (SPSite site = new SPSite(siteUrl))
                using (SPWeb web = site.RootWeb)
                {
                    LoadTreeViewForSubWebs(web); // kick it off with the root web                    
                }
            }
            catch (Exception ex)
            {
                Logger.LogToOperations("XmlSiteMapTimerjob->BuildSitemap: Anecxeption occured " + ex);
            }
        }

        /// <summary>
        /// Writes a node for a SPWeb.  Will call itself recursively if 
        /// currentWeb has sub webs.
        /// Will call LoadTreeViewForSubWebPages for the web's pages.
        /// </summary>
        /// <param name=””>SPWeb currentWeb</param>
        /// <calledBy>BuildSitemap, self(recursive)</calledBy>
        private void LoadTreeViewForSubWebs(SPWeb currentWeb)
        {

            string tagLabel = string.Empty;

            if (!currentWeb.AllowAnonymousAccess)
                return;

            if (PublishingWeb.IsPublishingWeb(currentWeb))
            {
                PublishingWeb pWeb = PublishingWeb.GetPublishingWeb(currentWeb);
                PublishingPageCollection pPageCol = pWeb.GetPublishingPages();

                //create xml links to site pages
                if (pPageCol != null)
                    LoadTreeViewForSubWebPages(pPageCol);
            }

            //create xml link to site
            //removed the link to the site (without a page) because Google sees them as redirectors
            //writeSitemapNode(currentWeb.Url + "/", currentWeb.LastItemModifiedDate.ToString("s"));

            //walk through lists
            foreach (SPList list in currentWeb.Lists)
            {
                //create xml links to HV libraries
                if (list is SPDocumentLibrary &amp;&amp; list.Title.StartsWith("HVDe_"))
                    LoadTreeViewForSubWebDocuments(list.Items);
            }

            //recurse through all subwebs
            foreach (SPWeb web in currentWeb.Webs)
            {
                using (web)
                {
                    //TODO: Additional exclude criteria?
                    if (web.ASPXPageIndexed)
                   {
                        LoadTreeViewForSubWebs(web);
                    }
                }
            }
        }

        /// <summary>
        /// writes a sitemap node for all documents in the SPList currentDocs
        /// </summary>
        /// <param name=””>SPListItemCollection currentDocs</param>
        /// <calledBy>LoadTreeViewForSubWebs</calledBy>
        private void LoadTreeViewForSubWebDocuments(SPListItemCollection currentDocs)
        {
            foreach (SPListItem item in currentDocs)
            {
                writeSitemapNode(item.Web.Url + "/" + item.Url, item.File.TimeLastModified.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz"));
            }
        }

        /// <summary>
        /// writes a sitemap node for all pages in the PublishingPageCollection currentPages
        /// </summary>
        /// <return></return>
        /// <param name=””>PublishingPageCollection currentPages</param>
        /// <calledBy>LoadTreeViewForSubWebs</calledBy>
        private void LoadTreeViewForSubWebPages(PublishingPageCollection currentPages)
        {
            foreach (PublishingPage page in currentPages)
            {
                writeSitemapNode(page.PublishingWeb.Url + "/" + page.Url, page.LastModifiedDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz"));
            }
        }

        /// <summary>
        /// writes a sitemap node
        /// </summary>
        /// <param name=””>string pageLocation, string lastModified</param>
        /// <calledBy>LoadTreeViewForSubWebs, LoadTreeViewForSubWebs</calledBy>
        private void writeSitemapNode(string pageLocation, string lastModified)
        {
            textWriter.AppendFormat("<url><loc>{0}</loc><lastmod>{1}</lastmod></url>", pageLocation, lastModified);
            counter++;

            if (counter == 50000)
            {
                Logger.LogToOperations("XmlSitemapTimerjob-> Maximum (50000) of nodes reached!!! ");
            }
        }

    }

Der SEO-Ping

Die XML-Sitemap haben wird uns im ersten Abschnitt schon angeschaut, drum möchte ich jetzt den so genannten „Suchmaschinen Ping“ einmal näher vorstellen. Der SEO Ping ist eine aktive Anfrage nach einem Crawl. So zu sagen ein: „Hey Crawler ich hab was neues! Komm mal vorbei!“ Jede Suchmaschine hat dabei Ihre eigenen Anforderungen bzgl. dieser Anfrage.

Natürlich kann so ein SEO-Ping auch für ein SharePoint WebCMS-Portal abgesetzt werden. Hierbei macht es Sinn dem Mechanismus eine eigene Konfigurations-Seite in der Zentraladministration zu geben.

image

Um die SEO-Ping-Funktionalität Code-seitig umzusetzen, bedarf es dieses dezenten Code-Beispiels:

public void NotifySearchEngines(object sender, EventArgs e) 
	{ 
		bool bSuccessful = false;
		bool gSuccessful = false;
		bool ySuccessful = false;
		bool aSuccessful = false;
		
		string url = String.Empty;
		
		//this action is only available if a web app is selected (DdlWebApps_SelectedIndexChanged), so don't check here again
		SPWeb rootWeb = GetSelectedWebAppsRootSiteTopWeb(Selector.CurrentItem as SPWebApplication);
		if (CheckboxNotifyBing.Checked) 
		{ 
			url = String.Format("http://www.bing.com/webmaster/ping.aspx?siteMap={0}/sitemap.xml", rootWeb.Url);
			HttpStatusCode bingNotified = Ping(url); 
			if (bingNotified == HttpStatusCode.OK) //noti successful? 
			{ 
				Config configMan = new Config(); 
				configMan.BingNotificationDate = DateTime.Now; 
				bSuccessful = true; 
			}
		} 
		if (CheckboxNotifyGoogle.Checked) 
		{ 
			url = String.Format("http://www.google.com/webmasters/sitemaps/ping?sitemap={0}/sitemap.xml", rootWeb.Url); 
			HttpStatusCode googleNotified = Ping(url); 
			if (googleNotified == HttpStatusCode.OK) //noti successful? 
			{ 
				Config configMan = new Config(); 
				configMan.GoogleNotificationDate = DateTime.Now; 
				gSuccessful = true; 
			} 
		} 
		if (CheckboxNotifyYahoo.Checked) 
		{ 
			url = String.Format("http://search.yahooapis.com/SiteExplorerService/V1/updateNotification?appid=HvportDe&url={0}/sitemap.xml", rootWeb.Url); 
			HttpStatusCode yahooNotified = Ping(url); 
			if (yahooNotified == HttpStatusCode.OK) //noti successful? 
			{ 
				Config configMan = new Config(); 
				configMan.YahooNotificationDate = DateTime.Now; 
				ySuccessful = true; 
			} 
		} 
		if (CheckboxNotifyAsk.Checked) 
		{ 
			url = String.Format("http://submissions.ask.com/ping?sitemap={0}/sitemap.xml", rootWeb.Url); 
			HttpStatusCode askNotified = Ping(url); 
			if (askNotified == HttpStatusCode.OK) //noti successful? 
			{ 
				Config configMan = new Config(); 
				configMan.AskNotificationDate = DateTime.Now; 
				aSuccessful = true; 
			} 
		} 
		if (aSuccessful &amp;&amp; bSuccessful &amp;&amp; ySuccessful &amp;&amp; gSuccessful) 
		{ 
			//this.ShowMessageInStatusBar("<strong>Erfolgreich:</strong>", "Alle Suchdienste wurden informiert", SPWebControlExtensionMethods.StatusBarPriColor.green); 
		} 
		else 
		{ 
			StringBuilder msg = new StringBuilder(); 
			if (!aSuccessful) 
			{ 
				msg.Append(" Ask konnte nicht informiert werden."); 
			} 
			if (!bSuccessful) 
			{ 
				msg.Append(" Bing konnte nicht informiert werden."); 
			} 
			if (!ySuccessful) 
			{ 
				msg.Append(" Yahoo konnte nicht informiert werden.");
			} 
			if (!gSuccessful) 
			{ 
				msg.Append(" Google konnte nicht informiert werden."); 
			} 
			//this.ShowMessageInStatusBar("<strong>Fehler:</strong>", msg.ToString(), SPWebControlExtensionMethods.StatusBarPriColor.red);
		}
	}

Wenn nun der Crawler Informiert wurde und vorbei kommt, dann stößt es als erstes auf Eure XML-Sitemap und bekommt so detaillierte Arbeitsanweisungen. Dieser SEO-Ansatz ist in Punkto Daten-Frische maximal effizient und effektiv.

Die Robots.txt

Was ist die Robots.txt und wofür brauch ich die Datei?

„Nach der Übereinkunft des Robots-Exclusion-Standard-Protokolls liest ein Webcrawler (Robot) beim Auffinden einer Webseite zuerst die Datei robots.txt (kleingeschrieben) im Stammverzeichnis (Root) einer Domain. In dieser Datei kann festgelegt werden, ob und wie die Webseite von einem Webcrawler besucht werden darf. Website-Betreiber haben so die Möglichkeit, ausgesuchte Bereiche ihrer Webpräsenz für (bestimmte) Suchmaschinen zu sperren.“ (www.wikipedia.de)

Die Robots.txt arbeitet demnach ähnlich wie die XML-Sitemap. Während die XML-Sitemap dem Crawler mitteilt, welche Portal-Struktur vorliegt und welche Seite sich geändert haben, gibt die Robos.txt genaue Anweisung „wer was lesen darf“.

Hier zwei Beispiele:

# robots.txt für example.com

# Diese Webcrawler schließe ich aus

User-agent: Sidewinder

Disallow: /

User-agent: Microsoft.URL.Control

Disallow: /

# Diese Verzeichnisse/Dateien sollen nicht durchsucht werden

User-agent: *

Disallow: /Lists/Forms/ # diese Inhalte stehen # diese Seiten gehören zu Listenelementen und dürfen im Internet nicht gezeigt werden

Disallow: /websites/Privat/Familie/Geburtstage/

Das Thema Robots.txt spielt selbst verständlich auch im Bereich SharePoint WebCMS eine große Rolle! Um den Inhalt dieser Datei zu editieren, erstelle ich immer gerne eine Pflegeseite in der zentralen Administration. In diesem Fall kann das natürlich die gleiche Seite sein wie für die SEO-Ping-Einstellungen.

image

Codeseitig wird der konfigurierte Inhalt der Robots.txt wie folgt (beim Klick auf den Speichern-Button) persistiert:

/// <summary>
        /// Save robot.txt content to the root site collections related file
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void SaveRobotsContent(object sender, EventArgs e)
        {
            //this action is only available if a web app is selected (DdlWebApps_SelectedIndexChanged), so don't check here again
            using(SPWeb rootWeb =GetSelectedWebAppsRootSiteTopWeb(Selector.CurrentItem as SPWebApplication))
            {

            SPFile robotsFile = rootWeb.GetFile(rootWeb.Url + "/robots.txt");
            if (robotsFile.Exists)
                robotsFile.Delete(); //delete old

            if (!String.IsNullOrEmpty(TextBoxRobotsContent.Text))
            {
                // create new robots.txt
                using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(TextBoxRobotsContent.Text)))
                {
                    rootWeb.Files.Add("robots.txt", stream, true);
                    //this.ShowMessageInStatusBar("<strong>Erfolgreich:</strong>", "Die Änderungen wurden übernommen!", SPWebControlExtensionMethods.StatusBarPriColor.green);
                }
            }
            else
            {
                //this.ShowMessageInStatusBar("<strong>Information:</strong>", "Da Sie keinen Inhalt für die robots.txt angegeben haben, wird diese entfernt, bis Sie neuen Inhalt definieren!", SPWebControlExtensionMethods.StatusBarPriColor.yellow);
            }
        }
            }

Wenn diese drei Bereiche (XML-Sitemap, SEO-Ping und Robots.txt) bei Eurem nächsten SharePoint-WebCMS-Projekt berücksichtigt werden, dann ist Euer Portal in Punkto SEO sehr gut aufgestellt. Ich hoffe der Artikel hat Euch gefallen und Ihr konntet etwas daraus für Eure Projekte mitnehmen.

Leave a comment

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Time limit is exhausted. Please reload the CAPTCHA.