Att använda markerdown för blogging
Inleiding
Markdown är en lätt markup språk som du kan använda för att lägga till formaterande element i vanliga textdokument.
På den här sidan använder jag en super enkel metod för att blogga , jag har försökt och misslyckats med att underhålla en blogg i det förflutna Jag ville göra det så enkelt som möjligt att skriva och publicera artiklar. Jag använder markerdown för att skriva mina artiklar och denna sidan har en enda tjänst som Markdig att konvertera markerdown till HTML.
Varför inte en statisk platsgenerator?
I ett ord enkelhet. Det här är inte ' det kommer inte att vara en super hög trafikplats, Jag använder ASP, . NET OutPutCache för att kassera sidorna och IM SK4 jag kommer inte göra den uppdaterad så ofta som vanligtMSC5 Jag ville behålla sidan så enkel som möjligt och inte behöva oroa mig för överskottet från en statisk webbplatsgenerator både i termer av byggprocessen och komplexiteten på sidan.
För att klargöra ";", statiska platsgenerator som Hugo / Jekyll etc... kan vara en bra lösning för många webbplatser men för den här ville jag hålla den enkla för mig så mycket som möjligt. I ' är en M SK2 år ASP. NET veteran så förstår du det inifrån och utåtMSC4 Den här webbplatsdesignen lägger till komplexitet~;~ Jag har synpunkter~,~ tjänster , kontroller och en LITT av handgriplig HTMLMska8 CSS men jag är bekväm med detM Ska9
Varför inte en database?
- Enkelhet i design; Databaser är kraftfulla system för data lagring | ( | och jag |' | ska lägga till ett för kommentarer snart | på rätt sätt använda databaser speciellt i en ASP-app, så lägger du till ett LITT kod, oavsett om du använder EF: s kärna, Damper eller rå SQL med ADO.NET. Jag ville hålla sidan så enkel som möjligt för att börja med.
- Låghet att uppdatera och implementera. Denna sida är tänkt att demonstrera hur enkel Docker | & | Docker Compose kan vara för att köra en sida |. | Jag kan uppdatera sidan genom att kolla i ny kod ♫ ( | inklusive innehåll ♪ ) | till GitHub |
- Att köra duplikater; eftersom jag har ZERO-data som inte finns i dockerns bild, betyder det att jag enkelt kan köra exakta dupliker lokalt ( på min lilla Ubuntu- kluster här hemma ). Det är ett bra sätt att testa ändringar med dockerna. när jag gjorde ImageSharp-förändringar ) innan de skickas till live-sitet
- För jag ville inte - jag ville se hur långt jag kunde komma med en enkel webbplatsdesign - och så långt är jag - jag - ganska nöjd med den.
Hur skriver du upp dina meddelanden?
Jag släpper helt enkelt in en ny .md file i Markdown-ordnet och platsen plockar upp den och visar den. ♫ ( ♫ då jag minns att sätta in den som innehåll. ♪ , ♫ detta gör den tillgänglig i utgångsfilerna.
När jag sen går in på GitHub så kör Actionen igång och webbplats är uppdaterad. Simple!
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)

Hur lägger man till bilder?
Eftersom jag precis lagt till bilden här, så ska jag visa er hur jag gjorde det.

Jag lägger sedan till en förlängning till min Markdig pipeline som skriver om dessa till den korrekta URL (all about simplicity). Se här för källkoden till uitbreidingen.
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;
}
}
The BlogService.
BlogService är en enkel tjänst som läser markerdown-filer från Markdown-ordnet och konverterar dem till HTML med Markdig.
Den fulla källan för detta är här.
Click to see the source code for the BlogService
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);
}
}
Bearbetar lêer
Koden för att bearbeta markerdown- plierna till HTML är ganska enkel, Jag använder Markdig biblioteket för att konvertera markerdown till HTML och sen använder jag några regelbundna uttryck för att extrahera kategorier och den publicerade datumen från markerdownfilen.
Metoden GetPage används för att extrahera innehållet i markerdown- pliken, den har några steg
- Ta ut titeln Enligt konventionen använder jag den första linjen i markerdown- pliken som titeln till meddelandet.
var lines = File.ReadAllLines(page);
// Get the title from the first line
var title = lines.Length > 0 ? Markdown.ToPlainText(lines[0].Trim()) : string.Empty;
Eftersom titeln är förefixad med "#" använder jag Markdown-metoden.ToPlainText för att dra bort "#" från titeln
- Extrahera kategorierna Varje artikel kan ha upp till två kategorier den här metoden extraherar dessa sedan tar jag bort taget från markerdown- pliken.
// 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, "");
GetCategories-metoden använder en regellig uttryck för att extrahera kategorierna från markerdownen.
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;
}
- Koppa ut den publicerade datumen Jag extraherar sedan datumet från meddelandet ( Jag använde skapade datumet men hur jag använder det här med ett helt docker-bild betyder att detta inte är riktigt användbart längre ) så jag inte använder ett vanligt uttryck Det här tolkar ett tag i formen som finns i varje .md-Datei
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, "");
- Extrahera innehållet Faktum är att få fram innehållet är ganska enkelt det här använder en pipeline ( för att ersätta bildens tag som nämns tidigare ) sedan ger man mig optionellt enkel text för listan av Beiträge eller HTML för det faktiska meddelandet
pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Use<ImgExtension>().Build();
var processed =
html ? Markdown.ToHtml(restOfTheLines, pipeline) : Markdown.ToPlainText(restOfTheLines, pipeline);
Ta 'slug' Det här är helt enkelt filenamen utan rozszerzen:
private string GetSlug(string fileName) { var slug = System.IO.Path.GetFileNameWithoutExtension(fileName); return slug.ToLowerInvariant(); }Ge tillbaka innehållet Nu har vi sidan innehåll som vi kan visa för bloggen!
The GetPage Method
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);
}