This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Utilizzare Markdown per Bloggare
Friday, 02 August 2024
Introduzione
Markdown è un linguaggio di marcatura leggero che si può usare per aggiungere elementi di formatazione ai documenti di testo in forma semplice. Creato da John Gruber in 2004, Markdown ora è una delle lingue di marcazione più popolari del mondoM SK2
Su questo sito uso un approccio super semplice al Blogging, dopo aver provato e fallito a mantenere un blog in passato volevo rendere il più facile possibile scrivere e pubblicare post. Uso la marcatura per scrivere i miei post e questo sito ha un solo servizio usando Markdig per convertire il taglio in HTML.
Perché non un generatore di sito statico?
In una parola semplicità. Questo non è'non sarà un sito ad alta velocità di trafficoM SK2 Uso ASPMSC3NET OutPutCache per immagazzinare le pagine e I ' non lo sto aggiornando talmente spessoMST5 Volevo mantenere il sito il più semplice possibile e non dovrei preoccuparmi della spesa di un generatore di sito statico sia in termini di processo di costruzione che complessità del sitoMSSK6
Per chiarire; generatori di siti statici come Hugo. / Jekyll etc... può essere una buona soluzione per molti siti ma per questo volevo mantenerla semplice. per me. . IM SK1 sono un 25 anno ASP. veterano del NET quindi lo capisci dentro e fuoriMSC4 Questo design del sito aggiunge complessitàMSSK5 Ho visioniMS, serviziMST7 controller e un sacco di HTML manuale & CSS ma ioMSV9 mi sento a mio agio con quelloMSS10
Perché non una base di dati?
- Semplicità di progettazione; Le database sono potenti sistemi per immagazzinare i dati (e ioM SK2ne aggiungerò uno per commenti in breve ) tuttavia aggiungono anche complessità in modo corretto. Usate database specialmente in un'applicazione ASP.NET dove aggiungete un LOT di codice, indipendentemente da se usate EF Core, Dapper o SQL crudo con ADO.NET. Volevo mantenere il sito il più semplice possibile. per cominciare..
- Facilità di aggiornare e implementazione. Questo sito è destinato a dimostrare quanto semplice sia il Docker M SK1 Docker Compose può essere per far funzionare un sito. Posso aggiornare il sito controllando il nuovo codice (incluendo il contenutoMSC4 su GitHubMska5 l'Azione funzionaMske6 costruisce l'immagine poi la tecnica Watchtower nel mio docker compose file aggiorna l'immaginatura del sito automaticamenteMNK7 È un modo molto semplice per aggiornare un site e volevo tenerlo così
- Condurre duplicate; dato che ho dati ZERO che non sono't all'interno dell'immagine del docker significa che posso ripetere facilmente duplicati accurati localmente M SK2 sul mio piccolo cluster Ubuntu qui a casa ). Questo è un ottimo modo per testare i cambiamenti con il docker | quando ho fatto i cambiamenti ImageSharp. ) prima di metterli sul sito in diretta.
- Perché non volevo' volere! volevo vedere quanto lontano potessi arrivare con un semplice progetto di sito e fino ad ora sono piuttosto felice.
Come scrivete i vostri post?
Metto semplicemente un nuovo file .md nel cartello Markdown e il sito lo raccoglie e lo riproduce ( quando ricordo di inserirlo come contenuto, questo lo assicuraM SK3di essere accessibile nei file di output
Poi quando faccio il checkin sul sito su GitHub l'Azione funziona e il sito è aggiornato. 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)

Come aggiungere immagini?
Dato che ho appena aggiunto l'immagine qui, Io' vi mostrerò come l'ho fattoMSC2 Ho semplicemente aggiunto l’immagine al folder wwwrootM SK3articleimages e la ho riferita nel file di demarcazione come questo.

Poi aggiungo un'estensione al mio pipeline Markdig che li riscrive alla corretta URL (il resto sulla semplicità). Vedete qui il codice sorgente per l'estensione.
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;
}
}
Il BlogService.
Il BlogService è un semplice servizio che legge i file Markdown dal folder Markdown e li converte in HTML usando Markdig.
La fonte completa di questo è in basso. Qui..
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);
}
}
Stiamo processando i file
Il codice per elaborare i file di scarico in HTML è abbastanza semplice, Uso la Markdig biblioteca per convertire il scarico nel HTML e poi uso qualche espressione regolare per estrarre le categorie e la data pubblicata dal file di Scarico
Il metodo GetPage viene usato per estrarre il contenuto del file di scarico, ha alcuni passaggi:
- Estratto il titolo. Con la convenzione uso la prima linea del file di scarico come titolo della posta. Quindi posso semplicemente fare
var lines = File.ReadAllLines(page);
// Get the title from the first line
var title = lines.Length > 0 ? Markdown.ToPlainText(lines[0].Trim()) : string.Empty;
Dato che il titolo è preceduto da "#" uso la metodo Markdown.ToPlainText per estrarre dal titolo "#"
- Estratto le categorie. Ogni post può avere fino a due categorie questo metodo li extrae poi rimuovo quella targhetta dal file di scarico.
// 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, "");
Il metodo GetCategories usa un'espressione regolare per estrarre le categorie dal file di scarico.
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;
}
- Estratto la data pubblicata. Dopodiché estraggo la data dal post ( stavo usando la data creata ma come lo deploy con un intero immagine di docker significa che questo non è più molto utile ') quindi io non uso un'espressione regolareM SK4 Questo analizza un'etichetta nella forma che è in ogni file .md.
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, "");
- Estratto il contenuto. In realtà ottenere il contenuto è abbastanza semplice questo usa un pipeline (per la sostituzione dell'immaginazione menzionata sopra) poi optionalmente mi dà un testo semplice per la lista dei post o HTML per il post veroM SK2
pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Use<ImgExtension>().Build();
var processed =
html ? Markdown.ToHtml(restOfTheLines, pipeline) : Markdown.ToPlainText(restOfTheLines, pipeline);
Prendete il 'slug' Questo è semplicemente il nome del file senza l'estensione:
private string GetSlug(string fileName) { var slug = System.IO.Path.GetFileNameWithoutExtension(fileName); return slug.ToLowerInvariant(); }Rendere il contenuto. Ora abbiamo contenuti che possiamo mostrare per il blog!
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);
}