Konzultant a lektor pro [ASP].NET Core & REST API

Markdown blog za 60 minut

Miroslav Holec

Miroslav Holec

27. září. 2014 , aktualizace 29. ledna. 2016

Po spuštění nového webu miroslavholec.cz jsem si všiml některých příjemných skutečností, souvisejících s návštěvností, SEO optimalizací a obsahem webu. Došlo mi, že v Google držím první místo na klíčová slova "školení resharper" a uvědomil jsem si, že bych chtěl takto získat traffic.

Jako nejsnazší cesta se nabízela mít blog s dobře cílenými články ale nechtěl jsem ztrácet čas vývojem obskurního cms nebo nastavováním Wordpressu. A tak jsem udělal minimalistický systém blogování založený na syntaxi markdown. Napsat článek nyní znamená nahrát MD soubor + json na patřičné místo v Azure Storage. Celé řešení popíšu v tomto postu.

Výhody celého řešení

1. Nastavení routování

Základem je pěkná URL, o to mi šlo především. Proto jsem chtěl docílit následujícího:

http://www.miroslavholec.cz/blog
http://www.miroslavholec.cz/blog/rss
http://www.miroslavholec.cz/blog/sitemap
http://www.miroslavholec.cz/blog/markdown-blog-za-60-minut
http://www.miroslavholec.cz/blog/1

Můj blog ještě umí štítky, což je defacto jen QueryString parametr a podle toho upravené Actions. O tom ale psát nebudu.

routes.MapRoute(
    name: RouteNames.Blog,
    url: "blog",
    defaults: new { controller = "Blog", action = "Index" }
);

routes.MapRoute(
    name: RouteNames.BlogSitemap,
    url: "blog/sitemap",
    defaults: new { controller = "Blog", action = "Sitemap" }
);

routes.MapRoute(
    name: RouteNames.BlogRss,
    url: "blog/rss/{tag}",
    defaults: new { controller = "Blog", action = "Rss", tag = UrlParameter.Optional }
);

routes.MapRoute(
    name: RouteNames.BlogPage,
    url: "blog/{page}",
    defaults: new { controller = "Blog", action = "Index" },
    constraints: new { page = @"\d+" }
);

routes.MapRoute(
    name: RouteNames.BlogDetail,
    url: "blog/{id}",
    defaults: new { controller = "Blog", action = "Detail" }
);

Hezčí by to asi bylo celé jako samostatná area, ale vzhledem k tomu, jak mám malý web jsem nechtěl věci komplikovat.

Ještě poznamenám, že {id} je unikátní název článku bez diakritiky a mezer. Tedy to, co se objevuje v URL. Zároveň je to unikátní název pro vytváření souborů ve formátu json a md. V případě článku, který právě čtete, existují tedy dva soubory:

markdown-blog-za-60-minut.md
markdown-blog-za-60-minut.json

2. Article.cs + json

Informace o článku ukládám do json souboru, který se následně deserializuje na do objektu Article.

JSON

{
	"Title":"Markdown blog za 60 minut",
	"Id":"markdown-blog-za-60-minut",
	"Body":"Nějaký body pro RSS, zároveň HTML description",
	"IsCommentable":true,
	"Created":"2014-09-26 22:00:00",
	"Keywords":["mvc", "off topic"]
}

Article.cs

Tak nějak předvídatelný :)

public class Article
{
    public string Title { get; set; }
    public string Id { get; set; }
    public string Body { get; set; }
    public string Html { get; set; }
    public bool IsCommentable { get; set; }
    public DateTime Created { get; set; }
    public List<string> Keywords { get; set; }
}

3. BlogController.cs

Všechno je vlastně založeno na dvou základních akcích. Za prvé načtení dávky článků ze storage a za druhé načtení detailů článku. Když umíme načíst dávku článků, můžeme ji zobrazit jako seznam článků, rss, sitemapu atd.

Action Index(int page)

[OutputCache(Duration = CacheDuration, VaryByParam = "page")]
public ActionResult Index(int page = 1)
{
    ViewBag.CurrentPage = page;

    return View(GetArticles(page));
}

To je celé. Parametr page je volitelný. Ukládám si ho do ViewBagu. To hlavní je tedy načtení článků ze storage. To řeší metoda GetArticles(int page). Algoritmus je následující:

Nakonec tu ještě máme třídu Markdown a metodu Transform(string text). To už není moje práce a originální kód ke stažení compileru najdete na aspnetresources.com.

Action Detail(int id)

[OutputCache(Duration = CacheDuration, VaryByParam = "id")]
public ActionResult Detail(string id)
{
    Article article;

    try
    {
        var file = AzureStorage.GetBlob("articles", string.Concat(id, ".json"));
        var content = AzureStorage.GetBlob("articles", string.Concat(id, ".md"));

        article = JsonConvert.DeserializeObject&lt;Article>(file.Content);

        var compiler = new Markdown();
        var html = compiler.Transform(content.Content);

        article.Html = html;
    }
    catch (Exception ex)
    {
       return RedirectToRoute(RouteNames.Blog);
    }

    return View("~/Views/Blog/Detail.cshtml", article);
}

Tady už je to celkem jednoduché. Na základě id článku (tedy názvu souboru bez přípony) se načte json a md soubor. JSON se deserializuje do objektu Article a md se zkompiluje do html, které se uloží do property article.html. Když všechno dopadne dobře, předá se article do view. V případě chyby jen suše uživatele přesměruji na homepage.

Rss() a Sitemap()

Akce Rss() a Sitemap() už jsou prakticky stejné jako Index. Pouze se zavolá GetArticles() metoda, ta vrátí několik článků a ty se následně zobrazí podle potřeby.

Drobnosti na závěr

PageSize ještě udává počet příspěvků na stránku. Je to konstanta přímo v BlogController.cs. CacheDuration je konstanta nastavená někde na BaseControlleru a udává dobu, po kterou zůstává stránka nacachovaná.

AzureStorage.GetBlob() je metoda, která vrací seznam blobů. Stejně tak na tomto místě může být metoda, která vrátí data z lokálního úložiště.

Poznámky k řešení a optimalizace

Obrázky k článkům

Obrázky je nutné nejprve připravit, nahrát do Azure Storage a pak teprve je nalinkovat. Uznávám, že tohle je trochu přes ruku ale pokud obrázky stejně nejprve přeipravujete, jediný krok navíc je tam ten upload do storage.

Zdrojáky

Na zobrazení zdrojáků používám SyntaxHighlighter. Ten mám pevně nastavený na csharp (i když mám ukázky i v jiných jazycích, v 90% je to csharp). Tento kus kódu projde všechny elementy pre a přidá jim potřebnou třídu csharp.

<script type="text/javascript">
    $(document).ready(function () {
        $("pre").addClass("brush: csharp");
        SyntaxHighlighter.all();
    });
</script>

Pokud by to někomu nestačilo, dá se i v markdownu použít html a vše si nastavit ručně. Mně to přišlo zbytečné.

Závěr

Celé řešení je skutečně primitivní. Jak jsem napsal na začátku, jsem nadšený hlavně z toho, že můžu článek psát v klidu bez nutnosti používat nějaký CMS. Článku nastavím ručně datum publikace, nahraju do Azure Storage a mám hotovo. Toto řešení není určitě pro každého, ale výsledek čistého HTML a snadná správa článků stojí za to.


Zeptejte se