Appearance
Are you an LLM? You can read better optimized documentation at /features/editorial_features.md for this page in Markdown format
Editorial features
This document provides a technical overview of the editorial features integrated into TheExampleApp. These features are designed to bridge the gap between the live web application and the Contentful web app, providing a streamlined workflow for content editors. They allow editors to easily navigate from a piece of content in the application directly to its corresponding entry in the Contentful editing interface and to visualize the current state of content (e.g., draft, pending changes).
These features are primarily implemented through a combination of ASP.NET Core Middleware and View Components, which are conditionally rendered based on session state.
Content editing integration
The core purpose of the editorial features is to enhance the content management lifecycle for editors. In a headless architecture, it can be challenging for non-technical users to locate the source of a specific piece of content displayed on the website. This integration solves that problem by providing:
- Direct Edit Links: Overlays or buttons that appear next to content elements, providing a deep link directly to the entry or asset editor in the Contentful web app.
- Content State Visibility: Visual indicators that show whether a piece of content is a draft, has been published, or has unpublished changes. This is crucial when using the Preview API to review changes before they go live.
These features are disabled by default and must be explicitly activated for a user's session.
Enabling editorial features
The editorial features are controlled on a per-session basis. Activation is managed by the Deeplinker middleware, which inspects the query string of incoming requests.
To enable the features, a user must visit any page in the application with the following query parameter: ?editorial_features=enabled
The Deeplinker middleware, registered in Startup.cs, detects this parameter and sets a value in the user's session.
csharp
// File: TheExampleApp/Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...
app.UseSession();
// ...
app.UseDeeplinks(); // Middleware is registered here
app.UseMvc();
// ...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
The middleware's logic specifically looks for the editorial_features key. If the value is enabled (case-insensitive), it sets the session string "EditorialFeatures" to "Enabled". Any other value for the parameter will explicitly set the session string to "Disabled", effectively turning the features off.
csharp
// File: TheExampleApp/Configuration/Deeplinker.cs
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
// ... other deeplinking logic ...
if (query.ContainsKey("editorial_features") && string.Equals(query["editorial_features"], "enabled", StringComparison.InvariantCultureIgnoreCase))
{
// Sets the session variable to enable features
context.Session.SetString("EditorialFeatures", "Enabled");
}
else if(query.ContainsKey("editorial_features"))
{
// Any other value disables the features for the session
context.Session.SetString("EditorialFeatures", "Disabled");
}
await _next(context);
}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
Once set, this session value persists, and all subsequent requests within that session will have the editorial features active until they are explicitly disabled. Components then check this session value to determine whether to render the UI elements.
Edit buttons
The "Edit" buttons, which provide deep links to the Contentful web app, are rendered by the EditorialFeaturesViewComponent. This is a standard ASP.NET Core View Component designed to be reusable across different views and layouts.
The component is invoked from a Razor view, passing in the SystemProperties of the Contentful entries being rendered on the page.
Implementation
The EditorialFeaturesViewComponent reads the session state to check if features are enabled. If they are, it passes the necessary data to its corresponding view (Default.cshtml) to construct the edit links.
The required data includes:
- The
SystemPropertiesof the entries, which contains theIdfor each entry. - The current Contentful
SpaceId.
The view then uses this information to generate a URL with the format: https://app.contentful.com/spaces/{space_id}/entries/{entry_id}.
csharp
// File: TheExampleApp/ViewComponents/EditorialFeaturesViewComponent.cs
namespace TheExampleApp.ViewComponents
{
/// <summary>
/// View component handling whether or not to display editorial features.
/// </summary>
public class EditorialFeaturesViewComponent : ViewComponent
{
private readonly ContentfulOptions _options;
public EditorialFeaturesViewComponent(IContentfulOptionsManager optionsManager)
{
_options = optionsManager.Options;
}
/// <summary>
/// Invokes the view component and returns the result.
/// </summary>
/// <param name="sys">The system properties of the entry to display editorial features for.</param>
public IViewComponentResult Invoke(IEnumerable<SystemProperties> sys)
{
var model = new EditorialFeaturesModel();
model.Sys = sys;
// Checks the session to see if features should be rendered.
model.FeaturesEnabled = HttpContext.Session.GetString("EditorialFeatures") == "Enabled";
model.SpaceId = _options.SpaceId;
model.UsePreviewApi = _options.UsePreviewApi;
return View(model);
}
}
/// <summary>
/// Model for the EditorialFeaturesViewComponent view.
/// </summary>
public class EditorialFeaturesModel
{
public IEnumerable<SystemProperties> Sys { get; set; }
public bool FeaturesEnabled { get; set; }
public bool UsePreviewApi { get; set; }
public string SpaceId { get; set; }
}
}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
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
This component can be invoked from any Razor page where you want to display edit buttons for a piece of displayed content. For example:
csharp
// Example usage in a Razor view
@await Component.InvokeAsync("EditorialFeatures", new { sys = new[] { Model.Sys } })1
2
2
Content state indicators
To give editors immediate feedback on the status of their content, the EntryStateViewComponent provides visual indicators for "Draft" and "Pending Changes" states. This feature is only relevant when using the Contentful Preview API, as the Delivery API only ever returns published content.
Implementation
This component uses a clever technique to determine the content state by comparing data from both the Contentful Preview and Delivery APIs.
Initial State: The component is invoked with the
SystemPropertiesof an entry (or entries) fetched from the Preview API. This data represents the "latest" version of the content, which may or may not be published.Delivery API Call: Inside the component, a new
ContentfulClientis instantiated, hard-coded to not use the preview API (usePreview: false). This forces it to query the Delivery API.State Determination:
- Draft: The component attempts to fetch the same entry/entries by ID from the Delivery API. If the API call fails, returns
null, or does not return all of the requested entries, it means at least one of them is not published. In this case, theDraftflag is set totrue. - Pending Changes: If the entries are successfully fetched from the Delivery API, the component compares the
UpdatedAttimestamp of the preview version with theUpdatedAttimestamp of the published version. If the timestamps do not match, it signifies that the entry has been published but has subsequent unpublished changes. ThePendingChangesflag is set totrue.
- Draft: The component attempts to fetch the same entry/entries by ID from the Delivery API. If the API call fails, returns
A helper function, TrimMilliseconds, is used during the timestamp comparison to prevent false positives caused by precision differences in DateTime objects.
csharp
// File: TheExampleApp/ViewComponents/EntryStateViewComponent.cs
public class EntryStateViewComponent : ViewComponent
{
private readonly HttpClient _httpClient;
private readonly ContentfulOptions _options;
public EntryStateViewComponent(HttpClient httpClient, IContentfulOptionsManager optionsManager)
{
_httpClient = httpClient;
_options = optionsManager.Options;
}
public async Task<IViewComponentResult> InvokeAsync(IEnumerable<SystemProperties> sys)
{
// Create a new client with preview set to false to always get the entry from the delivery API.
var client = new ContentfulClient(_httpClient, _options.DeliveryApiKey, _options.PreviewApiKey, _options.SpaceId, false);
IEnumerable<EntryStateModel> entries = null;
var model = new EntryStateModel();
try
{
// Try getting the entry by the specified Id from the Delivery API.
entries = await client.GetEntries<EntryStateModel>($"?sys.id[in]={string.Join(",", sys.Select(c => c.Id))}");
}
catch { /* A thrown exception indicates the entry is not published. */ }
if(entries == null || (entries.Any() == false || sys.Select(c => c.Id).Except(entries.Select(e => e.Sys.Id)).Any()))
{
// One of the entries is not published, thus it is in draft mode.
model.Draft = true;
}
if (entries != null && AnySystemPropertiesNotMatching(entries.Select(c => c.Sys), sys))
{
// The entry is published but the UpdatedAt dates do not match, thus it must have pending changes.
model.PendingChanges = true;
}
return View(model);
// ... helper methods for comparison ...
}
}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
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
Deeplinker implementation
The Deeplinker is an ASP.NET Core middleware class that plays a central role in integrating the application with the Contentful UI. While its name might suggest it creates deep links, its primary function is to interpret incoming requests that originate from deep links within the Contentful web app (e.g., "Open preview" buttons).
It inspects query string parameters to configure the application's state for the current user session.
Key Functionalities
Runtime Configuration Switching: When a user clicks a link in Contentful to preview content, Contentful can be configured to pass the space ID and API tokens in the query string. The
Deeplinkermiddleware detects these parameters (space_id,preview_token,delivery_token) and dynamically creates a newContentfulOptionsobject for the user's session. This allows a single application instance to preview content from multiple Contentful spaces without a code change or restart. See Contentful Setup for more on configuration.API Toggling: It handles the
?api=cpaquery parameter to switch the session to use the Contentful Preview API, which is essential for viewing drafts and unpublished changes. This is a core part of the application's Preview Mode.Editorial Feature Toggling: As detailed earlier, it processes the
?editorial_features=enabledparameter to activate the editor-facing UI helpers for the user's session.
csharp
// File: TheExampleApp/Configuration/Deeplinker.cs
public class Deeplinker
{
private readonly RequestDelegate _next;
private readonly IContentfulOptionsManager _manager;
public Deeplinker(RequestDelegate next, IContentfulOptionsManager manager)
{
_next = next;
_manager = manager;
}
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
// Handles full context switching for space/tokens
if (query.ContainsKey("space_id") && query.ContainsKey("preview_token") && query.ContainsKey("delivery_token"))
{
var currentOptions = new ContentfulOptions();
currentOptions.DeliveryApiKey = query["delivery_token"];
currentOptions.SpaceId = query["space_id"];
currentOptions.PreviewApiKey = query["preview_token"];
currentOptions.UsePreviewApi = query.ContainsKey("api") && query["api"] == "cpa";
// ... validation and session storage ...
context.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(currentOptions));
}
// Handles toggling between Preview (cpa) and Delivery (cda) APIs
else if (query.ContainsKey("api"))
{
var currentOptions = new ContentfulOptions { UsePreviewApi = query["api"] == "cpa", /* ... copy other options */ };
context.Session.SetString(nameof(ContentfulOptions), JsonConvert.SerializeObject(currentOptions));
}
// Handles enabling/disabling editorial features
if (query.ContainsKey("editorial_features") && string.Equals(query["editorial_features"], "enabled", StringComparison.InvariantCultureIgnoreCase))
{
context.Session.SetString("EditorialFeatures", "Enabled");
}
else if(query.ContainsKey("editorial_features"))
{
context.Session.SetString("EditorialFeatures", "Disabled");
}
await _next(context);
}
}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
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
This middleware is a powerful mechanism that makes the application highly adaptable to the editor's context, creating a seamless bridge between the CMS and the presentation layer.