Appearance
Are you an LLM? You can read better optimized documentation at /features/content_display.md for this page in Markdown format
Content display
This document provides a technical overview of how content is fetched from the Contentful headless CMS, processed by the backend, and rendered in the user-facing views. It covers the data fetching patterns, dynamic rendering of polymorphic content, and handling of specific asset types like images and Markdown.
Rendering Contentful content
The application follows a server-side rendering (SSR) pattern where content is fetched from Contentful during the request lifecycle and rendered into HTML on the server. This process is orchestrated primarily within ASP.NET Core Razor Pages.
The general workflow is as follows:
- A user request hits a Razor Page (e.g.,
Index.cshtmlorCourses/Lessons.cshtml). - The page's
OnGethandler in the corresponding.cshtml.csfile is executed. - Inside
OnGet, theContentful.Core.Search.QueryBuilderis used to construct a strongly-typed query for the required content entry (e.g., aLayoutor aCourse). - The
IContentfulClientsends the query to the Contentful Delivery API. - The API response (JSON) is deserialized by the Contentful SDK into C# model objects. This includes resolving linked entries and polymorphic content modules. See Core Application Models for more on the C# models.
- The populated model is passed to the Razor view (
.cshtml). - The Razor view iterates through the content and uses standard HTML, partial views, and custom Tag Helpers to render the final HTML response.
This architecture centralizes data-fetching logic in the page models and keeps the views focused on presentation. For more details on the initial setup, see Contentful Integration.
Content fetching
Content fetching is managed by the Contentful.aspnetcore SDK, which provides a fluent QueryBuilder API to construct requests to the Contentful API.
QueryBuilder Patterns
Queries are built using a chainable, fluent syntax that starts with QueryBuilder<T>.New, where T is the target C# model corresponding to a Contentful content type.
Key methods used in the application include:
.ContentTypeIs("content-type-id"): Specifies the Contentful content type to query..FieldEquals(f => f.Slug, "value"): Filters entries based on a specific field value. This is the primary method for retrieving specific pages or items by their URL slug..Include(N): Specifies the link resolution depth for nested content..LocaleIs("locale-code"): Fetches content for a specific language.
Example: Fetching the Home Page Layout The following snippet from Index.cshtml.cs demonstrates fetching a Layout entry where the slug field is "home".
csharp
// File: TheExampleApp/Pages/Index.cshtml.cs
public async Task OnGet()
{
// 1. Start a new query for the 'Layout' content type.
var queryBuilder = QueryBuilder<Layout>.New
// 2. Filter to the entry with the content type ID "layout".
.ContentTypeIs("layout")
// 3. Find the specific entry where the 'slug' field is "home".
.FieldEquals(f => f.Slug, "home")
// 4. Include linked entries up to 4 levels deep.
.Include(4)
// 5. Specify the locale for the content.
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
// Execute the query and get the first result.
var indexPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
IndexPage = indexPage;
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Include Depth for Nested Content
The .Include(N) method is critical for performance and for resolving complex content structures in a single API call. Contentful entries often link to other entries (e.g., a Layout links to its ContentModules, a Course links to its Lessons).
- Purpose: The
includeparameter tells the Contentful API to embed the content of linked entries in the JSON response. This prevents the "N+1" problem, where the application would have to make one initial query and then N subsequent queries to fetch each linked item. - Usage: The depth
Ndetermines how many levels of links are resolved.- In
Index.cshtml.cs,Include(4)is used, which is deep enough to resolve aLayout->ContentModule->HighlightedCourse->Authorstructure. - In
Lessons.cshtml.cs,Include(5)is used to accommodate potentially deeper nesting within course content.
- In
Choosing the correct include level is a balance. Too low, and you will get null for nested content that wasn't resolved. Too high, and you increase the size of the API response unnecessarily. The current values have been chosen to match the maximum depth of the content models.
Locale-Specific Queries
The application supports multiple languages by using the .LocaleIs() method in every query. This ensures that all fetched content corresponds to the user's selected language. For more information on how locales are managed, see the Localization documentation.
The locale is determined using the following priority:
- From the user's session:
HttpContext?.Session?.GetString(Startup.LOCALE_KEY) - As a fallback, from the server's current culture:
CultureInfo.CurrentCulture.ToString()
Example: Handling Not-Found Content with Localization When fetching a course and lesson in Lessons.cshtml.cs, the application performs two-stage validation. If either the course or the specific lesson is not found for the given slug and locale, the application returns a 404 Not Found result with an appropriate localized error message.
csharp
// File: TheExampleApp/Pages/Courses/Lessons.cshtml.cs
public async Task<IActionResult> OnGet(string slug, string lessonSlug)
{
var queryBuilder = QueryBuilder<Course>.New
.ContentTypeIs("course")
.FieldEquals(f => f.Slug, slug?.ToLower())
.Include(5)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
Course = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
if (Course == null)
{
// If the course is not found, store a localized error message in TempData
// and return a standard 404 response.
TempData["NotFound"] = _localizer["errorMessage404Course"].Value;
return NotFound();
}
SelectedLesson = Course.Lessons.FirstOrDefault(c => c.Slug == lessonSlug?.ToLower());
if (SelectedLesson == null)
{
// If the lesson is not found within the course, also return a 404
// with a lesson-specific error message.
TempData["NotFound"] = _localizer["errorMessage404Lesson"].Value;
return NotFound();
}
// ...
}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
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
Dynamic Module Rendering
A key architectural pattern in the application is the use of polymorphic "module" content. A single field in Contentful (e.g., the contentModules field on a Layout) is a reference field that can link to entries of different content types (e.g., a Hero Image, a Copy Block, etc.). The backend must be able to deserialize and render these different types dynamically.
ModulesResolver for Polymorphic Content
The Contentful SDK needs to know which C# class to use when it encounters a linked entry of a specific content type. This is achieved by implementing the IContentTypeResolver interface.
The ModulesResolver class contains a dictionary that maps a Contentful contentTypeId (a string) to a C# Type.
csharp
// File: TheExampleApp/Configuration/ModulesResolver.cs
/// <summary>
/// Resolves a strong type from a content type id. Instructing the serialization engine how to deserialize items in a collection.
/// </summary>
public class ModulesResolver : IContentTypeResolver
{
private Dictionary<string, Type> _types = new Dictionary<string, Type>()
{
{ "layoutCopy", typeof(LayoutCopy) },
{ "layoutHeroImage", typeof(LayoutHeroImage) },
{ "layoutHighlightedCourse", typeof(LayoutHighlightedCourse) },
{ "lessonCodeSnippets", typeof(LessonCodeSnippets) },
{ "lessonCopy", typeof(LessonCopy) },
{ "lessonImage", typeof(LessonImage) },
};
/// <summary>
/// Method to get a type based on the specified content type id.
/// </summary>
public Type Resolve(string contentTypeId)
{
return _types.TryGetValue(contentTypeId, out var type) ? type : null;
}
}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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
This resolver is registered with the ContentfulClient at application startup. When the SDK deserializes a collection of linked entries, it calls Resolve() for each item, passing the contentTypeId from the item's metadata. The resolver returns the corresponding C# type, which the SDK then uses to instantiate and populate the correct object.
Rendering Different Module Types
Once the page model is populated with a list of modules (e.g., IndexPage.ContentModules), the Razor view is responsible for rendering them. While the main layout view is not provided, the standard approach is to iterate through the list of modules and use a switch on the object's type to render a specific partial view for each module.
This pattern allows the view to dynamically adapt to the content structure defined in Contentful. For more details on the view structure, see Views and Partial Views.
Conceptual Example:
csharp
// This is a conceptual example of how modules would be rendered in a Razor view.
@foreach (var module in Model.IndexPage.ContentModules)
{
switch (module)
{
case LayoutCopy copy:
<partial name="_LayoutCopy" model="copy" />
break;
case LayoutHeroImage hero:
<partial name="_LayoutHeroImage" model="hero" />
break;
// ... other cases
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Layout vs. Lesson Modules
The ModulesResolver reveals a clear separation between modules intended for general layouts and those specific to lessons. This is a deliberate design choice to enforce content strategy and reusability.
| Module Category | Content Type ID | C# Type | Purpose |
|---|---|---|---|
| Layout | layoutCopy | LayoutCopy | A block of rich text for a layout. |
| Layout | layoutHeroImage | LayoutHeroImage | A large hero image for a page. |
| Layout | layoutHighlightedCourse | LayoutHighlightedCourse | A card highlighting a specific course. |
| Lesson | lessonCodeSnippets | LessonCodeSnippets | A formatted block of code. |
| Lesson | lessonCopy | LessonCopy | The main text content of a lesson. |
| Lesson | lessonImage | LessonImage | An image embedded within a lesson. |
This separation ensures that content editors can only use appropriate modules in the correct context (e.g., you cannot add a lessonCodeSnippets module directly to the home page layout).
Asset Handling
The application handles different types of assets from Contentful, including images and Markdown text.
Images from Contentful
Image fields in Contentful are deserialized by the SDK into Contentful.Core.Models.Asset objects. These objects contain all the necessary metadata for rendering an image, including its URL, title, and description (which should be used for alt text).
While no direct image rendering is shown in the provided files, a module like LayoutHeroImage would contain an Asset property. The corresponding partial view would render it as follows:
Hypothetical Razor Example:
csharp
@model LayoutHeroImage
<div class="hero">
@if (Model.HeroImage?.File != null)
{
<img src="@Model.HeroImage.File.Url" alt="@Model.HeroImage.Description" />
}
<h1>@Model.Title</h1>
</div>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Note: Always check for
nullon theAssetand itsFileproperty before attempting to accessUrl. An image field in Contentful may not be mandatory.
Markdown Content Processing
Content authors write rich text in Contentful using Markdown. The application renders this Markdown as HTML using a custom ASP.NET Core Tag Helper.
Markdown Tag Helper The MarkdownTagHelper provides a declarative way to convert a string of Markdown into HTML directly in a Razor view. It uses the Markdig library for the conversion.
The tag helper can be used in two ways:
- As a
<markdown>element with acontentattribute pointing to a model property - As a
markdownattribute on any HTML element, with Markdown content inside the element
csharp
// File: TheExampleApp/TagHelpers/MarkdownTagHelper.cs
[HtmlTargetElement("markdown")]
[HtmlTargetElement(Attributes = "markdown")]
public class MarkdownTagHelper : TagHelper
{
public ModelExpression Content { get; set; }
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// Remove the <markdown> tag itself, leaving only the rendered HTML
if (output.TagName == "markdown")
{
output.TagName = null;
}
output.Attributes.RemoveAll("markdown");
var content = await GetContent(output);
var markdown = content;
// Use Markdig to convert Markdown string to HTML.
var html = Markdown.ToHtml(markdown ?? "");
output.Content.SetHtmlContent(html ?? "");
}
private async Task<string> GetContent(TagHelperOutput output)
{
// If Content property is not set, use the tag's child content
if (Content == null)
return (await output.GetChildContentAsync()).GetContent();
// Otherwise, use the value from the Content model expression
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
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
Usage in a View The tag helper is used in views like LessonCopy.cshtml to render the Copy property of a LessonCopy module. This abstracts the conversion logic away from the view, keeping it clean and focused on structure.
csharp
// File: TheExampleApp/Views/Shared/LessonCopy.cshtml
@model LessonCopy
<div class="lesson-module lesson-module-copy">
<div class="lesson-module-copy__copy">
<markdown content="@Model.Copy"></markdown>
</div>
</div>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
This approach is highly efficient and maintains a strong separation of concerns, making it easy to manage how Markdown is processed across the entire application.