Creating An Episerver Editor Documentation View
documentation
episerver
optimizely
In this post, I will be describing how to add a custom editor view in Episerver that will be used to render documentation. Our example will be pulling the documentation from Github but I will discuss how the implementation can be extended to pull documentation from other sources.
Why Add a Documentation View? The obvious answer is to provide the editor with faster access to documentation. I originally created this view for a client that had tens of page and block types, some of which had dozens of properties that had complex logic. The ability to immediately see descriptions of properties as soon as you start editing a page we felt would be extremely valuable.
The not so obvious reason is that it forces developers to add the documentation. When the new page and block types are implemented, a blank page that is visible to the editor is good motivation for the dev to add the documentation.
Usage
You can view the full solution on my Github repo. To add it to your project, put the following projects into your solution and reference them in your web project:
- Documentation.Plugin
- Documentation.Plugin.Core
- Documentation.Plugin.Github
From there, you need to add the view ~/Views/EditorViewDocumentation/Index.cshtml.
Next, add the necessary ServiceConfigurationContext calls:
context.InitializeDocumentationPlugin(new GithubConfigurationOptions()
{
GithubApiToken = ConfigurationManager.AppSettings["GithubApiKey"],
GithubRepositoryName = ConfigurationManager.AppSettings["GithubRepositoryName"],
GithubRepositoryOwner = ConfigurationManager.AppSettings["GithubRepositoryOwner"],
GithubDocumentationFolder = ConfigurationManager.AppSettings["GithubDocumentationFolder"],
GithubBranch = ConfigurationManager.AppSettings["GithubBranch"],
GithubApiUrl = ConfigurationManager.AppSettings["GithubApiUrl"]
});
as well as the listed settings to your appSettings. 'GithubDocumentationFolder' is the repository path your markdown documentation files. In my example, my documentation folder is in the repository root. You can see my settings below.
<!-- Github Api key can be generated by going to
Settings > Developer Settings > Personal Access Tokens in Github -->
<add key="GithubApiKey" value="***************************"/>
<!-- Github repo owner. Can be the user or group owner -->
<add key="GithubRepositoryOwner" value="debpu06"/>
<!-- Github Repository Name -->
<add key="GithubRepositoryName" value="Alloy-Demo-Project"/>
<!-- Relative path to folder that contains documentation. This
example shows that the 'Documentation' folder is in the repository root -->
<add key="GithubDocumentationFolder" value="Documentation" />
<!-- specific branch of the documentation to use -->
<add key="GithubBranch" value="feature-editor-documentation-view" />
<!-- Github api url -->
<add key="GithubApiUrl" value="https://api.github.com" />
Lastly, all you need is to implement the Documentation.Plugin.Interfaces.IDocumented interface on your page types. In my example, I added it to the StartPage. It falls back to StartPage.md but can be set on a per instance basis.
[ContentType(
GUID = "19671657-B684-4D95-A61F-8DD4FE60D559",
GroupName = Global.GroupNames.Specialized)]
[SiteImageUrl]
public class StartPage : SitePageData, IDocumented
{
[Display(
Name = "Documentation Reference",
GroupName = SystemTabNames.Content,
Order = 310)]
[CultureSpecific]
public virtual string DocumentationReference
{
get { return this.GetPropertyValue(page => page.DocumentationReference) ?? "StartPage.md"; }
set { this.SetPropertyValue(page => page.DocumentationReference, value); }
}
You can then populate the property in episerver with the name of the markdown documentation file.
And that is all you need to get the Editor Documentation View working on your Episerver site.
Implementation Details
If you want to implement it yourself or tweak existing features, I will go through how I implemented the documentation view in more detail.
Custom Episerver ViewConfiguration
The first step is to create a subclass of ViewConfiguration and register it as a service with Episerver. I created a subclass called EditorViewConfiguration. In the constructor, you can see I assigned some values to the inherited ViewConfiguration properties. Most of these are self explanatory but keep note of the 'ViewType' property. That is the name of the view that will be rendering the documentation view.
namespace Documentation.Plugin.Infrastructure
{
[ServiceConfiguration(typeof(EPiServer.Shell.ViewConfiguration))]
public class EditorViewConfiguration : ViewConfiguration<PageData>
{
public EditorViewConfiguration()
{
Key = "EditorViewConfiguration";
Name = "Editor Documentation";
Description = "Page Showing Editor Documentation";
ControllerType = "epi-cms/widget/IFrameController";
ViewType = "/EditorViewDocumentation/";
IconClass = "epi-iconForms";
}
}
}
Next, I created an interface called IDocumented that has a single string property called DocumentationReference.
namespace Documentation.Plugin.Interfaces
{
public interface IDocumented
{
string DocumentationReference { get; set; }
}
}
I had the base page 'SitePageData' implement this interface so that all the page types that are subclasses of it will also implement it.
Side Note: I debated whether or not this property should be editable. For the most part documentation of how the individual page types function does not change, just the content. However, for the project I used in the initial implementation, they had some pages that had lots of logic that behaved very differently from campaign to campaign. So we ended up having the property editable to allow for documentation for different page instances.
The next step is to create the model, view, and controller for rendering the editor documentation view in the CMS. The view and ViewModel are fairly simple. The view just outputs the raw data that we will be getting back from our documentation repositories. The 'ViewType' that we referenced above will be where we add our Index.cshtml file.
~/Views/EditorViewDocumentation/Index.cshtml
<!DOCTYPE html>
<html>
<head>
<title>Editor Documenation</title>
@model Documentation.Plugin.Models.EditorViewDocumentationViewModel
@{
Layout = string.Empty;
}
</head>
<body id="documentation">
@Html.Raw(Model.DocumentationContent)
</body>
</html>
The ViewModel just contains a string property for the editor documentation markup.
namespace Documentation.Plugin.Models
{
public class EditorViewDocumentationViewModel
{
public string DocumentationContent { get; set; }
}
}
In the controller, the first thing we do in the Index method is get the reference to the page from the query string. We then use that along with an instance of IContentRepository to get the page instance, which we cast to the IDocumented interface we mentioned above.
//Get the episerver content id from the query string
var epiId = System.Web.HttpContext.Current.Request.QueryString["id"];
//Gets the current content based on the id
IDocumented currentContent = _contentRepository.Get<ContentData>(new ContentReference(epiId)) as IDocumented;
We can then use the IDocumented instance along with our documentation repository (which we will discuss next) to get the markup to be rendered and add it to the ViewModel, which we then return with the view.
//Grab the documentation markup
var result = _documentationRepository.GetDocumentationById(currentContent.DocumentationReference);
if (!string.IsNullOrEmpty(result))
{
model.DocumentationContent = result;
}
else
{
model.DocumentationContent = $"<p>No documentation exists for: {currentContent.DocumentationReference}</p>";
}
The full file can be viewed on Github
Getting the Github Documentation
To get the Github documentation, we can create a custom class GithubDocumentationRepository to handle querying Github for the documentation markdown files.
Side Note: To make things easier, I make a couple assumptions about the structure of the documentation. First, that the documentation will be in a consistent source in Github. I created a folder in the project repository called "Documentation" that will hold the markdown files. Secondly, that each page will have its own markdown file.
GithubDocumentationRepository implements IDocumentationRepository which defines a single method:
string GetDocumentationById(string reference);
GithubDocumentationRepository also subclasses BaseDocumentationRepository. In BaseDocumentationRepository we are doing 2 things. First we are initializing the HttpClient with the correct headers and the url for the Github API
protected void BuildHttpClient(string userName, string token, string apiUrl)
{
_client = new HttpClient();
_client.BaseAddress = new Uri(apiUrl);
_client.DefaultRequestHeaders.Authorization = GetBasicAuthHeader(userName, token);
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_client.DefaultRequestHeaders.Add("User-Agent", "Anything");
}
private AuthenticationHeaderValue GetBasicAuthHeader(string userName, string token)
{
string format = $"{userName}:{token}";
string authString = Convert.ToBase64String(Encoding.ASCII.GetBytes(format));
return new AuthenticationHeaderValue("Basic", authString);
}
We are also making the Get calls to the Github API in this base class.
protected string GetDocumentationData(string requestUrl)
{
using (var httpResponse = _client.GetAsync(requestUrl).Result)
{
if (httpResponse.StatusCode != HttpStatusCode.OK)
return string.Empty;
return httpResponse.Content.ReadAsStringAsync().Result;
}
}
You can grab the whole BaseDocumentationRepository.cs file. Back to our GithubDocumentationRepository, we need to initialize the repository by first calling BuildHttpClient that we created above.
public GithubDocumentationRepository(IConfigurationOptions configurationOptions)
{
_configurationOptions = configurationOptions as GithubConfigurationOptions;
//if for some reason options are not set, throw exception
if (_configurationOptions == null)
{
throw new ArgumentException(typeof(GithubConfigurationOptions).FullName);
}
BuildHttpClient(_configurationOptions.GithubRepositoryOwner, _configurationOptions.GithubApiToken, _configurationOptions.GithubApiUrl);
}
The first thing you will notice is the GithubConfigurationOptions. We will be discussing those next, but for the time being just know they are values added to appSettings to allow us to connect to the Github API.
From there we can implement the GetDocumentationById method we mentioned above. We start by building the Github API request url. We then make the call the GetDocumentationData method defined in the base documentation repository with our request.
var requestUrl =
$"/repos/{_configurationOptions.GithubRepositoryOwner}/" +
$"{_configurationOptions.GithubRepositoryName}" +
$"/contents/{_configurationOptions.GithubDocumentationFolder}/{reference}?ref=" +
$"{_configurationOptions.GithubBranch}";
//Getting json information about the file
var responseData = GetDocumentationData(requestUrl);
Github will not return the contents of the file directly. Instead, it returns information about the file. More information about it can be found inn the Github developer docs.
We then deserialize the response into a custom object called GithubResponse.
//Converting response to object
var deserializedObject = JsonConvert.DeserializeObject<GithubResponse>(responseData);
There is not much to say about this file, other than its modeled directly off of the Github API response for getting content. You can checkout the full implementation of the Github response. The most important part of it is the property DownloadUrl. Using this url, we can get the actual markdown of the documentation file. We can use the same GetDocumentationData method we used to get the initial information about the file.
//Get markdown from url of file
var documentMarkdown = GetDocumentationData(deserializedObject.DownloadUrl) ?? string.Empty;
Now that we have the actual markdown, we just need to convert it to html so that can be rendered when we pass it to the view. For this I used a third party plugin called Markdig. The completed method looks like this:
public string GetDocumentationById(string reference)
{
var requestUrl =
$"/repos/{_configurationOptions.GithubRepositoryOwner}/" +
$"{_configurationOptions.GithubRepositoryName}" +
$"/contents/{_configurationOptions.GithubDocumentationFolder}/{reference}?ref=" +
$"{_configurationOptions.GithubBranch}";
//Getting json information about the file
var responseData = GetDocumentationData(requestUrl);
//if information on file is not found return markup message
if (string.IsNullOrEmpty(responseData))
{
//handling will show no documentation message when response is empty string
return string.Empty;
}
//Converting response to object
var deserializedObject = JsonConvert.DeserializeObject<GithubResponse>(responseData);
//Get markdown from url of file
var documentMarkdown = GetDocumentationData(deserializedObject.DownloadUrl) ?? string.Empty;
//return markdown as html
return Markdig.Markdown.ToHtml(documentMarkdown);
}
Bringing It All Together
In order register our GithubDocumentationRepository, I created an extension method for ServiceConfigurationContext for registering GithubDocumentationRepository as an IDocumentationRepository singleton.
public static class ServiceConfigurationContextExtensions
{
public static void InitializeDocumentationPlugin(this ServiceConfigurationContext context, IConfigurationOptions options)
{
var githubOptions = options as GithubConfigurationOptions;
if (githubOptions != null)
{
context.Services.AddSingleton<IDocumentationRepository, GithubDocumentationRepository>();
context.Services.AddSingleton<IConfigurationOptions, GithubConfigurationOptions>(locator => githubOptions);
return;
}
}
}
As you probably noticed, GithubConfigurationOptions popped up again. They are passed into the InitializeDocumentationPlugin method and are used when creating a singleton of type IConfigurationOptions.
namespace Documentation.Plugin.Github.Models
{
public class GithubConfigurationOptions : IConfigurationOptions
{
public string GithubRepositoryOwner { get; set; }
public string GithubRepositoryName { get; set; }
public string GithubApiToken { get; set; }
public string GithubApiUrl { get; set; }
public string GithubDocumentationFolder { get; set; }
public string GithubBranch { get; set; }
}
}
As you can see, this contains all the information about connecting to the Github API. The IConfigurationOptions interface that it implements actually contains no methods or properties. Its purpose is to allow us to create other implementations of configuration options and register them as singletons. We will discuss this a bit more below when we talk about expanding on this project.
The last step is to initialize everything. You can add the following to your Initialization class in your web project:
context.InitializeDocumentationPlugin(new GithubConfigurationOptions()
{
GithubApiToken = ConfigurationManager.AppSettings["GithubApiKey"],
GithubRepositoryName = ConfigurationManager.AppSettings["GithubRepositoryName"],
GithubRepositoryOwner = ConfigurationManager.AppSettings["GithubRepositoryOwner"],
GithubDocumentationFolder = ConfigurationManager.AppSettings["GithubDocumentationFolder"],
GithubBranch = ConfigurationManager.AppSettings["GithubBranch"],
GithubApiUrl = ConfigurationManager.AppSettings["GithubApiUrl"]
});
It will call the ServiceConfigurationContext extension and pass along all the Github API properties that it pulls from the app settings.
You can view the full implementation on an Episerver Alloy demo site.
Next Steps
If you want to add this implementation to your solution, you can follow the steps in the Usage section above.
Custom Styling
For devs, viewing rendered markdown is pretty common. During initial implementation I did not see a need to add any custom styles to the final documentation view. However, if you have dedicated content editors who don't spend as much time as you do going over READMEs, custom styling may be a good idea.
Custom Documentation Source
If you want to customize the solution to work for a different documentation source, you can do so by creating your own implementation of IDocumentationRepository and IConfigurationOptions, then register those instead of calling InitializeDocumentationPlugin. I will be following up with an implementation using Confluence as a documentation source.
If you have any recommentations, feel free to create a pull request or send me a message on twitter