Appearance
Are you an LLM? You can read better optimized documentation at /features/markdown_rendering.md for this page in Markdown format
Markdown rendering
Overview
This document provides a technical deep-dive into the process of rendering Markdown content within the application. The primary source of this content is the Contentful headless CMS. The conversion from Markdown to HTML is handled by a custom ASP.NET Core Tag Helper, MarkdownTagHelper, which leverages the Markdig library. This mechanism allows content editors to use Markdown for rich text formatting in Contentful, which is then safely and consistently rendered in the application's views.
Markdig library
The core of the Markdown-to-HTML conversion is powered by the Markdig library. As specified in TheExampleApp.csproj, the project uses version 0.15.4.
xml
<!-- TheExampleApp.csproj -->
<PackageReference Include="Markdig">
<Version>0.15.4</Version>
</PackageReference>1
2
3
4
2
3
4
Markdig is a fast, powerful, and extensible Markdown processor for .NET. In our application, its role is straightforward: to take a string of Markdown text and convert it into its HTML equivalent. The MarkdownTagHelper utilizes the static Markdown.ToHtml() method for this purpose.
csharp
// From MarkdownTagHelper.cs
var html = Markdown.ToHtml(markdown ?? "");
output.Content.SetHtmlContent(html ?? "");1
2
3
2
3
The current implementation uses the default Markdig pipeline, which is highly compliant with the CommonMark specification. No custom extensions or advanced pipelines are configured at this time.
MarkdownTagHelper
The MarkdownTagHelper is a custom server-side component that encapsulates the logic for rendering Markdown. It provides a declarative way to handle Markdown conversion directly within Razor views.
See also: /components/tag_helpers.md
Implementation Details
The helper is defined in TheExampleApp/TagHelpers/MarkdownTagHelper.cs.
csharp
// TheExampleApp/TagHelpers/MarkdownTagHelper.cs
using Markdig;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
// ...
namespace TheExampleApp.TagHelpers
{
/// <summary>
/// Taghelper for turning markdown into html.
/// </summary>
[HtmlTargetElement("markdown")]
[HtmlTargetElement(Attributes = "markdown")]
public class MarkdownTagHelper : TagHelper
{
/// <summary>
/// The model expression for which property to convert.
/// </summary>
public ModelExpression Content { get; set; }
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// If the tag is <markdown>, remove the tag itself from the output.
if (output.TagName == "markdown")
{
output.TagName = null;
}
// If used as an attribute (e.g., <div markdown>), remove the attribute.
output.Attributes.RemoveAll("markdown");
// Get the raw markdown string from either the model or inline content.
var content = await GetContent(output);
var markdown = content;
// Convert markdown to HTML using Markdig.
var html = Markdown.ToHtml(markdown ?? "");
// Set the output's content to the rendered HTML.
output.Content.SetHtmlContent(html ?? "");
}
private async Task<string> GetContent(TagHelperOutput output)
{
// If the 'content' attribute is NOT bound, use the tag's inner content.
if (Content == null)
return (await output.GetChildContentAsync()).GetContent();
// If the 'content' attribute IS bound, use the value from the model.
return Content.Model?.ToString();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Key Features:
- Dual-Targeting: The
[HtmlTargetElement]attributes allow the helper to be invoked in two ways:- As a custom element:
<markdown>...</markdown> - As an attribute on any standard HTML element:
<div markdown>...</div>
- As a custom element:
- Clean Output: The helper removes its own tag (
<markdown>) or attribute (markdown) from the final rendered HTML, replacing it entirely with the converted content. - Flexible Content Source: The
GetContentmethod intelligently determines whether to use Markdown provided from a bound model property or Markdown written directly inside the tag in the view.
Usage in views
The MarkdownTagHelper can be used in any Razor view (.cshtml file). The most common pattern in this application is to bind it to a model property containing Markdown from Contentful.
Example 1: Binding to a Model Property
This is the primary use case. The content attribute of the tag helper is bound to a property on the view's model.
html
<!-- TheExampleApp/Views/Shared/LessonCopy.cshtml -->
@model LessonCopy
<div class="lesson-module lesson-module-copy">
<div class="lesson-module-copy__copy">
<!-- The 'content' attribute is bound to the Model.Copy property -->
<markdown content="@Model.Copy"></markdown>
</div>
</div>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Example 2: Using Inline Markdown
While less common for dynamic content, the helper can also process static Markdown written directly between its tags. This is useful for content that is part of the view itself rather than the data model.
html
<!-- Hypothetical Example -->
<div>
<markdown>
# This is a title
This is a paragraph with some **bold** and *italic* text.
- List item 1
- List item 2
</markdown>
</div>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
In this case, the Content property of the tag helper will be null, and the GetContent method will fall back to reading the child content of the tag.
Model binding
The connection between the data from Contentful and the MarkdownTagHelper is achieved through standard ASP.NET Core model binding in Razor.
See also: /models/lesson_modules.md
A model, such as
LessonCopy, defines a string property to hold the Markdown text.csharp// TheExampleApp/Models/Lesson - Copy.cs namespace TheExampleApp.Models { public class LessonCopy : ILessonModule { // ... other properties /// <summary> /// The body copy of the module. /// </summary> public string Copy { get; set; } } }1
2
3
4
5
6
7
8
9
10
11
12In the Razor view, this model is declared with
@model, and itsCopyproperty is passed to the tag helper'scontentattribute.html<!-- TheExampleApp/Views/Shared/LayoutCopy.cshtml --> @model LayoutCopy ... <div class="@("module-copy__copy" + visualStyle)"><markdown content="@Model.Copy" /></div>1
2
3
4
The Razor engine maps the content attribute to the Content property on the MarkdownTagHelper. The property's type, ModelExpression, is a special framework type that encapsulates both the value (Model.Copy) and metadata about the expression, allowing the tag helper to robustly access the model's data.
Inline vs. model content
The MarkdownTagHelper supports two distinct modes for sourcing its Markdown content, providing flexibility for different scenarios.
| Mode | Usage | How it Works | Primary Use Case |
|---|---|---|---|
| Model-Bound | <markdown content="@Model.Copy" /> | The Content property of the tag helper is populated. The GetContent method returns Content.Model.ToString(). | Rendering dynamic content fetched from the CMS (e.g., Contentful). This is the standard approach in the application. |
| Inline | <markdown># My Title</markdown> | The Content property is null. The GetContent method calls output.GetChildContentAsync() to read the content nested within the tag. | Rendering static, view-specific content that is not managed externally. Useful for UI mockups or simple, hard-coded rich text. |
Security considerations
CRITICAL: The current implementation of MarkdownTagHelper is vulnerable to Cross-Site Scripting (XSS) attacks.
The Markdig.Markdown.ToHtml() method, by default, does not sanitize its input. If a piece of Markdown content from Contentful contains malicious HTML, such as a <script> tag, it will be rendered directly into the page and executed by the user's browser.
Example Vulnerability:
If a content editor enters the following into a Contentful field:
markdown
This is some text.
<script>alert('XSS Attack!');</script>1
2
2
The MarkdownTagHelper will output the following HTML, causing the script to execute for every user viewing the page:
html
<p>This is some text.</p>
<script>alert('XSS Attack!');</script>1
2
2
Recommended Mitigation
To fix this vulnerability, the rendered HTML must be sanitized after the Markdown-to-HTML conversion. A dedicated HTML sanitization library should be integrated into the MarkdownTagHelper.
A recommended approach is to use a library like HtmlSanitizer.
Proposed Code Change:
- Add the
HtmlSanitizerNuGet package to the project. - Update the
ProcessAsyncmethod inMarkdownTagHelper.cs:
csharp
// Proposed change in MarkdownTagHelper.cs
using Markdig;
using Ganss.XSS; // Add this using statement
// ...
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// ... (existing code to get markdown content) ...
var markdown = await GetContent(output);
// 1. Convert Markdown to HTML
var rawHtml = Markdown.ToHtml(markdown ?? "");
// 2. Sanitize the resulting HTML
var sanitizer = new HtmlSanitizer();
var sanitizedHtml = sanitizer.Sanitize(rawHtml);
// 3. Set the output to the sanitized HTML
output.Content.SetHtmlContent(sanitizedHtml ?? "");
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
This change ensures that any dangerous HTML tags and attributes are removed before the content is rendered to the client, effectively mitigating the XSS risk. This is a high-priority change that should be implemented immediately.
Rendering pipeline
The end-to-end flow of content from Contentful to the user's browser follows these steps:
- Content Authoring: An editor creates or updates an entry in Contentful (e.g., a "Lesson Copy" module) and writes content in a Markdown-enabled text field.
- Data Fetching: A user requests a page. The ASP.NET Core application uses the
contentful.aspnetcoreSDK to query the Contentful Delivery API for the required content entry. - Model Deserialization: The JSON response from Contentful is deserialized into a strongly-typed C# model instance (e.g.,
LessonCopy). The Markdown text is now held in a string property likeLessonCopy.Copy. - View Rendering: The controller passes the populated model to the appropriate Razor view (e.g.,
LessonCopy.cshtml). - Tag Helper Execution: The Razor view engine encounters the
<markdown>tag and invokes theMarkdownTagHelper. It binds theModel.Copyproperty to the helper'sContentproperty. - Markdown Conversion: The
MarkdownTagHelper.ProcessAsyncmethod executes. It retrieves the Markdown string from itsContentproperty and passes it toMarkdig.Markdown.ToHtml(). - HTML Output: The tag helper replaces its own tag in the Razor output stream with the resulting (and hopefully sanitized) HTML string.
- HTTP Response: The fully rendered HTML page is assembled and sent to the user's browser.