StarBlog Blog Development Notes (4): Bulk Import of Markdown Blogs
Series of Articles Developing a Blog Project Based on .NetCore StarBlog - (1) Why Do You Need to Write a Blog Yourself? Developing a Blog Project Based on .NetCore StarBlog - (2) Environment Setup and Project Creation Developing a Blog Project Based on .NetCore StarBlog - (3) Model Design Developing a Blog Project Based on .NetCore StarBlog - (4) Batch Import of Markdown Blogs ... Preface Last week, the model design of the blog was introduced, and now the model design...
目录11 个章节
Series of Articles
- Developing a Blog Project with .NetCore: StarBlog - (1) Why Build Your Own Blog?
- Developing a Blog Project with .NetCore: StarBlog - (2) Environment Setup and Project Creation
- Developing a Blog Project with .NetCore: StarBlog - (3) Model Design
- Developing a Blog Project with .NetCore: StarBlog - (4) Batch Importing Markdown Blogs
- ...
Introduction
Last week, we discussed the model design for the blog. Now that the model is ready, it's time to start importing data.
The goal is to import all the markdown files within a folder, using the directory structure as the article categories, the file names as the article titles, and the file creation and update dates as the publication dates.
The general approach is to first traverse the directory using .Net's standard library, process the article content with a third-party markdown parsing library, and then write it to the database via ORM.
PS: Tomorrow is Labor Day. Happy May Day to all the working-class laborers!
Related Technologies
- File I/O related APIs
- Regular expressions
- ORM: FreeSQL
- Markdown parsing library: Markdig
Getting Started with Code
We'll begin by discussing the most critical parts: parsing Markdown content, extracting images, and handling headings.
To process Markdown content, I searched for relevant libraries and discovered that for .NET Core, the only available option is the Markdig library. Since it's still in development and lacks complete documentation, I had to piece things together by reading the GitHub repository's minimal explanations and experimenting with examples. Unfortunately, there aren't many good alternatives, and I'm too lazy (or maybe just bad at coding) to reinvent the wheel, so I'll just make do with it.
Markdig official website: https://github.com/xoofx/markdig
Create a new class in the StarBlog.Migrate project called PostProcessor. We'll implement the Markdown file processing logic in this class.
The complete code for PostProcessor.cs is here: https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/PostProcessor.cs
Constructor:
private readonly Post _post;
private readonly string _importPath;
private readonly string _assetsPath;
public PostProcessor(string importPath, string assetsPath, Post post) {
_post = post;
_assetsPath = assetsPath;
_importPath = importPath;
}
Here:
Post: The article model designed in our previous sectionimportPath: The path to the Markdown folder to be importedassetsPath: The storage path for resource files, used to store images from the Markdown files. In this project, the path is set toStarBlog.Web/wwwroot/media/blog
Article Summary Extraction
For article summary extraction, I implemented a simple approach: converting the Markdown content into plain text, then truncating it to the first N characters to form the summary. The code is as follows:
public string GetSummary(int length) {
return _post.Content == null
? string.Empty
: Markdown.ToPlainText(_post.Content).Limit(length);
}
Article Status and Title Handling
Previously, when writing blogs using local markdown files, out of personal habit, I would add status prefixes to the file names. For example, an unfinished article would look like this:
(Unfinished) StarBlog Development Notes(4): Batch Markdown Blog Import
Or for completed but unpublished articles, I would add (Unpublished).
Once published, I would remove the prefix. Therefore, during import, I need to use a regular expression to extract this prefix so that the blog article title imported into the database no longer carries the prefix.
The code is as follows:
public (string, string) InflateStatusTitle() {
const string pattern = @"^((.+))(.+)$";
var status = _post.Status ?? "Published";
var title = _post.Title;
if (string.IsNullOrEmpty(title)) return (status, $"Untitled Article {_post.CreationTime.ToLongDateString()}");
var result = Regex.Match(title, pattern);
if (!result.Success) return (status, title);
status = result.Groups[1].Value;
title = result.Groups[2].Value;
_post.Status = status;
_post.Title = title;
if (!new[] { "Published", "Released" }.Contains(_post.Status)) {
_post.IsPublish = false;
}
return (status, title);
}
The logic is straightforward: first check if the title is empty (which is unlikely for a file name, but done for thoroughness). Then, use a regular expression for matching. If a match is found, extract the status; if not, default to "Published".
Image Extraction & Replacement
The most complex part of Markdown content processing is this section, so I previously wrote a separate article to explain it in detail. Therefore, I won't repeat too much here. For more information, you can refer to my earlier article: Parsing Markdown Documents in C# and Implementing Image Link Replacement
Now, returning to our blog project, the code for this part is as follows:
public string MarkdownParse() {
if (_post.Content == null) {
return string.Empty;
}
var document = Markdown.Parse(_post.Content);
foreach (var node in document.AsEnumerable()) {
if (node is not ParagraphBlock { Inline: { } } paragraphBlock) continue;
foreach (var inline in paragraphBlock.Inline) {
if (inline is not LinkInline { IsImage: true } linkInline) continue;
if (linkInline.Url == null) continue;
if (linkInline.Url.StartsWith("http")) continue;
// Path handling
var imgPath = Path.Combine(_importPath, _post.Path, linkInline.Url);
var imgFilename = Path.GetFileName(linkInline.Url);
var destDir = Path.Combine(_assetsPath, _post.Id);
if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
var destPath = Path.Combine(destDir, imgFilename);
if (File.Exists(destPath)) {
// Handle duplicate image names
var imgId = GuidUtils.GuidTo16String();
imgFilename = $"{Path.GetFileNameWithoutExtension(imgFilename)}-{imgId}.{Path.GetExtension(imgFilename)}";
destPath = Path.Combine(destDir, imgFilename);
}
// Replace image link
linkInline.Url = imgFilename;
// Copy image
File.Copy(imgPath, destPath);
Console.WriteLine($"Copying {imgPath} to {destPath}");
}
}
using var writer = new StringWriter();
var render = new NormalizeRenderer(writer);
render.Render(document);
return writer.ToString();
}
The implementation steps are roughly as follows:
- Use the Markdown parsing functionality from the Markdig library.
- Extract all image links.
- Then, based on the
importPathdirectory passed in the constructor earlier, construct the full path of the image. - Next, copy the images into the
assetsPathdirectory. - Finally, replace the image paths in the Markdown with the newly generated image filenames.
Summary
Currently, this solution handles most images in Markdown files without issues, but there is still a problem!
Images with filenames containing spaces cannot be recognized!
This issue is a limitation of the Markdig library, I suppose. I tried reading through Markdig's source code to see if I could fix it, but unfortunately, I couldn't understand it well enough. So for now, there's no perfect workaround—I'll just have to submit an issue to the official repository. This library is actively maintained, so there's hope that the developers will address and fix this problem.
Traversing Directories
We've covered the key parts earlier; now let's talk about the relatively simple task of traversing directory files. If you're already proficient with file I/O, feel free to skip this section~
I implemented this using a recursive method, referencing an official Microsoft blog post: https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree
The key code is as follows; the complete code can be found here: https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/Program.cs
void WalkDirectoryTree(DirectoryInfo root) {
Console.WriteLine($"Scanning folder: {root.FullName}");
FileInfo[]? files = null;
DirectoryInfo[]? subDirs = null;
try {
files = root.GetFiles("*.md");
}
catch (UnauthorizedAccessException e) {
Console.WriteLine(e.Message);
}
catch (DirectoryNotFoundException e) {
Console.WriteLine(e.Message);
}
if (files != null) {
foreach (var fi in files) {
Console.WriteLine(fi.FullName);
// Code for processing articles, omitted
}
}
subDirs = root.GetDirectories();
foreach (var dirInfo in subDirs) {
if (exclusionDirs.Contains(dirInfo.Name)) {
continue;
}
if (dirInfo.Name.EndsWith(".assets")) {
continue;
}
WalkDirectoryTree(dirInfo);
}
}
This method is called "pre-order traversal," meaning it processes the files in the directory first, then processes the subdirectories within it.
The recursive method is simple to write, but it has a drawback: if the directory structure is deeply nested, it might cause a stack overflow. You could consider switching to a traversal based on the Stack<T> pattern. However, since a blog's directory hierarchy shouldn't be too deep, I opted for the simpler approach~
Writing to the Database
The ORM used in this project is FreeSQL. ORM operations will be covered in more detail in subsequent website development articles, so I'll skip them here. The code for writing article data to the database is quite straightforward—you can view it directly at: https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/Program.cs
Conclusion
OK, that covers everything about batch blog import. Once you handle a few tricky parts, it's not difficult anymore. Having the article data ready makes it much easier to start developing the blog website~
That's about it for now. See you in the next post~
Also, all project code has been uploaded to GitHub. Everyone is welcome to Star/Fork!
- Blog backend + frontend project address: https://github.com/Deali-Axy/StarBlog
- Admin backend frontend project address: https://github.com/Deali-Axy/StarBlog-Admin
_EOF_
StarBlog Blog Development Notes (4): Bulk Import of Markdown Blogs
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
StarBlog开源博客项目 - 2023年底更新内容一览
前台UI勉强能看,后台总算不那么辣眼睛了。数据库分页改对了,不然性能差得离谱。新增slug检查?哦,终于知道避免重复了。
2023-12-16 · StarBlog博客开发笔记 · 约 1 分钟
博客上线
终于上线啦! StarBlog博客项目从春节开始,跨越三个季节终于上线了~ 其实服务器这几天已经部署好了,然后我又测试了一会,暂时觉得OK,终于可以和大家分享了~ 博客地址是:blog.deali.cn 欢迎访问哈~ PS:友链模块即将完成,欢迎各位站长大佬们来交换友链~! 系列文章 顺便回顾一下已更新的12篇文章,接下来要介绍RESTFul接口和管理后台的前端开发了,欢迎继续关注~ 基于.Net
2022-06-20 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(25):图片接口与文件上传
前言 上传文件的接口设计有两种风格,一种是整个项目只设置一个接口用来上传,然后其他需要用到文件的地方,都只存一个引用ID;另一种是每个需要文件的地方单独管理各自的文件。这俩各有优劣吧,本项目中选择的是后者的风格,文章图片和照片模块又要能CRUD又要批量导入,还是各自管理文件比较好。 图片接口 说会正题,先介绍一下图片相关接口。 图片列表 首先CRUD是肯定有的,图片列表的分页查看也是有的,不过因为
2023-02-05 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(21):开始开发RESTFul接口
前言 最近电脑坏了,开源项目的进度也受到一些影响 这篇酝酿很久了,作为本系列第二部分(API接口开发)的第一篇,得想一个好的开头,想着想着就鸽了好久,索性不扯那么多了,直接开写吧~ 关于RESTFul 网上很多相关的文章都要把RESTFul历史来龙去脉给复制一遍,所以我这就不重复了,现在主要的HTTP接口风格就俩:RPC和RESTFul。 举个例子就可以看出这俩的区别 RPC风格 分别是增删改查的
2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟
讨论
共 0 条加载评论中…