Contentful ❤️️ Wyam == true

Contentful wyam integration

At Contentful, we love static site generators. They offer a beautiful combination of speed and simplicity, and they become even more powerful when combined with a dynamic content store to add the one thing a static site lacks, dynamism.


We already have numerous integrations and extension for many popular static site generators, with Jekyll, Middleman, and Roots being some. Today I'm happy to announce that we now have an official module for the .Net-based static site generator Wyam! Wyam is a highly modular and extremely configurable static content generator and toolkit. It is built by .Net MVP and all-around awesome guy Dave Glick, as well as by numerous Github contributors.

The concept of Wyam differs slightly from many other open source static site generators. In Wyam you craft your own pipeline of modules that work together to create your final static output. You configure your pipelines and modules yourself (even though there are a few pre-configured "recipes") using the config.wyam file, which can be extremely powerful as it's actually evaluated as C# code. This means that you can write C# and take full advantage of the entire .Net ecosystem. An example configuration file could look something like this:

1
2
3
4
5
6
7
8
9
#n Wyam.Markdown
#n Wyam.Yaml

Pipelines.Add(
    ReadFiles("*.md"),
    FrontMatter(Yaml()),
    Markdown(),
    WriteFiles(".html")
);

The first two lines are preprocessor directives pulling in NuGet packages which contain some of the modules that we then use in our pipeline. This simple pipeline consists of reading any files with a .md extension, reading any front matter in YAML and adding that as metadata to the documents, then parsing the markdown content to HTML and finally writing the files to disk with an .html extension. More information about writing your configuration file can be found at wyam.io.

How does Contentful fit into the Wyam family? With our official module you can now fetch any content from Contentful, and use that as the basis of your generated static content in Wyam.

The first thing you need to do is to add a reference to the NuGet package in your config.wyam.

1
#n -p Contentful.Wyam

Then we can now reference the module in our pipeline like this.

1
2
3
4
5
#n -p Contentful.Wyam

Pipelines.Add(
    Contentful("<delivery_api_key>", "<space_id>")
);

This would now fetch all of our entries from Contentful and add them to the pipeline. Every field of the entry will be made available as metadata to the document. There are a number of fluent methods available to configure which content the module will fetch and to add other configuration options.

1
2
3
4
5
6
7
#n -p Contentful.Wyam

Pipelines.Add(
    Contentful("<delivery_api_key>", "<space_id>")
        .WithContentType("<content_type_id>")
        .WithContentField("body")
);

Here we fetch all entries of a specific content type and we also specify which field of the content type should be used as the content of the documents created in the Wyam pipeline.

By adding a few more modules we can create some really powerful pipelines. In the next example we are using Razor templates with a layout that is then compiled into static HTML files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Preprocessor directives
#n Wyam.Razor
#n Wyam.Markdown
#n -p Contentful.Wyam
#n Wyam.Html
#n Wyam.Less

// Body code
Pipelines.Add("Contentful",
    Contentful("<delivery_api_key>", "<space_id>")
        .WithContentField("body")
        .WithContentType("blogPost"),
    Markdown(),
    Excerpt()
        .WithOuterHtml(false),
    Meta("Body", @doc.Content),
    Merge(ReadFiles("templates/post.cshtml")),
    Razor(),
    WriteFiles($"posts/{@doc["slug"]}.html")
);

In this example we're setting the body field of the content fetched from Contentful as the content of our documents. We're also specifying to only fetch entries of the blogPost content type. We then parse the markdown content of each document into HTML. The next module, Excerpt, pulls the first paragraph out of our HTML and adds it as metadata to our documents; this can be useful if we for example want to show a list of all blog posts and include a small excerpt of each. We then add the content of our document into a metadata property called Body so that we can access it later in our Razor file. After that we merge each document with a Razor template post.cshtml and using the Razor() module we then compile that into HTML and finally write the files to our posts output directory. Note how we name each HTML after the slug metadata property of the document. This is the slug field in Contentful!

The Razor template we're using is then accessing the metadata from the documents that were fetched from Contentful.

1
2
3
4
5
6
7
8
9
<h1>@Model.Get("Title")</h1>

@Html.Raw(Model.Get("Body"))

<div>
    @foreach (var image in Model.Get<JToken>("Images")) {
        <div>@Html.Raw(Model.ImageTagForAsset(image, width: 100, height: 100))</div>
    }
</div>

The Model for each document is a Wyam IDocument, which is the central class in a Wyam pipeline. Each document has any number of metadata objects in a simple key/value store that you can access using the Get<T> method. This method will then intelligently cast the value into T, and you can use the result in your code. As you can see we're getting the title, the body and images from our metadata, all of which are of course fields in our entry in Contentful. The Contentful.Wyam package also provides a couple of handy extension methods, and we can see that we're using the ImageTagForAsset one here. It simply takes a JToken or asset id and generates an img tag leveraging the image API of Contentful in the background. That means that you can do things like use facial recognition, resize images, set the border radius, set the background color and much more, directly in your static site.

You can also configure multiple pipelines for different parts of your content. For example, it's possible to have one pipeline that fetches content from Contentful and another that just reads ordinary Markdown files off of disk. Or even have two pipelines that read two different content types off of Contentful and put them in separate directories as in the next example:

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
// Preprocessor directives
#n Wyam.Razor
#n Wyam.Markdown
#n -p Contentful.Wyam
#n Wyam.Html
#n Wyam.Less

// Body code
Pipelines.Add("BlogPosts",
    Contentful("<delivery_api_key>", "<space_id>")
        .WithContentField("body")
        .WithContentType("blogPost"),
    Markdown(),
    Excerpt()
        .WithOuterHtml(false),
    Meta("Body", @doc.Content),
    Merge(ReadFiles("templates/post.cshtml")),
    Razor(),
    WriteFiles($"posts/{@doc["slug"]}.html")
);

Pipelines.Add("Articles",
    Contentful("<delivery_api_key>", "<space_id>")
        .WithContentField("body")
        .WithContentType("article"),
    Markdown(),
    Meta("Body", @doc.Content),
    Merge(ReadFiles("templates/article.cshtml")),
    Razor(),
    WriteFiles($"articles/{@doc["slug"]}.html")
);

Localization

By default, Contentful only fetches the configured default locale for the entries, but this can be easily configured in your pipeline.

1
2
3
4
Pipelines.Add(
    Contentful("<delivery_api_key>", "<space_id>")
        .WithLocale("sv-SE")
);

If you want to fetch all entries in all configured locales, simply pass * as your argument.

1
2
3
4
Pipelines.Add(
    Contentful("<delivery_api_key>", "<space_id>")
        .WithLocale("*")
);

Please note that this will result in one document per entry per locale. If our entries were localized in three locales, every entry would be split into three separate documents in your pipeline. You can then use the metadata property ContentfulKeys.EntryLocale to see which locale a specific entry belongs to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Preprocessor directives
#n Wyam.Razor
#n Wyam.Markdown
#n -p Contentful.Wyam
#n Wyam.Html
#n Wyam.Less

// Body code
Pipelines.Add("BlogPosts",
    Contentful("<delivery_api_key>", "<space_id>")
        .WithContentField("body")
        .WithContentType("blogPost")
        .WithLocale("*"),
    Markdown(),
    Excerpt()
        .WithOuterHtml(false),
    Meta("Body", @doc.Content),
    Merge(ReadFiles("templates/post.cshtml")),
    Razor(),
    WriteFiles($"posts/{@doc["ContentfulKeys.EntryLocale"]}/{@doc["slug"]}.html")
);

Note how we put each document in a subdirectory with the same name as the documents locale by using @doc["ContetnfulKeys.EntryLocale"].

Handling included entries and assets

If you're familiar with our Content Delivery API, you know that you can save quite a few API calls by making sure you leverage the includes.Entry and includes.Asset arrays, which include entries and assets that are referenced from the those returned in your initial query. This is also possible in the Wyam module: there are two separate collections available in the metadata with the keys ContentfulKeys.IncludedAssets and ContentfulKeys.IncludedEntries. There are also extension methods available directly on IDocument to get an entry or asset directly from the collections. Below are a few examples in a Razor template.

1
2
3
4
5
6
7
8
9
10
11
12
// A collection of all included entries
@Model.List<Entry<dynamic>>(ContentfulKeys.IncludedEntries)
// A collection of all included assets
@Model.List<Asset>(ContentfulKeys.IncludedAssets)

@foreach (var referencedEntry in Model.Get<JToken>("ReferencedEntry")) {
    // I'm using the GetIncludedEntry extension method,
    // if you need to get an asset use GetIncludedAsset in the same manner
    var entry = Model.GetIncludedEntry(referencedEntry); 
    <div>@entry.Fields.name["en-US"]</div>
}

Naturally you can configure how many levels of referenced content you want to include.

1
2
3
4
Pipelines.Add(
    Contentful("<delivery_api_key>", "<space_id>")
        .WithIncludes(7)
);

This would include up to seven levels of referenced entries and assets.

Eager to start building something?

Go checkout the Wyam GitHub repository, the Wyam website, and the Contentful.Wyam GitHub repository. We can't wait to see what you will create!

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.