Module to retrieve content from Kentico Kontent for building static websites with Statiq.
Works with .NET 8.
To get started, you can follow the tutorial below or check out the starter site. It'll give you a nice impression of what you can do with Kontent and Statiq.
This is a quick guide to get some content from Kontent and render it to HTML. You should be familiar with ASP.NET Core and it helps if you're a bit familiar with Kontent and Statiq Framework.
- Setup a project on Kontent, there's a free Developer Plan. This document assumes you're using the demo project.
- Get the Project ID of your project, you'll need it later
- Follow the getting started steps for Statiq.Framework, then come back here to set up a pipeline
- Add
Kontent.Statiq
to the project:
dotnet add package Kontent.Statiq --version 2.0.0-*
- Install the Kontent generator tool to generate strong-typed models for your project
- Generate the models for your project into the Models folder:
KontentModelGenerator --projectid "<your-project-id>" --outputdir Models --namespace My.Models -s true -g true
The KontentModelGenerator tool generates a C# class for each of the content types in your Kontent project as well as a TypeProvider
. The type provider maps content types returned by the Kontent Delivery API to the generated C# classes. It also provides all the code names for the properties on your content models so you can more easily write queries for the delivery SDK.
The KomtentModelGenerator makes it easy to keep your code in sync with your Kontent project. You'll need to re-run it whenever you make changes to the content model in Kontent.
- Configure the Delivery client for your Kontent project
In program.cs
, add in the service configurations needed to register the Delivery client and the custom type provider:
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Ai.Delivery.Extensions;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using Statiq.App;
using Statiq.Common;
using My.Models; // <== your model namespace
public static class Program
{
public static async Task<int> Main(string[] args) =>
await Bootstrapper
.Factory
.CreateDefault(args)
.ConfigureServices((services, config) =>
{
// Add the type provider
services.AddSingleton<ITypeProvider, CustomTypeProvider>();
// Configure Delivery SDK
services.AddDeliveryClient(opts =>
opts.WithEnvironmentId("<your-environment-id>")
.UseProductionApi()
.Build());
})
.RunAsync();
}
- Add a pipeline to your project. The pipeline will automatically detected and executed by Statiq.
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Statiq;
using Statiq.Common;
using Statiq.Core;
using My.Models; // <== your model namespace
namespace My // <== your namespace
{
public class ArticlesPipeline : Pipeline
{
public ArticlesPipeline(IDeliveryClient client)
{
InputModules = new ModuleList{
// Load all articles from Kontent,
new Kontent<Article>(client)
// Use the BodyContent field as the document content
.WithContent(page => page.BodyContent),
// Set the output path for each article
new SetDestination(Config.FromDocument((doc, ctx)
=> new NormalizedPath( $"article/{doc.AsKontent<Article>().UrlPattern}.html"))),
};
OutputModules = new ModuleList {
// Write each article to disk
new WriteFiles()
};
}
}
}
- Run the generator:
dotnet run
You should now see that for every Article in the Kontent site there's an html file in the \output\article
folder.
For more control over your output, you can feed the rich content model provided by Kontent into Razor views. This is very similar to an ASP.NET (Core) web application.
- Add a Razor view named
_Article.cshtml
into/input
:
@model My.Models.Article
<h3>@Model.Title</h3>
@Model.BodyCopy
The Razor view uses the c# model class generated by Kontent. If you open the project in Visual Studio, you'll get code completion and feedback to help you write your view.
- Replace the Articles pipeline from the previous section with the following code:
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Statiq;
using Statiq.Common;
using Statiq.Core;
using Statiq.Razor;
using My.Models; // <== your model namespace
namespace My // <== your namespace
{
public class ArticlesPipeline : Pipeline
{
public ArticlesPipeline(IDeliveryClient client)
{
InputModules = new ModuleList{
// Load all articles from Kontent
new Kontent<Article>(client),
// Set the output path for each article
new SetDestination(KontentConfig.Get(
(Article item) => new NormalizedPath( $"article/{item.UrlPattern}.html"))),
};
ProcessModules = new ModuleList {
// Pull in the Razor view, the path is relative to /input
new MergeContent(new ReadFiles(patterns: "_Article.cshtml") ),
// Render the Razor view into the content of the document
new RenderRazor()
// Use the strongly-typed model for the Razor view
.WithModel(KontentConfig.As<Article>())
};
OutputModules = new ModuleList {
// Write each article to disk
new WriteFiles()
};
}
}
}
- Run the generator again:
dotnet run
The generated html files are in the \output\article
folder.
This is a very basic pipeline and gives you everything you need to get started with content served from a Kontent project. But Kontent has more advanced features and some nice extras that you can leverage to get better integration and more robust code.
If you need to filter out the the input document s from Kontent, it is possible to specify the query of your request using WithQuery(IQueryParameter[])
builder method.
Let's say, you just want first three latest articles that have the Title element set and you want to load only a subset of.
// ...
new Kontent<Article>(client)
.WithQuery(
new NotEmptyFilter($"elements.{Article.TitleCodename}"),
new OrderParameter($"elements.{Article.PostDateCodename}", SortOrder.Descending),
new LimitParameter(3),
new ElementsParameter(Article.TitleCodename, Article.SummaryCodeName, Article.PostDateCodename, Article.UrlPatternCodeName)
)
// ...
For all filtering possibilities, take a look to the Kontent .Net SDK docs.
Kontent allows you to have content within content. Which is very powerfull but requires a bit of work on the client side to make it work. You basically have two options:
Inline resolvers are used by the Kontent Delivery API Client to transform inline content items directly into HTML. This is used when your model class uses a string
property for a rich-text field.
Inline resolvers are perfect for simple content with very basic HTML.
You can also use the structured content in your application. This is achieved by making the content property of type IRichTextContent
. This allows you to render the inline content in views, partials, display templates or what ever code is appropriate.
Both these content models can be used with Statiq, it's up to your preferences.
Where ever Statiq hands you an IDocument
, use the extension method .AsKontent<TModel>()
to get the typed model from the document.
Kontent.Statiq wraps the strong typed content model in a Statiq Document. In order to access the strong typed model within the document, a couple of helpers are available.
Example | Description |
---|---|
.AsKontent<Page>() |
An extention on IDocument that gets the original Kontent item from the metadata |
Within Statiq, configuration delegates are a powerful way to specify custom logic that is evaluated during processing of the content. Kontent.Statiq provides a couple of helpers to get access to the strongly typed Kontent model wrapped inside the Statiq Document.
Example | Description |
---|---|
KontentConfig.As<Page>() |
Fetch the entire Kontent item |
KontentConfig.Get((Page page) => page.Title) |
Fetch a single value from a Kontent item |
KontentConfig.GetChildren<Page>(page => page.RelatedPages) |
Fetch a list of related content items from a property of the Kontent item. |
Kontent can also manage your images. It hase a very comprehensive set of image manipulations baked into the Delivery API and you can leverage that with Statiq.
For example:
<img src="@Model.TeaserImage.First().Url?w=350&h=350&fit=crop"/>
In this example TeaserImage
is an Asset field on a strong-typed model. We pick the first asset. You can set the image transformation parameters by hand or use the ImageUrl
extension method to get access to the ImageUrlBuilder from the Kontent .NET SDK.
The ImageUrlBuilder
provides a fluent API to specify image manipulations including resizing and a focus point. Finally, the Url
property returns the full URL.
Now you can leave it at that and get them served up from the Kontent API. For the full static site experience however, you can also download the images and store them in the site.
Kontent Statiq contains the KontentImageProcessor
module that will pickup all images in your HTML content. It will generate a local unique URL for each image and replace the original url with the local url.
After that you can use the KontentDownloadImages
module to download the images. Using the Statiq built-in task you can write them to disk.
These modules will process all images in your html. Use the
WithFilter
extension to limit what images processed and downloaded into your application.
If you're following along with the sample project:
- Process all images by adding the
KontentImageProcessor
module to the end of theProcessModules
phase of the Article pipeline (see demo code above for full listing).
...
ProcessModules = new ModuleList {
new MergeContent(new ReadFiles(patterns: "Article.cshtml") ),
new RenderRazor()
.WithModel(Config.FromDocument((document, context) => document.AsKontent<My.Models.Article>())),
// Get urls from all images and replace with a local path
new KontentImageProcessor()
};
...
- Now create a new pipeline:
public class DownloadImages : Pipeline
{
public DownloadImages()
{
Dependencies.Add(nameof(Articles));
PostProcessModules = new ModuleList(
// Pull documents from other pipelines
new ReplaceDocuments(Dependencies.ToArray()),
// Download the images
new KontentDownloadImages()
);
OutputModules = new ModuleList(
// Write the collected images to disk
new WriteFiles()
);
}
}
The KontentImageProcessor
will add any replaced image url to a collection in the Document metadata for processing by the KontentDownloadImages
module at a later stage.
The KontentTaxonomy
module allows you to get the taxonomies defined in Kontent. You can download a specific taxonomy group or a single group using filters.
public class Taxonomies : Pipeline
{
public Taxonomies(IDeliveryClient client)
{
PostProcessModules = new ModuleList(
// Load the taxonomy named 'menu'
new KontentTaxonomy(client)
.WithQuery(new EqualsFilter("system.codename", "menu"))
);
}
}
This will return all the terms for the taxonomy group menu as a list. For convenience the root node is skipped because you cannot select the root node as a tag in Kontent.
Using the .WithNesting(nesting:true, collapseRoot:false)
method on the module, you can make the module return a hierarchical structure of documents and control whether the root node is returned as well.
public class Taxonomies : Pipeline
{
public Taxonomies(IDeliveryClient client)
{
PostProcessModules = new ModuleList(
// Load the taxonomy named 'menu'
new KontentTaxonomy(client)
.WithQuery(new EqualsFilter("system.codename", "menu"))
);
}
}
To flatten this into a list, use the FlattenTree
module.
If you add a taxonomy field to one of your content types, this will result in a property of type IEnumerable<ITaxonomyTerm>
in the generated model class. You can match these terms up to a taxonomy group to get a structured set of pages. This enables you to manage the structure of the site using taxonomy or generate an overview of documents per tag. Note that you don't need to load the entire taxonomy if you don't need that structure.
new Pipeline
{
InputModules =
{
new Kontent<Article>(deliveryClient),
// Set taxonomy terms as metadata
new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Tags)),
// Group by taxonomy
new GroupDocuments( "Tags" )
.WithComparer(new TaxonomyTermComparer())
}
}
The output for this pipeline is a flat list of documents, one for each taxonomy term with the documents for that term as its children.
The taxonomy term on each document is avalailable through doc.AsTaxonomyTerm(Keys.GroupKey)
.
If you do need the structure of the taxonomy, for example to build a sitemap, you can join content to the taxonomy data returned by the KontentTaxonomy
module.
var articlePipeline = new Pipeline
{
InputModules =
{
// Load all articles
new Kontent<Article>(deliveryClient),
// Set taxonomy terms as metadata
new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap)),
// Group the articles by tag
new GroupDocuments( "Tags" ).WithComparer(new TaxonomyTermComparer())
}
};
var sitemapPipeline = new Pipeline
{
Dependencies = { "Articles" },
InputModules =
{
// Load the taxonomy as a flat list
new KontentTaxonomy(deliveryClient)
.WithQuery(new EqualsFilter("system.codename", "menu")),
},
ProcessModules = {
new SetMetadata( Keys.Children, Config.FromDocument((doc, ctx) =>
{
var taxonomyTerm = doc.AsKontentTaxonomyTerm().Codename;
// Find the articles for this term
return ctx.Outputs.FromPipeline("Articles")
.FirstOrDefault( x => x.AsKontentTaxonomyTerm(Keys.GroupKey)?.Codename==taxonomyTerm)
?.GetChildren()
.ToArray();
}))
}
};
The output of the sitemap pipeline is a flat list of nodes representing the taxonomy terms. Each nodes children are the documents that are tagged with that term.
Note that the path to each node in the tree is available through doc.Get<string>(Keys.TreePath)
.
Extensions on IDocument
that can be used with documents loaded through the KontentTaxonomy
module:
Example | Description |
---|---|
.AsKontentTaxonomy() |
Fetch the entire ITaxonomyGroup from a root taxonomy document. |
.AsKontentTaxonomyTerm() |
Fetch a single term from an IDocument |
.AsKomtentTaxonomyTerms() |
Fetch a list of child terms from an IDocument, if any. |
All the keys used by the Modules provided are listed in the KontentKeys class. For most cases there is a helper or extension to access the metadata but if you need direct access, you can use these keys to do so.
Key | Value |
---|---|
Keys.Title |
The name of the term or group |
Keys.GroupKey |
The code name of the term or group |
Keys.TreePath |
A string[] containing the path of the node in the taxonomy by code name |
Keys.Children |
The list of child terms as Statiq IDocument |
KontentKeys.Taxonomy.Terms |
The child terms as ITaxonomyTermDetails[] |
KontentKeys.Taxonomy.Group |
The taxonomy group as ITaxonomyGroup |
KontentKeys.Taxonomy.Term |
The taxonomy term as ITaxonomyTermDetails |
KontentKeys.System.Name |
The name of the term or group |
KontentKeys.System.CodeName |
The code name of the term or group |
KontentKeys.System.Id |
The id of the group (not available for terms) |
KontentKeys.System.LastModified |
The DateTime the group was last modified (not available for terms) |
<object type="application/kenticocloud" data-type="item" data-rel="component" data-codename="n2ef9e997_4691_0118_8777_c0ac9cee683b"></object>
This means that the Kontent Delivery SDK was unable to resolve the type of content included in your rich text field. Make sure you read the section on structured content and follow the configuration steps.
Implement and register a link resolver. See the Kontent docs for more information.
Make sure all your classes are in a namespace.
If you have a lot of content items, use the items-feed by calling .WithItemsFeed()
method on the Kontent module.
Mostly because Statiq itself is still beta and release packages should not rely on beta dependencies. As soon as Statiq moves to 1.0, so will this project. The Kontent Statiq is being used in production projects.
You'll need a .NET 6 development setup: Windows, Mac, Linux with VisualStudio, VS Code or Rider.
- You're welcome to send pull requests. Please create an issue first and include a unit test with your pull request to verify your code.
- Massive refactorings, code cleanups etc. will be rejected unless explictly agreed upon
- Adding additional Nuget package dependencies to the main assemblies is strongly discouraged
- Please read and respect the code of conduct
- Marnix van Valen on twitter : @marnixvanvalen or e-mail : marnix [at] alanta [dot] nl
- Why Kontent and Jamstack are primed for .NET and the Enterprise @ Kontent Horizons 2021 Brian McKeiver, 25 nov 2021
- Building fast Jamstack sites on .NET @ CloudSummit 2021 Ondrej Polesny on YouTube, 21 sept 2021
- Jamstack on .NET : how I built this site Marnix van Valen, 17 may 2021
- Returning to .NET Matt Nield, 11 mar 2021
- JAMStack on .NET microsite by Kentico
- Jamstack and .NET - friends or enemies? Ondrej Polesny on YouTube, 5 nov 2020
- Statiq Starter Kontent Lumen sources on Github, 20 oct 2020
- On .NET Live - Generating static web applications with Statiq on Youtube, 6 aug. 2020
- Static sites with Kentico Cloud, Statiq and Netlify Kristian Bortnik, 31 jan 2018