").WithRecursiveCalling();
72 | ```
73 |
74 | Once the `Contentful` module has run it will output all of your entries in Contentful as Wyam documents. Available as metadata will be all of the fields of the entry.
75 | Here's an example in a razor file.
76 |
77 | ```csharp
78 |
79 | Model.Get("productName")
80 |
81 | ```
82 |
83 | This would fetch the `productName` field from the current document.
84 |
85 | There's also four specific metadata properties that are always available: `ContentfulId`, `ContentfulLocale`, `ContentfulIncludedAssets` and `ContentfulIncludedEntries`.
86 |
87 | `ContentfulId` and `ContentfulLocale` are simply the id and the locale of the entry of the current document in Contentful.
88 |
89 | The `ContentfulIncludedAssets` and `ContentfulIncludedEntries` are two collections of referenced entries and assets. These correspond to the `includes` section of the json
90 | response returned from Contentful and can be used to input images, links to PDFs or other entries in your documents.
91 |
92 | To use them directly as metadata means alot of casting back and can result in quite bloated output.
93 |
94 | ```csharp
95 |
c.SystemProperties.Id == (Model.Get("featuredImage") as JToken)["sys"]["id"].ToString()).FilesLocalized["en-US"].Url)" />
96 | ```
97 |
98 | The contentful.wyam packages has a number of helper extension methods that are better suited for the task.
99 |
100 | ```csharp
101 | @Model.ImageTagForAsset(Model.Get("featuredImage"))
102 | //This will output an image tag as in the bloated example above.
103 | ```
104 |
105 | The image tag extension method also allows you to leverage the entire powerful Contentful image API to manipulate your images. It has the following optional parameters:
106 |
107 | * `alt` — the alt text of the image, will default to the title of the Asset in Contentful if available.
108 | * `width` — the width of the image.
109 | * `height` — the height of the image.
110 | * `jpgQuality` — the quality of the image if it is a JPG.
111 | * `resizeBehaviour` — how the image should be resized if needed.
112 | * `format` — the format of the image, supported are JPG, PNG or WEBP.
113 | * `cornerRadius` — the radius of the corners.
114 | * `focus` — the focus area of the image.
115 | * `backgroundColor` — the background color of the image.
116 |
117 | For more information on how to use the Contentful image API, refer to [the official documentation](https://www.contentful.com/developers/docs/references/images-api/).
118 |
119 | There are also extensions methods available to get an `Asset` or `Entry` directly.
120 |
121 | ```csharp
122 | @Model.GetIncludedAsset(Model.Get("topImage")) //Accepts a JToken, extracts the id and returns an Asset from the ContentfulIncludedAssets.
123 | @Model.GetIncludedAssetById("") //Returns an asset from ContentfulIncludedAssets by the specified id.
124 |
125 | @Model.GetIncludedEntry(Model.Get("referencedEntry")) //Accepts a JToken, extracts the id and returns an Entry from the ContentfulIncludedEntries.
126 | @Model.GetIncludedEntryById("") //Returns an entry from ContentfulIncludedEntries by the specified id.
127 | ```
128 |
129 | ## Example pipeline
130 |
131 | A common scenario for a content pipeline is to fetch a number of entries from contentful, parse markdown and then output them as .html files.
132 |
133 | Here's an example of such a pipeline with a complete `config.wyam` configuration file.
134 |
135 | ```csharp
136 | // Preprocessor directives
137 | #n -p Wyam.Razor
138 | #n -p Wyam.Markdown
139 | #n -p Contentful.Wyam
140 |
141 | // Body code
142 | Pipelines.Add("Contentful-pipeline",
143 | Contentful("","")
144 | .WithContentField("body").WithContentType(""), //Get all entries from contentful of the specified type, set the body field as content for each resulting document.
145 | Markdown(), //Parse the markdown from the documents and turn it into html.
146 | Meta("Body", @doc.Content), //Put the parsed HTML result into a metadata property to access later in the .cshtml files
147 | Merge(ReadFiles("templates/post.cshtml")), //Merge the documents with the post.cshtml template, now ready to be parsed by the Razor engine
148 | Razor(), //Parse the resulting documents using the Razor engine, turning them into plain .html output again.
149 | WriteFiles($"{@doc["slug"]}.html") //Write the files as .html files to disk
150 | );
151 | ```
--------------------------------------------------------------------------------
/Contentful.Wyam/IDocumentExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Wyam.Common.Documents;
5 | using Wyam.Common.Meta;
6 | using Contentful.Core.Images;
7 | using Contentful.Core.Models;
8 | using Newtonsoft.Json.Linq;
9 | using System.Linq;
10 |
11 | namespace Contentful.Wyam
12 | {
13 | ///
14 | /// Helper extension methods for IDocument.
15 | ///
16 | public static class IDocumentExtensions
17 | {
18 | ///
19 | /// Gets an asset from the ContentfulIncludedAssets collection.
20 | ///
21 | /// The IDocument.
22 | /// The Json token representing the asset.
23 | /// The found asset or null.
24 | public static Asset GetIncludedAsset(this IDocument doc, JToken token)
25 | {
26 | if (token["sys"] == null || token["sys"]["id"] == null)
27 | {
28 | return null;
29 | }
30 |
31 | return GetIncludedAssetById(doc, token["sys"]["id"].ToString());
32 | }
33 |
34 | ///
35 | /// Gets an asset from the ContentfulIncludedAssets collection.
36 | ///
37 | /// The IDocument.
38 | /// The id of the asset.
39 | /// The found asset or null.
40 | public static Asset GetIncludedAssetById(this IDocument doc, string id)
41 | {
42 | var assets = doc.List(ContentfulKeys.IncludedAssets);
43 |
44 | return assets.FirstOrDefault(c => c.SystemProperties.Id == id);
45 | }
46 |
47 | ///
48 | /// Gets an entry from the ContentfulIncludedEntries collection.
49 | ///
50 | /// The IDocument.
51 | /// The Json token representing the entry.
52 | /// The found entry or null.
53 | public static Entry GetIncludedEntry(this IDocument doc, JToken token)
54 | {
55 | if (token["sys"] == null || token["sys"]["id"] == null)
56 | {
57 | return null;
58 | }
59 |
60 | return GetIncludedEntryById(doc, token["sys"]["id"].ToString());
61 | }
62 |
63 | ///
64 | /// Gets an entry from the ContentfulIncludedEntries collection.
65 | ///
66 | /// The IDocument.
67 | /// The id of the entry.
68 | /// The found entry or null.
69 | public static Entry GetIncludedEntryById(this IDocument doc, string id)
70 | {
71 | var entries = doc.List>(ContentfulKeys.IncludedEntries);
72 |
73 | return entries.FirstOrDefault(c => c.SystemProperties.Id == id);
74 | }
75 |
76 | ///
77 | /// Creates an image tag for an asset.
78 | ///
79 | /// The IDocument.
80 | /// The Json token representing the asset.
81 | /// The alt text of the image. Will default to the title of the asset if null.
82 | /// The width of the image.
83 | /// The height of the image.
84 | /// The quality of the image.
85 | /// How the image should resize to conform to the width and height.
86 | /// The format of the image, jpg,png or webp.
87 | /// The corner radius of the image.
88 | /// The focus area of the image when resizing.
89 | /// The background color of any padding that is added to the image.
90 | /// The image tag as a string.
91 | public static string ImageTagForAsset(this IDocument doc, JToken token, string alt = null,
92 | int? width = null, int? height = null, int? jpgQuality = null, ImageResizeBehaviour resizeBehaviour = ImageResizeBehaviour.Default,
93 | ImageFormat format = ImageFormat.Default, int? cornerRadius = 0, ImageFocusArea focus = ImageFocusArea.Default, string backgroundColor = null)
94 | {
95 | if (token["sys"] == null || token["sys"]["id"] == null)
96 | {
97 | return null;
98 | }
99 |
100 | return ImageTagForAsset(doc, token["sys"]["id"].ToString(), alt, width, height, jpgQuality, resizeBehaviour, format, cornerRadius, focus, backgroundColor);
101 | }
102 |
103 | ///
104 | /// Creates an image tag for an asset.
105 | ///
106 | /// The IDocument.
107 | /// The id of the asset.
108 | /// The alt text of the image. Will default to the title of the asset if null.
109 | /// The width of the image.
110 | /// The height of the image.
111 | /// The quality of the image.
112 | /// How the image should resize to conform to the width and height.
113 | /// The format of the image, jpg,png or webp.
114 | /// The corner radius of the image.
115 | /// The focus area of the image when resizing.
116 | /// The background color of any padding that is added to the image.
117 | /// The image tag as a string.
118 | public static string ImageTagForAsset(this IDocument doc, string assetId, string alt = null,
119 | int? width = null, int? height = null, int? jpgQuality = null, ImageResizeBehaviour resizeBehaviour = ImageResizeBehaviour.Default,
120 | ImageFormat format = ImageFormat.Default, int? cornerRadius = 0, ImageFocusArea focus = ImageFocusArea.Default, string backgroundColor = null)
121 | {
122 | var asset = doc.List(ContentfulKeys.IncludedAssets)?.FirstOrDefault(c => c.SystemProperties.Id == assetId);
123 |
124 | if (asset == null)
125 | {
126 | return string.Empty;
127 | }
128 |
129 | var locale = doc.Get(ContentfulKeys.EntryLocale);
130 |
131 | var imageUrlBuilder = ImageUrlBuilder.New();
132 |
133 | if (width.HasValue)
134 | {
135 | imageUrlBuilder.SetWidth(width.Value);
136 | }
137 |
138 | if (height.HasValue)
139 | {
140 | imageUrlBuilder.SetHeight(height.Value);
141 | }
142 |
143 | if (jpgQuality.HasValue)
144 | {
145 | imageUrlBuilder.SetJpgQuality(jpgQuality.Value);
146 | }
147 |
148 | if (cornerRadius.HasValue)
149 | {
150 | imageUrlBuilder.SetCornerRadius(cornerRadius.Value);
151 | }
152 |
153 | imageUrlBuilder.SetResizingBehaviour(resizeBehaviour).SetFormat(format).SetFocusArea(focus).SetBackgroundColor(backgroundColor);
154 |
155 | if (alt == null && !string.IsNullOrEmpty(asset.TitleLocalized[locale]))
156 | {
157 | alt = asset.TitleLocalized[locale];
158 | }
159 |
160 | return $@"
";
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/Contentful.Wyam/Contentful.cs:
--------------------------------------------------------------------------------
1 | using Contentful.Core;
2 | using Contentful.Core.Errors;
3 | using Contentful.Core.Models;
4 | using Contentful.Core.Models.Management;
5 | using Contentful.Core.Search;
6 | using Newtonsoft.Json.Linq;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 | using System.Linq;
11 | using System.Net.Http;
12 | using System.Text;
13 | using System.Threading.Tasks;
14 | using Wyam.Common.Documents;
15 | using Wyam.Common.Execution;
16 | using Wyam.Common.Modules;
17 |
18 | namespace Contentful.Wyam
19 | {
20 | ///
21 | /// Fetch content from Contentful and create new documents using the fields as metadata.
22 | ///
23 | ///
24 | /// For each Entry in Contentful one output document per requested locale will be created. For each output document every field of the Entry will be available
25 | /// as metadata. In addition a number of other metadata properties will be set for each document.
26 | ///
27 | ///
28 | ///
29 | ///
30 | ///
31 | /// Content
32 | public class Contentful : IModule
33 | {
34 | private readonly IContentfulClient _client;
35 | private string _contentField = "";
36 | private string _contentTypeId = "";
37 | private string _locale = null;
38 | private int _includes = 1;
39 | private int _limit = 100;
40 | private int _skip = 0;
41 | private bool _recursive = false;
42 |
43 | ///
44 | /// Calls the Contentful API using the specified deliverykey and spaceId.
45 | ///
46 | /// The Contentful Content Delivery API key.
47 | /// The id of the space in Contentful from which to fetch content.
48 | public Contentful(string deliveryKey, string spaceId) : this(deliveryKey, spaceId, false)
49 | {
50 |
51 | }
52 |
53 | ///
54 | /// Calls the Contentful API using the specified deliverykey and spaceId.
55 | ///
56 | /// The Contentful Content Delivery API key.
57 | /// The id of the space in Contentful from which to fetch content.
58 | /// Whether or not to use the Contentful Preview API. Note that if the preview API is used a preview API key must also be specified.
59 | public Contentful(string deliveryKey, string spaceId, bool usePreview)
60 | {
61 | var httpClient = new HttpClient();
62 | _client = new ContentfulClient(httpClient, deliveryKey, "", spaceId, usePreview);
63 | }
64 |
65 | ///
66 | /// Calls the Contentful API using the specified IContentfulClient.
67 | ///
68 | /// The IContentfulClient to use when calling the Contentul API.
69 | public Contentful(IContentfulClient client)
70 | {
71 | _client = client ?? throw new ArgumentNullException(nameof(client), "IContentful client cannot be null.");
72 | }
73 |
74 | ///
75 | /// Specifies which field of the Contentful entries that should be used as content for the documents created.
76 | ///
77 | /// The id of the field in Contentful.
78 | public Contentful WithContentField(string field)
79 | {
80 | _contentField = field;
81 | return this;
82 | }
83 |
84 | ///
85 | /// Specifies that only entries of a specific content type should be fetched.
86 | ///
87 | /// The id of the content type in Contentful.
88 | public Contentful WithContentType(string contentTypeId)
89 | {
90 | _contentTypeId = contentTypeId;
91 | return this;
92 | }
93 |
94 | ///
95 | /// Specifies that only entries of a specific locale should be fetched.
96 | ///
97 | ///
98 | /// Note that by default only the default locale is fetched. If a specific locale is specified only that locale will be fetched.
99 | /// To fetch all locales use "*".
100 | ///
101 | /// The locale code of the locale in Contentful. E.g. "en-US".
102 | public Contentful WithLocale(string locale)
103 | {
104 | _locale = locale;
105 | return this;
106 | }
107 |
108 | ///
109 | /// Specifies the levels of included assets and entries that should be resolved when calling Contentful.
110 | ///
111 | ///
112 | /// Note that the maximum number of levels resolved are 10 and the default is 1.
113 | ///
114 | /// The number of levels of references that should be resolved.
115 | public Contentful WithIncludes(int includes)
116 | {
117 | _includes = includes;
118 | return this;
119 | }
120 |
121 | ///
122 | /// Specifies the maximum number of entries to fetch from Contentful.
123 | ///
124 | /// The maximum number of entries.
125 | public Contentful WithLimit(int limit)
126 | {
127 | _limit = limit;
128 | return this;
129 | }
130 |
131 | ///
132 | /// Specifies the number of entries to skip when fetching from Contentful.
133 | ///
134 | /// The number of entries to skip.
135 | public Contentful WithSkip(int skip)
136 | {
137 | _skip = skip;
138 | return this;
139 | }
140 |
141 | ///
142 | /// Specifies that the Contentful API should be called recursively until all entries have been fetched.
143 | ///
144 | ///
145 | /// Note that this might result in several calls being made to the Contentful API. Ratelimits may apply.
146 | ///
147 | public Contentful WithRecursiveCalling()
148 | {
149 | _recursive = true;
150 | return this;
151 | }
152 |
153 | ///
154 | public IEnumerable Execute(IReadOnlyList inputs, IExecutionContext context)
155 | {
156 | Space space = null;
157 |
158 | try
159 | {
160 | space = _client.GetSpace().Result;
161 | }
162 | catch (AggregateException ae)
163 | {
164 | ae.Handle((ex) => {
165 |
166 | if (ex is ContentfulException)
167 | {
168 | Trace.TraceError($"Error when fetching space from Contentful: {ex.Message}");
169 | Trace.TraceError($"Details: {(ex as ContentfulException).ErrorDetails.Errors}");
170 | Trace.TraceError($"ContentfulRequestId:{(ex as ContentfulException).RequestId}");
171 | }
172 |
173 | return false;
174 | });
175 | }
176 |
177 | var queryBuilder = CreateQueryBuilder();
178 | ContentfulCollection entries = null;
179 |
180 | try
181 | {
182 | entries = _client.GetEntries(queryBuilder).Result;
183 | }
184 | catch (AggregateException ae)
185 | {
186 | ae.Handle((ex) => {
187 |
188 | if (ex is ContentfulException)
189 | {
190 | Trace.TraceError($"Error when fetching entries from Contentful: {ex.Message}");
191 | Trace.TraceError($"Details: {(ex as ContentfulException).ErrorDetails.Errors}");
192 | Trace.TraceError($"ContentfulRequestId:{(ex as ContentfulException).RequestId}");
193 | }
194 |
195 | return false;
196 | });
197 | }
198 |
199 | if (_recursive && entries.Total > entries.Items.Count())
200 | {
201 | entries = FetchEntriesRecursively(entries);
202 | }
203 |
204 | var includedAssets = entries.IncludedAssets;
205 | var includedEntries = entries.IncludedEntries;
206 |
207 | var locales = space.Locales.Where(l => l.Default);
208 |
209 | if (_locale == "*")
210 | {
211 | locales = space.Locales;
212 | }
213 | else if (!string.IsNullOrEmpty(_locale))
214 | {
215 | locales = space.Locales.Where(l => l.Code == _locale);
216 | }
217 |
218 | if (!locales.Any())
219 | {
220 | //Warn or throw here?
221 | throw new ArgumentException($"Locale {_locale} not found for space. Note that locale codes are case-sensitive.");
222 | }
223 |
224 | foreach (var entry in entries)
225 | {
226 | foreach (var locale in locales)
227 | {
228 | var localeCode = locale.Code;
229 |
230 | var items = (entry as IEnumerable>).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
231 |
232 | var content = "No content";
233 |
234 | content = items.ContainsKey(_contentField) ? GetNextValidJTokenValue(items[_contentField], locale).Value() : "No content";
235 |
236 | if (string.IsNullOrEmpty(_contentField))
237 | {
238 | content = "";
239 | }
240 |
241 | var metaData = items
242 | .Where(c => !c.Key.StartsWith("$") && c.Key != "sys")
243 | .Select(c => new KeyValuePair(c.Key, GetNextValidJTokenValue(c.Value, locale))).ToList();
244 | metaData.Add(new KeyValuePair(ContentfulKeys.EntryId, $"{entry["sys"]["id"]}"));
245 | metaData.Add(new KeyValuePair(ContentfulKeys.EntryLocale, localeCode));
246 | metaData.Add(new KeyValuePair(ContentfulKeys.IncludedAssets, includedAssets));
247 | metaData.Add(new KeyValuePair(ContentfulKeys.IncludedEntries, includedEntries));
248 | var doc = context.GetDocument(context.GetContentStream(content), metaData);
249 |
250 | yield return doc;
251 | }
252 | }
253 |
254 | JToken GetNextValidJTokenValue(JToken token, Locale locale)
255 | {
256 | var localizedField = token.Children().FirstOrDefault(c => c.Name == locale.Code);
257 |
258 | if (localizedField == null && locale.FallbackCode != null)
259 | {
260 | return GetNextValidJTokenValue(token, locales.FirstOrDefault(c => c.Code == locale.FallbackCode));
261 | }
262 |
263 | return localizedField?.Value;
264 | }
265 | }
266 |
267 | private QueryBuilder CreateQueryBuilder()
268 | {
269 | var queryBuilder = QueryBuilder.New.LocaleIs("*").Include(_includes)
270 | .OrderBy(SortOrderBuilder.New("sys.createdAt").Build())
271 | .Limit(_limit).Skip(_skip);
272 |
273 | if (!string.IsNullOrEmpty(_contentTypeId))
274 | {
275 | queryBuilder.ContentTypeIs(_contentTypeId);
276 | }
277 |
278 | return queryBuilder;
279 | }
280 |
281 | private ContentfulCollection FetchEntriesRecursively(ContentfulCollection entries)
282 | {
283 | var entryList = new List();
284 | var includedAssets = new List();
285 | var includedEntries = new List>();
286 | var collection = new ContentfulCollection()
287 | {
288 | Items = entries.Items
289 | };
290 | entryList.AddRange(entries.Items);
291 | includedAssets.AddRange(entries.IncludedAssets);
292 | includedEntries.AddRange(entries.IncludedEntries);
293 |
294 | _skip += _limit;
295 |
296 | while (collection.Count() == _limit)
297 | {
298 | collection = _client.GetEntries(CreateQueryBuilder()).Result;
299 |
300 | entryList.AddRange(collection.Items);
301 | includedAssets.AddRange(collection.IncludedAssets.Where(c => !includedAssets.Any(a => a.SystemProperties.Id == c.SystemProperties.Id)));
302 | includedEntries.AddRange(collection.IncludedEntries.Where(c => !includedAssets.Any(a => a.SystemProperties.Id == c.SystemProperties.Id)));
303 |
304 | _skip += _limit;
305 | }
306 |
307 | collection.Items = entryList;
308 | collection.IncludedAssets = includedAssets;
309 | collection.IncludedEntries = includedEntries;
310 |
311 | return collection;
312 | }
313 | }
314 | }
315 |
--------------------------------------------------------------------------------