Wer mit dem Bloggen beginnen möchte stellt sich neben dem Themenbereich zu Beginn jede Menge Fragen zu vielerlei Punkten rund um die technischen Anforderungen. Es muss entschieden werden ob man selbst hostet, welche Plattform und somit welche Technologie zum Einsatz kommen soll, oder ob man nicht selbst etwas baut. Bei allen Fragen steht man vor einer riesigen Auswahl an verfügbaren Systemen und Plattformen.

Ohne das Rad komplett neu erfinden zu wollen, habe ich mich für die eigene Entwicklung entschieden. Ausschlaggebend war vielmehr der Wunsch nur Funktionen zu haben, die ich auch wirklich brauche - Minimalismus also. Minimalismus soll auch beim Setup herrschen. D.h. keine Installationsorgien für Datenbank- und Webserver. Stattdessen "Dropbox-Deployment". Die Posts wollte ich aus diesem Grund in einem Format, dass ich mit fast jedem Editor auf diversen OS-Plattformen bearbeiten kann. Markdown erscheint mir dafür geeignet.

blog.cayas.de verwendet ASP.NET MVC, Markdown und Dropbox.

Auf der Suche nach möglichen Ansätzen um in die Entwicklung eines Blogsystems einzusteigen, bin ich auf den Blogpost von Greg Arroyo gestoßen, der später auch die Grundlage für mein Blogsystem werden sollte. Inzwischen ist davon nur noch wenig zu erkennen, denn es fehlen grundlegende Dinge wie KeyWords, Kategorien, Tags und ein RSS-Feed. Bevor ich erkläre wie ich die genannten Punkte umgesetzt habe, gibt es noch eine Optimierung umzusetzen. Es geht um die verteilten Summary-Dateien zu jedem Blogpost.

Eine Summary für alle Posts

Die erste Änderung am System ist die Zusammenfassung aller Summary Dateien in eine Summary.txt. Änderungen an den Meta-Daten der Artikel sind so einfacher umzusetzen. Ich habe dazu die Methode GetBlogListings der Klasse BlogFileSystem dahingehend geändert, dass der Name der Datei als zusätzlicher Parameter summaryFile übergeben werden muss.

public List<BlogPostHeader> GetBlogListings(int limit, string summaryFile)
{
    var blogPostHeader = new List<BlogPostHeader>();

    if (String.IsNullOrWhiteSpace(summaryFile))
        return blogPostHeader;

    if (!File.Exists(Path.Combine(pathToBlogPosts, summaryFile)))
        return blogPostHeader;

    var blogListing = new JavaScriptSerializer().Deserialize<BlogPostList>(File.ReadAllText(Path.Combine(pathToBlogPosts, summaryFile)));

    for (int i = 0; i < blogListing.BlogPostHeaders.Count; i++)
    {
        if (i == limit)
            break;

        blogPostHeader.Add(blogListing.BlogPostHeaders[i]);
    }

    return blogPostHeader;
}

Die Methode GetBlogPostsFiles wird damit überflüssig. Die Action Index im Controller und die Methode GetBlogPostByTitleForUrl muss noch auf die neuen Gegebenheiten angepasst werden.

public ActionResult Index()
{
    var fileSystem = new BlogFileSystem(Server.MapPath(ConfigurationManager.AppSettings["BlogPostsDirectory"]));
    var model = fileSystem.GetBlogListings(5, ConfigurationManager.AppSettings["BlogPostsSummaryFile"]);
    return View(model);
}

public BlogPost GetBlogPostByTitleForUrl(string title)
{
    var matchingFiles = GetFilesForBlogPostByTitleForUrl(title);
    var blogPostHeaders = GetBlogListings(5, ConfigurationManager.AppSettings["BlogPostsSummaryFile"]);

    foreach (var postHeader in blogPostHeaders)
    {
        if (postHeader.Url.Equals(title, StringComparison.InvariantCultureIgnoreCase))
        {
            var blogPost = new BlogPost(postHeader);
            blogPost.Body = File.ReadAllText(matchingFiles.Where(i => !i.Contains("_summary")).FirstOrDefault());
            return blogPost;
        }
    }

    return null;
}

Die neue Summary.txt hat jetzt folgenden Aufbau:

{
    BlogPostHeaders:
    [
        {
            Title: "Android Studio Designer",
            Url: "android-studio-designer",
            PostDate: "2013-06-03",
            Author: "Sebastian",
            ShortDescription: "Die Google I/O ist seit ein paar Wochen...",
            Image: ""
        },
        {
            Title: "Xamarin Evolve Event 2013",
            Url: "xamarin-evolve-event-2013",
            PostDate: "2013-04-20",
            Author: "Sebastian",
            ShortDescription: "Vom 14. bis zum 17.04. haben sich in Austin, Texas ...",
            Image: ""
        }
    ]
}

Implementierung von Kategorien

Für thematisch zusammenpassende Beiträge bieten sich Kategorien und Tags an, die sich schnell in das Basissystem einbauen lassen. Der richtige Ort dafür ist die BlogListing, welche bei mir BlogPostHeader heißt, die um eine Property Tags und Categories erweitert werden muss.

public class BlogPostHeader
{
    ...
    public string Image { get; set; }
    public string Tags { get; set; }
    public string Categories { get; set; }
}

In der bereits angepassten Summary.txt sollte anschließend das Json-Array um jeweils diese beiden Properties erweitert werden, wenn man Beiträge mit diesen Informationen ausstatten möchte.

{
    Title: "Xamarin Evolve Event 2013",
    Url: "xamarin-evolve-event-2013",
    PostDate: "2013-04-20",
    Author: "Sebastian",
    ShortDescription: "Vom 14. bis zum 17.04. haben sich in Austin, Texas ...",
    Image: "",
    Tags: "Xamarin, Evolve",
    Categories: "Business"
}

Ich verwende aktuell die Kategorien nur im RSS-Feed. Grundsätzlich lassen sich die Informationen aber wie KeyWords, die ich im nächsten Abschnitt bespreche, verwenden.

KeyWords

Für den eigentlichen Blogpost sind KeyWords sicherlich nicht wirklich interessant, da es dafür die bereits zuvor integrierten Tags und Kategorien gibt. Was die SEO Auswirkungen angeht, so gibt es Befürworter und Gegner auf beiden Seiten.

KeyWords in das Blogsystem einzubauen ist technisch betrachtet keine große Sache, da lediglich wieder nur das Json-Array in der Summary.txt und die BlogPostHeader Klasse angepasst werden müssen. Die Vorgehensweise ist der für die Implementierung von Tags und Kategorien ähnlich. Nur der Controller muss in diesem Fall keine Änderung erfahren, weil die KeyWords durch das Modell direkt an die View geliefert werden.

ViewBag.MetaKeyWords = @Model.KeyWords;

In meinem Template wird anschließend nur noch das KeyWords-Tag entsprechend umgesetzt

@if (!String.IsNullOrWhiteSpace(ViewBag.MetaKeyWords))
{
    <meta name="keywords" content="@ViewBag.MetaKeyWords" />
}

Den Feed-Generator integrieren

Nach all der Arbeit, die bisher in dem System steckt, sollen neue Beträge natürlich leicht gefunden werden und zwar auch ohne den Umweg mit einem Browser ständig das Blog besuchen zu müssen.

Um das Umsetzen zu können habe ich mir eine Klasse RSSResult erstellt, die von ActionResult ableitet, und entsprechend alle Blogpost in den Response-Stream schreibt.

public class RssResult : ActionResult
{
    List<BlogPostHeader> posts;

    public RssResult(List<BlogPostHeader> posts)
    {
        this.posts = posts;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.ContentType = "application/rss+xml";

        using (var writer = System.Xml.XmlWriter.Create(context.HttpContext.Response.OutputStream))
        {
            writer.WriteStartElement("rss");
            writer.WriteAttributeString("version", "2.0");
            writer.WriteStartElement("channel");

            writer.WriteElementString("title", "Blog von Cayas Software");
            writer.WriteElementString("description", "Unsere Gedanken zu verschiedenen Themen rund um die Softwareentwicklung.");
            writer.WriteElementString("link", "http://blog.cayas.de");

            posts.ForEach(x =>
            {
                writer.WriteStartElement("item");
                writer.WriteElementString("title", x.Title);
                writer.WriteElementString("description", x.ShortDescription);
                writer.WriteElementString("link", String.Format("{0}/{1}", "http://blog.cayas.de", x.Url.ToLower()));
                writer.WriteElementString("pubDate", x.PostDate.ToShortDateString());
                writer.WriteEndElement();
            });

            writer.WriteEndElement();
            writer.WriteEndElement();
        }
    }
}

Mit dem RSSResult lässt sich nun eine Action im Controller anlegen.

public RssResult PostRssFeed(string category)
{
    var fileSystem = new BlogFileSystem(Server.MapPath(ConfigurationManager.AppSettings["BlogPostsDirectory"]));
    var model = fileSystem.GetBlogListings(5, ConfigurationManager.AppSettings["BlogPostsSummaryFile"]);

    if (!String.IsNullOrWhiteSpace(category))
    {
        model = model.Where(m => m.Categories!= null && (m.Categories.Split(new char[]{','})).Contains(category)).ToList();
    }

    return new RssResult(model);
}

Eine Action alleine reicht nicht weshalb wir innerhalb der RouteConfig.cs auch eine Route mit folgendem Aufbau anlegen:

routes.MapRoute(
    "RssFeed",
    "Feed/{type}/{category}",
    new { controller = "Home", action = "PostRssFeed", type = "rss", category = UrlParameter.Optional }
);

"category" ist ein optionaler Parameter, was sich nützlich erweisen wird, wenn wir unseren Blog in diverse Verzeichnisse eintragen. Manche Verzeichnisse möchten halt nur die zu ihrem Thema passenden Beiträge. Dieser Anforderung werden wir damit gerecht. Der Aufruf im Browser sieht wie folgt aus:

blog.cayas.de/feed/rss/Business

Das Code-Beispiel ist Case-sensitive. "Business" ist also nicht gleich "business". Der type-Paramter ist aktuell bedeutungslos. Er war interessant als ich darüber nachdachte neben RSS auch weitere Formate wie Atom zu unterstützen. Wer das vorhat, muss die Action um diesen Parameter erweitern.

Wir haben jetzt ein Blogsystem, das die wesentlichen Funktionen, die ein Blog haben sollte, beinhaltet. Auf der To-Do Liste steht noch die Dropbox Integration, die die gewünschte Flexibilität beim Veröffentlichen von neuen Beiträgen bringt.

Fazit

Die Ausgangsbasis für mein Blogsystem war dank der Integration von MarkdownSharp und des einfachen Aufbaus gut gewählt um wie gewünscht nicht alles neu umsetzen zu müssen. Die von mir implementierten Features sind leicht selbst umzusetzen und erweitern das System um wichtige Funktionen.

Für die Zukunft steht der Wechsel auf eine andere Markdown Bibliothek an, da manche Konstrukte, zum Beispiel Tabellen, mit ihr nicht umsetzbar sind. Ein zeitlich gesteuertes Publishing wäre sicherlich auch noch interessant. Das Thema Caching sollte auf jeden Fall auch nicht aus den Augen verloren werden. Aber das kann dann ja jeder für sich umsetzen. Einen guten Ansatz dafür könnte in diesem follow up Blogpost von Andy Gradl liegen.

comments powered by Disqus