# Markdown gebruiken voor Bloggen

<!--category-- ASP.NET, Markdown -->
<datetime class="hidden">2024-08-02T17:00</datetime>

## Inleiding

Markdown is een lichtgewicht markuptaal die je kan gebruiken om formaterende elementen aan eenvoudige tekstdocumenten toe te voegen.. Geskep door John Gruber in 2004, Markdown wordt nu één van de werelds populairste markup talen.

Op deze site gebruik ik een super eenvoudige aanpak om te bloggen, ik heb geprobeerd en mislukt een blog in het verleden te onderhouden Ik wilde het zo gemakkelijk mogelijk maken om posten te schrijven en te publiceren. Ik gebruik markdown om mijn posts te schrijven, en deze site heeft één enkele dienst die [Markdig](https://github.com/xoofx/markdig) om de markdown om te zetten in HTML.

[TOC]

## Waarom geen Static Site generator?

In een woord 'eenvoud'. Dit is niet'het gaat geen super-hoge verkeerssite worden , Ik gebruik ASP, .NET OutPutCache om de pagina's te cachen en IM SK4 ik ga het niet zo vaak opdaterenMSC5 Ik wilde de site zo eenvoudig mogelijk houden en me geen zorgen hoeven te maken over de overhead van een statische sitegenerator zowel in termen van het bouwproces als de complexiteit van de site.

Om ; statische sitegeneratoren te verklaren, zoals: [Hugo](https://gohugo.io/) / [Jekyll](https://jekyllrb.com/) etc... kan een goede oplossing zijn voor vele sites, maar voor deze wilde ik het simpel houden. *Voor mij* Ik ben een 25 jaar ASP, . Net-veteran, dus begrijp het binnen en buiten.

## Waarom geen database?

1. Eenvoud van ontwerp; Databanken zijn krachtige systemen voor het opslaan van gegevens ( en IM SK2 voeg ik er eentje toe voor commentaties op korte termijn ) maar ze voegen ook complexiteit toe. *correct.* Gebruik vooral databases in een ASP-applicatie, je voegt een LITT aan code toe. [EF Core](https://learn.microsoft.com/en-us/ef/core/), [Dapper](https://github.com/DapperLib/Dapper) Of ruwe SQL met ADO.NET. Ik wilde de site zo eenvoudig mogelijk houden. *Om te beginnen*.
2. Ease of update and deployment. Deze site is bedoeld om te demonstreren hoe eenvoudig Docker & Docker Compose kan zijn om een site te runnenM SK2 Ik kan de site opdateer door in nieuwe code te checken ( inclusief inhoudMSC4 naar GitHubMska5 de actie looptMske6 bouwt het beeld dan de Watchtower-methode in mijn docker-composer-lêer het site-beeld automatisch aktualiseertMsek7 Dit is een heel simpele manier om een website te updaten en ik wilde hem zo houden.
3. Duplikaten uitvoeren; omdat ik ZERO-gegevens heb die niet in het dockerbeeld zitten, betekent dat ik nauwkeurige kopieën lokaal kan uitvoeren ( op mijn kleine Ubuntu-cluster hier thuis ). Dit is een geweldige manier om veranderingen met de docker te testen. [Ik maakte de ImageSharp-wijzigingen.](/blog/imagesharpwithdocker) ) voordat je ze op de live-site plaatst.
4. Want ik wilde niet zien hoe ver ik met een eenvoudige site-ontwerp kon komen en tot nu toe ben ik er erg blij mee.

## Hoe schrijf je posten?

Ik gooi gewoon een nieuwe .md-lêer in de Markdown-gids en de site pakt hem op en maakt het weer. (als ik me herinner om het als inhoud te aetten.

Als ik de site naar GitHub kijk, loopt de actie en de site wordt opgedateerd.

```mermaid
flowchart LR
    A[Write New Markdown File] -->|Checkin To Github| B(Github Action Triggers) -->  C(Builds Docker Image) --> D(Watchtower Pulls new Image) --> E(Site Updated)
   
  
```

![setascontent.png](setascontent.png)

## Hoe voeg je beelden toe?

Omdat ik net het beeld hier toevoegde,,, "I'll show you how I did it", . Ik voegde het beeld simpelweg toe in de wwwroot-directory, /, "articleimages folder" en refereerde ernaar in de markdown-file zoals deze.

```markdown
![setascontent.png](setascontent.png)
```

Ik voeg dan een uitbreiding toe aan mijn Markdig-pijplijn die deze herschrijft naar de juiste URL. [Kijk hier naar de broncode voor de uitbreiding.](https://github.com/scottgal/mostlylucidweb/blob/main/Mostlylucid/MarkDigExtensions/ImgExtension.cs)

```csharp
using Markdig;
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;

namespace Mostlylucid.MarkDigExtensions;

public class ImgExtension : IMarkdownExtension
{
    public void Setup(MarkdownPipelineBuilder pipeline)
    {
        pipeline.DocumentProcessed += ChangeImgPath;
    }

    public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
    {
    }

    public void ChangeImgPath(MarkdownDocument document)
    {
        foreach (var link in document.Descendants<LinkInline>())
            if (link.IsImage)
                link.Url = "/articleimages/" + link.Url;
    }
}
```

## De Blogservice.

De BlogService is een eenvoudige dienst die de markdown-bestanden uit de Markdown-gids leest en ze in HTML omvormt met Markdig..

De volledige bron voor dit is onder en [Hier.](https://github.com/scottgal/mostlylucidweb/blob/main/Mostlylucid/Services/BlogService.cs).

<details>
<summary>Click to see the source code for the BlogService</summary>

```csharp

using System.Globalization;
using System.Text.RegularExpressions;
using Markdig;
using Microsoft.Extensions.Caching.Memory;
using Mostlylucid.MarkDigExtensions;
using Mostlylucid.Models.Blog;

namespace Mostlylucid.Services;

public class BlogService
{
private const string Path = "Markdown";
private const string CacheKey = "Categories";

    private static readonly Regex DateRegex = new(
        @"<datetime class=""hidden"">(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})</datetime>",
        RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.NonBacktracking);

    private static readonly Regex WordCoountRegex = new(@"\b\w+\b",
        RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.NonBacktracking);

    private static readonly Regex CategoryRegex = new(@"<!--\s*category\s*--\s*([^,]+?)\s*(?:,\s*([^,]+?)\s*)?-->",
        RegexOptions.Compiled | RegexOptions.Singleline);

    private readonly ILogger<BlogService> _logger;

    private readonly IMemoryCache _memoryCache;

    private readonly MarkdownPipeline pipeline;

    public BlogService(IMemoryCache memoryCache, ILogger<BlogService> logger)
    {
        _logger = logger;
        _memoryCache = memoryCache;
        pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Use<ImgExtension>().Build();
        ListCategories();
    }


    private Dictionary<string, List<string>> GetFromCache()
    {
        return _memoryCache.Get<Dictionary<string, List<string>>>(CacheKey) ?? new Dictionary<string, List<string>>();
    }

    private void SetCache(Dictionary<string, List<string>> categories)
    {
        _memoryCache.Set(CacheKey, categories, new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12)
        });
    }

    private void ListCategories()
    {
        var cacheCats = GetFromCache();
        var pages = Directory.GetFiles("Markdown", "*.md");
        var count = 0;

        foreach (var page in pages)
        {
            var pageAlreadyAdded = cacheCats.Values.Any(x => x.Contains(page));

            if (pageAlreadyAdded) continue;


            var text = File.ReadAllText(page);
            var categories = GetCategories(text);
            if (!categories.Any()) continue;
            count++;
            foreach (var category in categories)
                if (cacheCats.TryGetValue(category, out var pagesList))
                {
                    pagesList.Add(page);
                    cacheCats[category] = pagesList;
                    _logger.LogInformation("Added category {Category} for {Page}", category, page);
                }
                else
                {
                    cacheCats.Add(category, new List<string> { page });
                    _logger.LogInformation("Created category {Category} for {Page}", category, page);
                }
        }

        if (count > 0) SetCache(cacheCats);
    }

    public List<string> GetCategories()
    {
        var cacheCats = GetFromCache();
        return cacheCats.Keys.ToList();
    }


    public List<PostListModel> GetPostsByCategory(string category)
    {
        var pages = GetFromCache()[category];
        return GetPosts(pages.ToArray());
    }

    public BlogPostViewModel? GetPost(string postName)
    {
        try
        {
            var path = System.IO.Path.Combine(Path, postName + ".md");
            var page = GetPage(path, true);
            return new BlogPostViewModel
            {
                Categories = page.categories, WordCount = WordCount(page.restOfTheLines), Content = page.processed,
                PublishedDate = page.publishDate, Slug = page.slug, Title = page.title
            };
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Error getting post {PostName}", postName);
            return null;
        }
    }

    private int WordCount(string text)
    {
        return WordCoountRegex.Matches(text).Count;
    }


    private string GetSlug(string fileName)
    {
        var slug = System.IO.Path.GetFileNameWithoutExtension(fileName);
        return slug.ToLowerInvariant();
    }

    private static string[] GetCategories(string markdownText)
    {
        var matches = CategoryRegex.Matches(markdownText);
        var categories = matches
            .SelectMany(match => match.Groups.Cast<Group>()
                .Skip(1) // Skip the entire match group
                .Where(group => group.Success) // Ensure the group matched
                .Select(group => group.Value.Trim()))
            .ToArray();
        return categories;
    }

    public (string title, string slug, DateTime publishDate, string processed, string[] categories, string
        restOfTheLines) GetPage(string page, bool html)
    {
        var fileInfo = new FileInfo(page);

        // Ensure the file exists
        if (!fileInfo.Exists) throw new FileNotFoundException("The specified file does not exist.", page);

        // Read all lines from the file
        var lines = File.ReadAllLines(page);

        // Get the title from the first line
        var title = lines.Length > 0 ? Markdown.ToPlainText(lines[0].Trim()) : string.Empty;

        // Concatenate the rest of the lines with newline characters
        var restOfTheLines = string.Join(Environment.NewLine, lines.Skip(1));

        // Extract categories from the text
        var categories = GetCategories(restOfTheLines);

        var publishedDate = fileInfo.CreationTime;
        var publishDate = DateRegex.Match(restOfTheLines).Groups[1].Value;
        if (!string.IsNullOrWhiteSpace(publishDate))
            publishedDate = DateTime.ParseExact(publishDate, "yyyy-MM-ddTHH:mm", CultureInfo.InvariantCulture);

        // Remove category tags from the text
        restOfTheLines = CategoryRegex.Replace(restOfTheLines, "");
        restOfTheLines = DateRegex.Replace(restOfTheLines, "");
        // Process the rest of the lines as either HTML or plain text
        var processed =
            html ? Markdown.ToHtml(restOfTheLines, pipeline) : Markdown.ToPlainText(restOfTheLines, pipeline);

        // Generate the slug from the page filename
        var slug = GetSlug(page);


        // Return the parsed and processed content
        return (title, slug, publishedDate, processed, categories, restOfTheLines);
    }

    public List<PostListModel> GetPosts(string[] pages)
    {
        List<PostListModel> pageModels = new();

        foreach (var page in pages)
        {
            var pageInfo = GetPage(page, false);

            var summary = Markdown.ToPlainText(pageInfo.restOfTheLines).Substring(0, 100) + "...";
            pageModels.Add(new PostListModel
            {
                Categories = pageInfo.categories, Title = pageInfo.title,
                Slug = pageInfo.slug, WordCount = WordCount(pageInfo.restOfTheLines),
                PublishedDate = pageInfo.publishDate, Summary = summary
            });
        }

        pageModels = pageModels.OrderByDescending(x => x.PublishedDate).ToList();
        return pageModels;
    }


    public List<PostListModel> GetPostsForFiles()
    {
        var pages = Directory.GetFiles("Markdown", "*.md");
        return GetPosts(pages);
    }
}
```

</details>
Zoals je ziet heeft dit een paar elementen:

### Verwerking van lêers

De code om de markerdown-bestanden in HTML te verwerken is vrij simpel, Ik gebruik de Markdig-bibliotheek om de markdown naar HTML te vertalen en dan gebruik ik een paar regelmatige uitdrukkingen om de categorieën en het gepubliceerde datum van de marker down-bestand te extraheren.

De GetPage-methode wordt gebruikt om de inhoud van de markerdown-file te extraheren.

1. Pak de Titel uit
   Op conventionele wijze gebruik ik de eerste lijn van de markdown-file als de titel van het bericht.

```csharp
        var lines = File.ReadAllLines(page);

        // Get the title from the first line
        var title = lines.Length > 0 ? Markdown.ToPlainText(lines[0].Trim()) : string.Empty;
```

Omdat de titel bijgevoegd is met "#" gebruik ik de Markdown-Methode.ToPlainText om de "#" uit de titel te halen.

2. Pak de categorieën uit.
   Elke post kan tot twee categorieën hebben deze methode extraheert deze dan verwyder ik die tag van de markerdown-file.

```csharp
// Concatenate the rest of the lines with newline characters
        var restOfTheLines = string.Join(Environment.NewLine, lines.Skip(1));

        // Extract categories from the text
        var categories = GetCategories(restOfTheLines);

   // Remove category tags from the text
        restOfTheLines = CategoryRegex.Replace(restOfTheLines, "");

```

De GetCategories-methode maakt gebruik van een gewone uitdrukking om de categorieën te extraheren uit de merkdown-file.

```csharp
    private static readonly Regex CategoryRegex = new(@"<!--\s*category\s*--\s*([^,]+?)\s*(?:,\s*([^,]+?)\s*)?-->",
        RegexOptions.Compiled | RegexOptions.Singleline);

    private static string[] GetCategories(string markdownText)
    {
        var matches = CategoryRegex.Matches(markdownText);
        var categories = matches
            .SelectMany(match => match.Groups.Cast<Group>()
                .Skip(1) // Skip the entire match group
                .Where(group => group.Success) // Ensure the group matched
                .Select(group => group.Value.Trim()))
            .ToArray();
        return categories;
        
        
    }
```

3. Uittrek de gepubliceerde datum.
   Ik haal dan de datum uit de post. (Ik gebruikte het gecreëerde datum, maar hoe ik dit inzet met een hele dockerbeeld betekent dat dit niet meer echt bruikbaar is.
   Dit berekent een tag in de vorm die in elke .md lêer is.

```razor
 <datetime class="hidden">2024-08-02T17:00</datetime>
```

```csharp
     private static readonly Regex DateRegex = new(
        @"<datetime class=""hidden"">(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})</datetime>",
        RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.NonBacktracking);
     
           var publishedDate = fileInfo.CreationTime;
        var publishDate = DateRegex.Match(restOfTheLines).Groups[1].Value;
        if (!string.IsNullOrWhiteSpace(publishDate))
            publishedDate = DateTime.ParseExact(publishDate, "yyyy-MM-ddTHH:mm", CultureInfo.InvariantCulture);

     
        restOfTheLines = DateRegex.Replace(restOfTheLines, "");
```

4. Voeg de inhoud uit.
   Het krijgen van de inhoud is eigenlijk vrij eenvoudig, dit gebruikt een pipeline ( voor het beeld-tag vervangen, zoals gezegd ) dan geeft het me optioneel een eenvoudige tekst voor de lijst van posten of HTML voor de echte post.

```csharp
    pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Use<ImgExtension>().Build();
    
   var processed =
            html ? Markdown.ToHtml(restOfTheLines, pipeline) : Markdown.ToPlainText(restOfTheLines, pipeline);
```

5. Kry de 'slug'
   Dit is simpelweg de lêernaam zonder de uitbreiding:
   
   ```csharp
       private string GetSlug(string fileName)
       {
           var slug = System.IO.Path.GetFileNameWithoutExtension(fileName);
           return slug.ToLowerInvariant();
       }
   ```

6. Return the content
   Nu hebben we paginainhoud dat we kunnen laten zien voor de blog!

<details>
<summary> The GetPage Method</summary>

```csharp
public (string title, string slug, DateTime publishDate, string processed, string[] categories, string
        restOfTheLines) GetPage(string page, bool html)
    {
        var fileInfo = new FileInfo(page);

        // Ensure the file exists
        if (!fileInfo.Exists) throw new FileNotFoundException("The specified file does not exist.", page);

        // Read all lines from the file
        var lines = File.ReadAllLines(page);

        // Get the title from the first line
        var title = lines.Length > 0 ? Markdown.ToPlainText(lines[0].Trim()) : string.Empty;

        // Concatenate the rest of the lines with newline characters
        var restOfTheLines = string.Join(Environment.NewLine, lines.Skip(1));

        // Extract categories from the text
        var categories = GetCategories(restOfTheLines);

        var publishedDate = fileInfo.CreationTime;
        var publishDate = DateRegex.Match(restOfTheLines).Groups[1].Value;
        if (!string.IsNullOrWhiteSpace(publishDate))
            publishedDate = DateTime.ParseExact(publishDate, "yyyy-MM-ddTHH:mm", CultureInfo.InvariantCulture);

        // Remove category tags from the text
        restOfTheLines = CategoryRegex.Replace(restOfTheLines, "");
        restOfTheLines = DateRegex.Replace(restOfTheLines, "");
        // Process the rest of the lines as either HTML or plain text
        var processed =
            html ? Markdown.ToHtml(restOfTheLines, pipeline) : Markdown.ToPlainText(restOfTheLines, pipeline);

        // Generate the slug from the page filename
        var slug = GetSlug(page);


        // Return the parsed and processed content
        return (title, slug, publishedDate, processed, categories, restOfTheLines);
    }
```

</details>
De code hieronder laat zien hoe ik de lijst van blogposts genereer. `GetPage(page, false)` methode om de titel te extraheren, categorieën, publicatiedatum en de verwerkte inhoudM SK2

```csharp
     public List<PostListModel> GetPosts(string[] pages)
    {
        List<PostListModel> pageModels = new();

        foreach (var page in pages)
        {
            var pageInfo = GetPage(page, false);

            var summary = Markdown.ToPlainText(pageInfo.restOfTheLines).Substring(0, 100) + "...";
            pageModels.Add(new PostListModel
            {
                Categories = pageInfo.categories, Title = pageInfo.title,
                Slug = pageInfo.slug, WordCount = WordCount(pageInfo.restOfTheLines),
                PublishedDate = pageInfo.publishDate, Summary = summary
            });
        }

        pageModels = pageModels.OrderByDescending(x => x.PublishedDate).ToList();
        return pageModels;
    }


    public List<PostListModel> GetPostsForFiles()
    {
        var pages = Directory.GetFiles("Markdown", "*.md");
        return GetPosts(pages);
    }
```