From 6f7c795b2ec149240d84f202a9e6847b5e35b665 Mon Sep 17 00:00:00 2001 From: stefan judis Date: Wed, 17 Aug 2016 11:46:30 +0200 Subject: [PATCH] feat: Refactor most of it Refactored base structure, added coverage and semantic-release. --- .editorconfig | 1 + .eslintrc | 3 + .gitignore | 3 + .jshintrc | 8 - .npmignore | 16 ++ .travis.yml | 22 ++ README.md | 258 +++++++++------------ docs/global-settings.md | 52 +++++ docs/source-file-settings.md | 295 ++++++++++++++++++++++++ index.js | 56 +++++ lib/index.js | 189 --------------- lib/processor.js | 128 ++++++++++ lib/processor.spec.js | 209 +++++++++++++++++ lib/util.js | 94 ++++++++ lib/util.spec.js | 154 +++++++++++++ lib/validator.js | 120 ++++++++++ lib/validator.spec.js | 97 ++++++++ package.json | 44 +++- test/index.spec.js | 173 ++++++++++++++ test/layouts/post.awesome | 1 + test/layouts/post.html | 1 + test/layouts/posts.html | 2 + test/layouts/single-post.html | 1 + test/src/posts-custom-filename.html | 9 + test/src/posts-extension.awesome | 10 + test/src/posts-filtered.html | 9 + test/src/posts-limited.html | 8 + test/src/posts-ordered.html | 8 + test/src/posts-permalink-structure.html | 9 + test/src/posts.html | 8 + test/src/single-post.html | 7 + 31 files changed, 1639 insertions(+), 356 deletions(-) create mode 100644 .eslintrc delete mode 100644 .jshintrc create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 docs/global-settings.md create mode 100644 docs/source-file-settings.md create mode 100644 index.js delete mode 100644 lib/index.js create mode 100644 lib/processor.js create mode 100644 lib/processor.spec.js create mode 100644 lib/util.js create mode 100644 lib/util.spec.js create mode 100644 lib/validator.js create mode 100644 lib/validator.spec.js create mode 100644 test/index.spec.js create mode 100644 test/layouts/post.awesome create mode 100644 test/layouts/post.html create mode 100644 test/layouts/posts.html create mode 100644 test/layouts/single-post.html create mode 100644 test/src/posts-custom-filename.html create mode 100644 test/src/posts-extension.awesome create mode 100644 test/src/posts-filtered.html create mode 100644 test/src/posts-limited.html create mode 100644 test/src/posts-ordered.html create mode 100644 test/src/posts-permalink-structure.html create mode 100644 test/src/posts.html create mode 100644 test/src/single-post.html diff --git a/.editorconfig b/.editorconfig index 0f3bb61..42664a9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,4 @@ [*.js] indent_style = space indent_size = 2 +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e3578aa --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "standard" +} diff --git a/.gitignore b/.gitignore index 3c3629e..670c872 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules +.vscode +test/build +.nyc_output diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index d623d53..0000000 --- a/.jshintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "globalstrict": true, - "node": true, - "quotmark": "single", - "strict": true, - "trailing": true, - "unused": true -} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f59886d --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +.*.swp +._* +.DS_Store +.git +.hg +.npmrc +.lock-wscript +.svn +.wafpickle-* +config.gypi +CVS +npm-debug.log +*.spec.js +test +.vscode +.nyc_output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e129a13 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +sudo: false +language: node_js +cache: + directories: + - node_modules +notifications: + email: false +node_js: + - '6' + - '4' +before_install: + - npm i -g npm@^2.0.0 +before_script: + - npm prune +after_success: + - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' + - python travis_after_all.py + - export $(cat .to_export_back) &> /dev/null + - npm run semantic-release +branches: + except: + - /^v\d+\.\d+\.\d+$/ diff --git a/README.md b/README.md index 5162b87..fff90e8 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,136 @@ # contentful-metalsmith +[![Build Status](https://travis-ci.org/contentful-labs/contentful-metalsmith.svg?branch=master)](https://travis-ci.org/contentful-labs/contentful-metalsmith) +[![Coverage Status](https://coveralls.io/repos/github/contentful-labs/contentful-metalsmith/badge.svg?branch=refactor)](https://coveralls.io/github/contentful-labs/contentful-metalsmith?branch=refactor) -A Metalsmith's plugin to get content from [Contentful](http://www.contentful.com) +A Metalsmith' plugin to generate files using content from [Contentful](http://www.contentful.com) -[![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) +## About -## Deprecation notice +This plugin for [metalsmith](http://www.metalsmith.io) allows you to build a static site using the data stored at [Contentful](http://www.contentful.com). It is built on top of the [Contentful JavaScript Client](https://github.com/contentful/contentful.js). -This project has not been maintained for some time and won't be maintained moving forward. +## Getting started -While it might work with some older versions of metalsmith, it's known not to work with more recent versions. +### Install -## About +```bash +$ npm install contentful-metalsmith +``` -This plugin for [metalsmith](http://www.metalsmith.io) allows you to build a static site using the data stored at [Contentful](http://www.contentful.com). This -plugin is built on top of the [Contentful JavaScript Client](https://github.com/contentful/contentful.js). +### Configure required globals -## TL;DR +When you use metalsmith using the [cli](https://github.com/metalsmith/metalsmith#cli) edit your `metalsmith.json` and add `contentful-metalsmith` in the plugins section. -1. Install it +```javascript +// metalsmith.json - ```javascript - $ npm install contentful-metalsmith - ``` -2. Configure it (example for [metalsmith CLI](https://github.com/segmentio/metalsmith#cli)) +{ + "source": "src", + "destination": "build", + "plugins": { + "contentful-metalsmith": { + "access_token": "YOUR_CONTENTFUL_ACCESS_TOKEN", + "space_id": "YOUR_CONTENTFUL_SPACE_ID" + } + } +} +``` - ```javascript - $ vim metalsmith.json +When you use the [JavaScript Api](https://github.com/metalsmith/metalsmith#api) add `contentful-metalsmith` to the used plugins. - ---- +```javascript +metalsmith.source('src') +metalsmith.destination('build') - { - "source": "src", - "destination": "build", - "plugins": { - ..., - "contentful-metalsmith": { - "accessToken" : "YOUR_CONTENTFUL_ACCESS_TOKEN" - }, - ... - } - } - ``` - -3. Create a source file - - ```html - - --- - title: OMG metalsmith-contentful - contentful: - space_id: AN_SPACE_ID - template: entries.html - --- - - [OPTIONAL CONTENT] - ``` - -4. Create the template (handlebarsjs on this case) - - ```html - - - - - - - Contentful-metalsmith plugin example - - - - - - - - {{contents}} - - - - - - - - - - {{#each contentful.entries}} - - - - - - {{/each}} - -
TypeIdUpdated
- {{this.data.sys.contentType.sys.id}} - - {{this.data.sys.id}} - - {{this.data.sys.updatedAt}} -
- - - ``` - -5. Enjoy - -# Usage - -The first thing that you have to do to use this plugin is to install and configure it (see the TL;DR section for that). Once you have done this you can create and setup source files to fetch data from [Contentful](http://www.contentful.com). - -## Setup a source file - -To fetch data from [Contentful](http://www.contentful.com) you have to include some extra metadata in a metalsmith source file. The available configuration parameters are the following: - -* `space_id` (**required**), the id of the space from where you want to get entries. -* `entry_template` (optional), the template that will be used to render each individual entry. -* `filter` (optional), this parameter has to include some of the [filtering options](https://www.contentful.com/developers/documentation/content-delivery-api/http/#search) offered by the [Contentful's Content Delivery API](https://www.contentful.com/developers/documentation/content-delivery-api/). - -All this parameters have to be nested under the key `contentful`. - - -An example: - -```yaml - - --- - title: OMG metalsmith-contentful - contentful: - space_id: cfexampleapi - content_type: cat - filter: - sys.id[in]: 'finn,jake' - entry_template: entry.html - template: example.html - --- +metalsmith.use(require('contentful-metalsmith')({ 'access_token' : 'YOUR_CONTENTFUL_ACCESS_TOKEN' })) ``` -## Using the fetched entries on the templates +**Global parameters:** + +- `acccess_token` +- `space_id` + +You can find the `access_token` and `space_id` in your [app](https://app.contentful.com) at `APIs -> Content delivery API Keys`. -We have to make a distinction between two types of templates: +------------------------------ -* The template rendered for the source file. -* And the template rendered for each individual entry. +To read more on all global parameters and settings read the [global settings documentation](./docs/global-settings.md). -In the context of the template rendered for the source file you will have access to a property named `contentful`. This property holds the following properties: +### Create files based on the files defined in your source folder -* `entries`, an array with all the fetched entries. The structure of each of this entry objects will be the same as the explained below for the entry template. -* `contentTypes`, an object with the id of the fetched [contentTypes](https://www.contentful.com/developers/documentation/content-delivery-api/http/#content-types) as keys. Under each key there will be an array with all the entries belonging to that particular contentType. +We're considering that you use [metalsmith-layouts](https://github.com/superwolff/metalsmith-layouts) for file rendering. That for the `layout` key is used for rendered source files and child templates. -In the context of the template rendered for an individual entry you will have access to the following properties under the property `data`: +*`source/posts.html`* -* `id`, a shortcut to the entry's id. -* `contentType`, a shortcut to the entry's contentType. -* `data`, the body of the entry as returned by [Contentful's Content Delivery API](https://www.contentful.com/developers/documentation/content-delivery-api/) +```markdown +--- +title: metalsmith-contentful file +contentful: + content_type: post + layout: post.html +layout: posts.html +--- -## Entry filename config -Following from the example above there are some options to help with getting the structure output: -```yaml - --- - title: OMG metalsmith-contentful - contentful: - space_id: cfexampleapi - content_type: cat - entry_template: entry.html - entry_filename_pattern: :sys.locale/:fields.slug - permalink_style: true - use_template_extension: true - template: example.html - --- +[OPTIONAL CONTENT] ``` -* `entry_filename_pattern` takes a pattern similar to the permalink plugin where you can reference Contentful system and user entered fields, prefixed `sys.` and `field.` respectively. -* `permalink_style` will name a directory with the last part of the pattern and add an `index.html` for the file content. e.g. `my/file/path.html` vs. `my/file/path/index.html`. -* `use_template_extension` is only required if you want the extension to match the template extension. `.html` is used by default. +*`layouts/posts.html`* + +```html + + + + + {{title}} + + + + + + + {{contents}} + + +``` + +*`layouts/post.html`* + +```html + + + + + {{data.fields.title}} + + + + + +

{{data.fields.title}}

+

{{data.fields.description}}

+ + {{contents}} + + +``` + +**This example will** + +- render `posts.html` providing data of the entries of content type `post` +- render several single files with the template `post.html` providing the data of a particular post + +------------------------------ + +To read more on source file parameters and settings read the [source file documentation](./docs/source-file-settings.md). # License -MIT +MIT \ No newline at end of file diff --git a/docs/global-settings.md b/docs/global-settings.md new file mode 100644 index 0000000..b4f461b --- /dev/null +++ b/docs/global-settings.md @@ -0,0 +1,52 @@ +# Global settings + +To communication with Contentful you have to set at least `access_token` and `space_id`. +You can find these in your [app](https://app.contentful.com) at `APIs -> Content delivery API Keys`. + +```json +{ + "source": "src", + "destination": "build", + "plugins": { + "contentful-metalsmith": { + "access_token" : "YOUR_CONTENTFUL_ACCESS_TOKEN", + "space_id": "YOUR_CONTENTFUL_SPACE_ID", + "host": "preview.contentful.com" + } + } +} +``` + +## Parameters + +### `access_token` *(optional)* + +Global access token used to connect with the Contentful API. +You can define the `access_token` global in your `metalsmith.json` or define it separately in given source files. + +*Recommended way here is to set the `access_token` and `space_id` of your mainly used space in the `metalsmith.json` or global config and overwrite it if needed in depending source files.* + +**Side note:** When you decide to not set a global `access_token` you have to set it in every single source file. + +See [source file settings](./source-file-settings.md) for further information. + +### `space_id` *(optional)* + +Global space id the data will be fetched from. +You can define the `space_id` global in your `metalsmith.json` or define it separately in given source files. + +*Recommended way here is to set the `access_token` and `space_id` of your mainly used space in the `metalsmith.json` or global config and overwrite it if needed in depending source files.* + +**Side note:** When you decide to not set a global `space_id` you have to set it in every single source file. + +See [source file settings](./source-file-settings.md) for further information. + +### `host` *(optional)* + +In case you want to use the [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/) you can set the depending token +and change the `host` property to `preview.contentful.com`. + +For using the [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) you can ignore this option, as it is defaulting to **Content Delivery API**. + +*Recommended way here is to set the `host` in the `metalsmith.json` or global config and overwrite it if needed in depending source files.* + diff --git a/docs/source-file-settings.md b/docs/source-file-settings.md new file mode 100644 index 0000000..9072dfa --- /dev/null +++ b/docs/source-file-settings.md @@ -0,0 +1,295 @@ +# Source file settings + +Using `contentful-metalsmith` you can render singe or multiple files with your data on Contentful. + +## Parameters + +### `content_type` *(optional)* + +**Render a collection page including multiple entries.** + +You can define a `content_type` in your source file. This content type id is base for the depending data fetching. +The collection data will be available in the template under the `data.entries` key. + +*`source/posts.html`* + +```markdown +--- +title: Post overview +contentful: + content_type: post # id of the given content type +layout: posts.html +--- +Your post overview content here +``` + +*`layouts/posts.html`* + +```html + + + + + {{title}} + + + + + + + {{contents}} + + +``` + +### `entry_template` *(optional)* + +**Render a collection page including multiple entries and render every entry using a separate template.** + +*`source/posts.html`* + +```markdown +--- +title: Post overview +contentful: + content_type: post # id of the given content type - can refer to a hash + entry_template: post.html # template found in `layouts` folder +layout: posts.html +--- +Your post overview content here +``` + +*`layouts/posts.html`* + +```html + + + + + {{title}} + + + + + + + {{contents}} + + +``` + +*`layouts/post.html`* + +```html + + + + + + {{data.fields.title}} + + + + + +

{{data.fields.title}}

+

{{data.fields.description}}

+ + +``` + +*For convenience and making it easier to link to the child files, the `_fileName` property was added to every fetched entry*. + +### `entry_filename_pattern` *(optional)* + +**Define custom file names for rendered child entries** + +`source/posts-with-custom-file-name.html` + +```markdown +--- +title: test__posts__customFileName +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + entry_filename_pattern: post-${ fields.title } + entry_template: post.html +layout: posts.html +--- +POSTS-CONTENT-CUSTOM-FILENAME +``` + +Using `entry_filename_pattern` you can define what the file names of the entries should be. You can use `${}` notation to access any property of the given entry. + +**Whatever you choose will be "slugged" using [slug](https://www.npmjs.com/package/slug).** + +So you don't have to worry about whitespaces or anything. + +E.g. +- `entry_filename_pattern: post-${ fields.title }` +- `entry_filename_pattern: ${ fields.title }-${ sys.id }` + +**Default value:** `${sys.contentType.sys.id}-${sys.id}` + +### `entry_id` *(optional)* + +**Render a file based on a single entry** + +*`source/single-post.html`* + +```markdown +--- +title: Single entry to display +contentful: + entry_id: A96usFSlY4G0W4kwAqswk +layout: single-post.html +--- +POST-SINGLE-POST +``` + +*`layouts/single-post.html`* + +```html + + + + + {{data.fields.title}} + + + + + +

{{data.fields.title}}

+

{{data.fields.description}}

+ + {{contents}} + + +``` + +### `filter` *(optional)* + +**Filter the entries before rendering to one or multiple files** + +```markdown +--- +title: Post overview of entries including "rabbit" +contentful: + content_type: post + filter: + query: 'rabbit' +layout: posts.html +--- +Post that include rabbits are... +``` + +If you want to learn more about the filter syntax, check out the [Content Delivery API documentation](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters). + +### `limit` *(optional)* + +**Limit the entries before rendering to one or multiple files** + +```markdown +--- +title: Post overview limited to ten entries +contentful: + content_type: post + limit: 10 +layout: posts.html +--- +10 posts are... +``` + +If you want to learn more about limits, check out the [Content Delivery API documentation](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/limit). + +### `order` *(optional)* + +**Order the entries before rendering to one or multiple files** + +```markdown +--- +title: Post overview sorted by creation date +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + order: -sys.createdAt +layout: posts.html +--- +The oldest posts are... +``` + +If you want to learn more about order settings, check out the [Content Delivery API documentation](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order). + +### `use_template_extension` *(optional)* + +**Render child entries with template extension of the source file.** + +*`source/single-post-with.extension`* + +```markdown +--- +title: test__posts__extension +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + use_template_extension: true + entry_template: post.awesome + foo: bar +layout: posts.html +--- +POSTS-CONTENT-EXTENSION +``` + +This will render several child entry with the set `entry_filename_pattern` and the extension `awesome`. + +### `create_permalinks` *(optional)* + +**Render directories including an index file with the depending `entry_filename_pattern` e.g. `my/file/path.html` becomes `my/file/path/index.html`.** + +*`source/single-post-with-permalinks.html`* + +```markdown +--- +title: test__posts__permalinks +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + entry_template: post.html + create_permalinks: true +layout: posts.html +--- +POSTS-CONTENT-PERMALINKS +``` + +### `space_id` & `access_token` *(optional)* + +**Overwrite the space id and/or access token for a single file.** + +*`source/different-space.html`* + +```markdown +--- +title: test__posts__from_somewhere_else +contentful: + space_id: 1qptv5yuwnfh + access_token: d599954af0e2ae1e3714f69ca9f0812cafc44578c9b5c5e8f87119757ce2b1e3 + content_type: 2wKn6yEnZewu2SCCkus4as +layout: posts.html +--- +POSTS-CONTENT-DIFFERENT-SPACE +``` \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..2ea7d34 --- /dev/null +++ b/index.js @@ -0,0 +1,56 @@ +'use strict' + +const processor = require('./lib/processor') + +/** + * Plugin function + * + * @param {Object|undefined} options + * + * @return {Function} Function to be used in metalsmith process + */ +function plugin (options) { + options = options || {} + + /** + * Function to process all read files by metalsmith + * + * @param {Object} files file map + * @param {Object} metalsmith metalsmith + * @param {Function} done success callback + */ + return function (files, metalsmith, done) { + return new Promise(resolve => { + resolve(Object.keys(files)) + }) + .then(fileNames => { + return Promise.all( + fileNames.map(fileName => { + files[fileName]._fileName = fileName + + return processor.processFile(files[fileName], options) + }) + ) + }) + .then((fileMaps) => { + fileMaps.forEach(map => { + Object.assign(files, map) + }) + + done() + }) + .catch((error) => { + // friendly error formatting to give + // more information in error case by api + // -> see error.details + done( + new Error(` + ${error.message} + ${error.details ? JSON.stringify(error.details, null, 2) : ''} + `) + ) + }) + } +} + +module.exports = plugin diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 56da20b..0000000 --- a/lib/index.js +++ /dev/null @@ -1,189 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var each = require('async').each; -var contentful = require('contentful'); -var debug = require('debug')('contentful-metalsmith'); -var slug = require('slug-component'); - -/** - * Expose the plugin - */ -module.exports = plugin; - - -function plugin(options){ - enforcep(options, 'accessToken'); - - return function(files, metalsmith, done){ - var keys = Object.keys(files); - - each(keys, processFile, done); - - //TODO: switch to bluebird promises - function processFile(file, fileProcessedCallback){ - var fileMetadata = files[file], client, query; - - if (!fileMetadata.contentful) { - fileProcessedCallback(); - return; - } - - enforcep(fileMetadata.contentful, 'space_id'); - - /* - * contentTypes will contain all the contentTypes - * fetched for this source file - */ - fileMetadata.contentful.contentTypes = {}; - - /* - * entries will contain the entries fetched from the API - * for this file - */ - fileMetadata.contentful.entries = []; - - client = createContentfulClient(options.accessToken, fileMetadata.contentful.space_id), - query = _.extend({}, - (fileMetadata.contentful.content_type ? {content_type : fileMetadata.contentful.content_type} : undefined), - fileMetadata.contentful.filter - ); - - client.entries(query).then(onSuccessfulEntriesFetch(fileMetadata.contentful, fileProcessedCallback), onErroneousEntriesFetch(fileProcessedCallback)); - - /* - * Proceed to next file - */ - debug('Processed file ' + file ); - } - - function onSuccessfulEntriesFetch(options, done) { - return function(data){ - each(data, - entryProcessor({ - entries : options.entries, - template : options.entry_template, - contentTypes : options.contentTypes, - filenameField : options.entry_filename_pattern, - asPermalink : options.permalink_style, - useTemplateExtension : options.use_template_extension - }), - done); - }; - } - - function onErroneousEntriesFetch(done) { - return function(err) { - debug('An unexpected error happened while trying to fetch the entries (' + err.message +')'); - done(err); - }; - } - - - function ensureContentType(contentTypes, contentType){ - contentTypes[contentType] = contentTypes[contentType] || []; - return contentTypes; - } - - function pushEntryToContentType(contentTypes, contentType, entry){ - contentTypes[contentType].push(entry); - } - - function entryProcessor(options) { - return function (entry, entryProcessedCallback){ - var file, - //TODO: fix the contentType - contentType = options.contentType ? contentType : entry.sys.contentType.sys.id; - var extension = (options.useTemplateExtension) ? options.template.split('.').slice(1).pop() : 'html'; - - /** - * Builds a filename out of a pattern if provided. - * - * @param {Object} entry - * @return {String} - */ - var getFilename = function getFilename (entry) { - // Default filename - var filename = contentType + '-' + file.id + "." + extension; - /** - * Get the params from a `pattern` string. - * - * @param {String} pattern - * @return {Array} - */ - var getParams = function getParams (pattern) { - var matcher = /:([\w]+\.[\w]+)/g; - var ret = []; - var m; - while (m = matcher.exec(pattern)) ret.push(m[1]); - return ret; - } - - if (options.filenameField) { - var pattern = options.filenameField; - var params = getParams(pattern); - - params.forEach(function(element, index){ - var element_parts = element.split('.'); - var prefix = element_parts[0]; - var item = element_parts[1]; - - if (entry[prefix] && void 0 !== entry[prefix][item]) { - pattern = pattern.replace(":" + element, slug(entry[prefix][item].toString())); - } - }); - - // Check all have been processed - if (getParams(pattern).join('') === '') { - filename = (options.asPermalink) ? pattern + "/index.html" : pattern + "." + extension; - } - } - return filename; - }; - - /* - * Create a "virtual" (virtual because it doesn't exist in the src/ dir) - * file that will be processed by metalsmith - */ - file = { - contents : "", // Contents needs to be defined beacuse other plugins expect it - data : entry, - id : entry.sys.id, - contentType : contentType, - template : options.template - }; - - /* - * Give a name to the file that will be created on - * the build dir - */ - if (options.template){ - files[getFilename(entry)] = file; - } - - // This check is being performed for each entry, it might be done out of this loop - ensureContentType(options.contentTypes, contentType); - pushEntryToContentType(options.contentTypes, contentType, file); - options.entries.push(file); - - entryProcessedCallback(); - }; - } - - function createContentfulClient(accessToken, spaceId){ - return contentful.createClient({ - space : spaceId, - accessToken : accessToken - }); - } - }; - - function exists(value){ - return value != null; - } - - function enforcep(object, property) { - if (!exists(object[property])) - throw new TypeError('Expected property ' + property); - } -} diff --git a/lib/processor.js b/lib/processor.js new file mode 100644 index 0000000..4deacfb --- /dev/null +++ b/lib/processor.js @@ -0,0 +1,128 @@ +'use strict' + +const contentful = require('contentful') +const validator = require('./validator') +const util = require('./util') +const clients = {} + +/** + * Create contentful client + * + * @param {String} accessToken access token + * @param {String} spaceId space id + * + * @return {Object} contentful client + */ +function getContentfulClient (accessToken, spaceId, host) { + if (!clients[spaceId]) { + clients[spaceId] = contentful.createClient({ + space: spaceId, + accessToken, + host + }) + } + + return clients[spaceId] +} + +/** + * Enrich all fetched entries with additional properties + * + * @param {Array} entries fetched entries + * @param {Object} file file the entries were fetched for + * + * @return {Array} enriched entries + */ +function mapEntriesForFile (entries, file, options) { + return entries.map(entry => { + entry._fileName = util.getFileName(entry, file.contentful, options) + + return entry + }) +} + +/** + * Process the fetched entries by contentful + * for given file + * + * @param {Object} file file read by metalsmith + * @param {Array} entries entries fetched from contentful + * + * @return {Object} file mapping object + */ +function processEntriesForFile (file, entries) { + const options = file.contentful + const files = {} + + files[file._fileName] = file + + if (options.entry_id) { + validator.validateSingleEntryForFile(entries[0], file) + + file.data = entries[0] + } else { + let contentTypes = entries.reduce((collection, entry) => { + if (!collection[entry.sys.contentType.sys.id]) { + collection[entry.sys.contentType.sys.id] = [] + } + collection[entry.sys.contentType.sys.id].push(entry) + + return collection + }, {}) + + file.data = { entries, contentTypes } + } + + if (options.entry_template) { + return entries.reduce((fileMap, entry) => { + fileMap[ entry._fileName ] = { + // `contents` need to be defined because there + // might be other plugins that expect it + contents: '', + data: entry, + id: entry.sys.id, + contentType: options.content_type, + layout: options.entry_template, + + _fileName: entry._fileName, + _parentFileName: file._fileName + } + + return fileMap + }, files) + } + + return files +} + +/** + * Process one file and connect it with contentful data + * + * @param {Object} file file read by metalsmith + * @param {Object} options contentful metalsmith options + * + * @return {Boolean|Promise} + */ +function processFile (file, options) { + if (!file.contentful) { + return true + } + + validator.validateFile(file) + validator.validateFileAndOptions(file, options) + + const spaceId = file.contentful.space_id || options.space_id + const accessToken = file.contentful.access_token || options.access_token + const host = file.contentful.host || options.host + const query = util.getEntriesQuery(file.contentful) + + const client = getContentfulClient(accessToken, spaceId, host) + + return client.entries(query) + .then(entries => mapEntriesForFile(entries, file, options)) + .then(entries => processEntriesForFile(file, entries)) +} + +module.exports = { + processFile +} diff --git a/lib/processor.spec.js b/lib/processor.spec.js new file mode 100644 index 0000000..e2bc682 --- /dev/null +++ b/lib/processor.spec.js @@ -0,0 +1,209 @@ +import test from 'ava' +import proxyquire from 'proxyquire' +import contentful from 'contentful' + +test('processor.processFile - return the initial file when no contentful meta is set', t => { + const processor = proxyquire( + './processor', + { + contentful + } + ) + + const file = {} + + t.is(processor.processFile(file), true) +}) + +test.cb('processor.processFile - call entries with correct properties coming from file meta', t => { + const processor = proxyquire( + './processor', + { + contentful: { + createClient: function (options) { + t.is(options.accessToken, 'bar') + t.is(options.host, 'baz') + t.is(options.space, 'foo') + + t.end() + + return { + entries: function () { + return new Promise(resolve => resolve([])) + } + } + } + } + } + ) + + processor.processFile({ + contentful: { + host: 'baz', + space_id: 'foo' + } + }, { + access_token: 'bar' + }) +}) + +test('processor.processFile - call entries with correct properties coming from global settings', t => { + const processor = proxyquire( + './processor', + { + contentful: { + createClient: function (options) { + t.is(options.accessToken, 'global-bar') + t.is(options.host, 'global-baz') + t.is(options.space, 'global-foo') + + return { + entries: function () { + return new Promise(resolve => resolve([])) + } + } + } + } + } + ) + + return processor.processFile({ + contentful: {} + }, { + access_token: 'global-bar', + host: 'global-baz', + space_id: 'global-foo' + }) +}) + +test('processor.processFile - resolves correctly for a single entry', t => { + const entryToBeReturned = { + sys: { + id: 'bazinga', + contentType: { + sys: { + id: 'boing' + } + } + }, + fields: { + name: 'John Doe' + } + } + + const processor = proxyquire( + './processor', + { + contentful: { + createClient: function () { + return { + entries: function () { + return new Promise(resolve => resolve([ entryToBeReturned ])) + } + } + } + } + } + ) + + return processor.processFile({ + contentful: { + entry_id: '123456789' + }, + _fileName: 'awesome-file.html' + }, { + access_token: 'global-bar', + host: 'global-baz', + space_id: 'global-foo' + }).then(fileMap => { + t.is(typeof fileMap['awesome-file.html'], 'object') + t.is(fileMap['awesome-file.html'].data, entryToBeReturned) + }) +}) + +test('processor.processFile - resolves correctly for multiple entries', t => { + const entriesToBeReturned = [ + { + sys: { + id: 'bazinga', + contentType: { + sys: { + id: 'boing' + } + } + }, + fields: { + name: 'John Doe' + } + }, + { + sys: { + id: 'badabum', + contentType: { + sys: { + id: 'kazum' + } + } + }, + fields: { + name: 'Jane Doe' + } + } + ] + + const processor = proxyquire( + './processor', + { + contentful: { + createClient: function () { + return { + entries: function () { + return new Promise(resolve => resolve(entriesToBeReturned)) + } + } + } + } + } + ) + + /* eslint-disable no-template-curly-in-string */ + return processor.processFile({ + contentful: { + content_type: 'article', + entry_filename_pattern: 'article/${ fields.name }', + entry_template: 'template.html' + + }, + _fileName: 'awesome-file.html' + }, { + access_token: 'global-bar', + host: 'global-baz', + space_id: 'global-foo' + }).then(fileMap => { + t.deepEqual( + fileMap['article/john-doe.html'], + { + contents: '', + data: entriesToBeReturned[0], + id: entriesToBeReturned[0].sys.id, + contentType: 'article', + layout: 'template.html', + _fileName: 'article/john-doe.html', + _parentFileName: 'awesome-file.html' + } + ) + + t.deepEqual( + fileMap['article/jane-doe.html'], + { + contents: '', + data: entriesToBeReturned[1], + id: entriesToBeReturned[1].sys.id, + contentType: 'article', + layout: 'template.html', + _fileName: 'article/jane-doe.html', + _parentFileName: 'awesome-file.html' + } + ) + }) +}) diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..ef8df3a --- /dev/null +++ b/lib/util.js @@ -0,0 +1,94 @@ +'use strict' + +const validator = require('./validator') +const pick = require('lodash.pick') +const template = require('lodash.template') +const slug = require('slug-component') + +const propertiesToPickEasily = ['limit', 'order'] +const notFoundKey = '__not-available__' +const notFoundRegex = new RegExp(notFoundKey) + +/** + * Wrapped slug function to return + * dummy string in case of no value + * + * @param {String|undefined} value string to be "slugged" + * + * @return {String} slug or placeholder + */ +function _slug (value) { + if (value) { + return slug(value) + } + + return notFoundKey +} + +/** + * Get entries query + * + * @param {Object} options file meta options + * + * @return {Object} query obejct + */ +function getEntriesQuery (options) { + const query = {} + + if (options.content_type && options.content_type !== '*') { + query.content_type = options.content_type + } + + if (options.entry_id) { + query['sys.id'] = options.entry_id + } + + if (options.filter) { + Object.assign(query, options.filter) + } + + Object.assign(query, pick(options, propertiesToPickEasily)) + + return query +} + +/** + * @param {Object} entry contentful entry + * @param {Object} options file meta options + * + * @return {String} file name + */ +function getFileName (entry, fileOptions, globalOptions) { + fileOptions = fileOptions || {} + globalOptions = globalOptions || {} + + const extension = fileOptions.use_template_extension + ? fileOptions.entry_template.split('.').slice(1).pop() + : 'html' + + let renderedPattern = `${entry.sys.contentType.sys.id}-${entry.sys.id}` + + if (fileOptions.entry_filename_pattern) { + /* eslint-disable no-template-curly-in-string */ + const pattern = fileOptions.entry_filename_pattern.replace( + /\${\s*?(.*?)\s*?}/g, + '${_slug($1)}' + ) + + const renderData = Object.assign({ _slug }, entry) + renderedPattern = template(pattern)(renderData) + + validator.validateFileNameForEntry(renderedPattern, entry, notFoundRegex, globalOptions) + } + + if (fileOptions.create_permalinks) { + return `${renderedPattern}/index.${extension}` + } + + return `${renderedPattern}.${extension}` +} + +module.exports = { + getEntriesQuery, + getFileName +} diff --git a/lib/util.spec.js b/lib/util.spec.js new file mode 100644 index 0000000..ab405cd --- /dev/null +++ b/lib/util.spec.js @@ -0,0 +1,154 @@ +import test from 'ava' +import util from './util' + +test('util.getEntriesQuery - it should set content_type properly', t => { + let query = util.getEntriesQuery({}) + t.is(query.content_type, undefined) + + query = util.getEntriesQuery({ content_type: '*' }) + t.is(query.content_type, undefined) + + query = util.getEntriesQuery({ content_type: 'foo' }) + t.is(query.content_type, 'foo') +}) + +test('util.getEntriesQuery - it should set sys.id properly', t => { + let query = util.getEntriesQuery({ entry_id: 'bar' }) + t.is(query['sys.id'], 'bar') +}) + +test('util.getEntriesQuery - it should set given properties properly', t => { + let query = util.getEntriesQuery({ limit: 10 }) + t.is(query.limit, 10) + + query = util.getEntriesQuery({ filter: { foo: 'bar' } }) + t.is(query.foo, 'bar') + + query = util.getEntriesQuery({ order: 'ASC' }) + t.is(query.order, 'ASC') +}) + +test('util.getFileName - should use the correct defaults', t => { + const fileName = util.getFileName( + { + sys: { + contentType: { + sys: { + id: 'foo' + } + }, + id: 'bar' + } + } + ) + + t.is(fileName, 'foo-bar.html') + t.pass() +}) + +test('util.getFileName - should handle pattern correctly', t => { + /* eslint-disable no-template-curly-in-string */ + const fileName = util.getFileName( + { + sys: { + contentType: { + sys: { + id: 'foo' + } + }, + id: 'bar' + }, + fields: { + title: 'baz', + author: { + name: 'boing' + } + } + }, + { + entry_filename_pattern: '${sys.id}/${fields.author.name}-${fields.title}' + } + ) + + t.is(fileName, 'bar/boing-baz.html') + t.pass() +}) + +test('util.getFileName - should handle pattern and permalinks correctly', t => { + /* eslint-disable no-template-curly-in-string */ + const fileName = util.getFileName( + { + sys: { + contentType: { + sys: { + id: 'foo' + } + }, + id: 'bar' + }, + fields: { + title: 'baz', + author: { + name: 'boing' + } + } + }, + { + entry_filename_pattern: '${sys.id}/${fields.author.name}-${fields.title}', + create_permalinks: true + } + ) + + t.is(fileName, 'bar/boing-baz/index.html') + t.pass() +}) + +test('util.getFileName - should handle extensions correctly', t => { + /* eslint-disable no-template-curly-in-string */ + const fileName = util.getFileName( + { + sys: { + contentType: { + sys: { + id: 'foo' + } + }, + id: 'foo' + }, + fields: { + title: 'bar' + } + }, + { + entry_template: 'something.crazy', + entry_filename_pattern: '${sys.id}/${fields.title}', + use_template_extension: true + } + ) + + t.is(fileName, 'foo/bar.crazy') + t.pass() +}) + +test('util.getFileName - should set notFound key', t => { + /* eslint-disable no-template-curly-in-string */ + const fileName = util.getFileName( + { + sys: { + contentType: { + sys: { + id: 'foo' + } + }, + id: 'foo' + }, + fields: {} + }, + { + entry_filename_pattern: '${sys.id}/${fields.notFound}' + } + ) + + t.is(fileName, 'foo/__not-available__.html') + t.pass() +}) diff --git a/lib/validator.js b/lib/validator.js new file mode 100644 index 0000000..2398dc7 --- /dev/null +++ b/lib/validator.js @@ -0,0 +1,120 @@ +'use strict' + +/** + * @param {Object} file file object containing settings + * + * @throws Error in case `entry_template` and `entry_id` are + * set in the same file + */ +function validateFile (file) { + if (file.contentful.entry_id && file.contentful.entry_template) { + throw new Error(` + 'entry_id' and 'entry_template' set in '${file._fileName}'. + + Please set only one of these as these are conflicting. + For more infos check TODO docs.`) + } +} + +/** + * @param {Object} file file object containing settings + * @param {Object} options options object containing metalsmith settings + * + * @throws Error in case no `space_id` is found in file or general settings + */ +function validateFileAndOptions (file, options) { + let spaceId = file.contentful && file.contentful.space_id + ? file.contentful.space_id + : options.space_id + + if (!spaceId) { + throw new Error(` + No space id found. + + Please set the space id either in your 'metalsmith.json' + + "plugins": { + "contentful-metalsmith": { + "space_id" : "SPACE_ID" + } + } + + or in ${file._fileName} + + contentful: + space_id: SPACE_ID + + You can find it in your contentful app ( https://app.contentful.com/ ) + under 'API -> Content delivery / preview keys'` + ) + } + + let accessToken = file.contentful && file.contentful.access_token + ? file.contentful.access_token + : options.access_token + + if (!accessToken) { + throw new Error(` + No access token found found. + + Please set the access token either in your 'metalsmith.json' + + "plugins": { + "contentful-metalsmith": { + "access_token" : "ACCESS_TOKEN" + } + } + + or in ${file._fileName} + + contentful: + access_token: ACCESS_TOKEN + + You can find it in your contentful app ( https://app.contentful.com/ ) + under 'API -> Content delivery / preview keys'` + ) + } +} + +/** + * @param {Object} entry fetched entry + * @param {Object} file file that included `entry_id` + * + * @throws Error in case the fetched entry is undefined + */ +function validateSingleEntryForFile (entry, file) { + if (!entry) { + throw new Error(` + Single entry with id '${file.contentful.entry_id}' defined in ${file._fileName} was not found.` + ) + } +} + +/** + * @param {Object} fileName evaluated file name + * @param {Object} entry entry fetched from contentful + * @parem {RexExp} regex RegExp to check for + * @param {Object} options global metalsmith options + * + * @throws Error in case of detected pattern + */ +function validateFileNameForEntry (fileName, entry, regex, options) { + if (regex.test(fileName)) { + if (options.throw_errors) { + throw new Error( + `contentful-metalsmith: \'entry_file_pattern\' for entry with id '${entry.sys.id}' could not be resolved` + ) + } else { + console.warn( + `contentful-metalsmith: \'entry_file_pattern\' for entry with id '${entry.sys.id}' could not be resolved -> falling back to ${fileName}` + ) + } + } +} + +module.exports = { + validateFile, + validateFileAndOptions, + validateFileNameForEntry, + validateSingleEntryForFile +} diff --git a/lib/validator.spec.js b/lib/validator.spec.js new file mode 100644 index 0000000..3818889 --- /dev/null +++ b/lib/validator.spec.js @@ -0,0 +1,97 @@ +import test from 'ava' +import validator from './validator' + +test('Validator.validateFile - it should throw when id and template are set', t => { + t.throws( + _ => validator.validateFile({ contentful: { entry_id: 1, entry_template: 'foo' } }) + ) +}) + +test('Validator.validateFile - it should not throw when id,template or none are set', t => { + t.notThrows( + _ => validator.validateFile({ contentful: { entry_template: 'foo' } }) + ) + + t.notThrows( + _ => validator.validateFile({ contentful: { entry_id: 1 } }) + ) + + t.notThrows( + _ => validator.validateFile({ contentful: {} }) + ) +}) + +test('Validator.validateFileAndOptions - it should throw when no space id is set', t => { + t.throws( + _ => validator.validateFileAndOptions({}, {}) + ) +}) + +test('Validator.validateFileAndOptions - it should not throw when space id is set', t => { + t.notThrows( + _ => validator.validateFileAndOptions({ contentful: { space_id: 1, access_token: 2 } }, {}) + ) + + t.notThrows( + _ => validator.validateFileAndOptions({ contentful: { access_token: 2 } }, { space_id: 1 }) + ) +}) + +test('Validator.validateFileAndOptions - it should throw when no access_token is set', t => { + t.throws( + _ => validator.validateFileAndOptions({ contentful: { space_id: 1 } }, {}) + ) +}) + +test('Validator.validateFileAndOptions - it should not throw when an access token is set', t => { + t.notThrows( + _ => validator.validateFileAndOptions({ contentful: { access_token: 1, space_id: 2 } }, {}) + ) + + t.notThrows( + _ => validator.validateFileAndOptions({ contentful: { space_id: 2 } }, { access_token: 1 }) + ) +}) + +test('Validator.validateFileNameForEntry - it should throw when pattern is found and global options is set', t => { + t.throws( + _ => validator.validateFileNameForEntry( + 'foo__bar__baz', + {}, + /__bar__/, + { throw_errors: true } + ) + ) +}) + +test('Validator.validateFileNameForEntry - it should not throw when pattern is not found or global option is not set', t => { + t.notThrows( + _ => validator.validateFileNameForEntry( + 'foo__bar__baz', + { sys: { id: 123 } }, + /___bara___/, + { throw_errors: true } + ) + ) + + t.notThrows( + _ => validator.validateFileNameForEntry( + 'foo__bar__baz', + { sys: { id: 123 } }, + /__bar__/, + { throw_errors: false } + ) + ) +}) + +test('Validator.validateSingleEntryForFile - it should throw when entry is undefined', t => { + t.throws( + _ => validator.validateSingleEntryForFile(undefined, {}) + ) +}) + +test('Validator.validateSingleEntryForFile - it should not throw when entry is defined', t => { + t.notThrows( + _ => validator.validateSingleEntryForFile({}, {}) + ) +}) diff --git a/package.json b/package.json index 6024463..4ff80f3 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,49 @@ { "name": "contentful-metalsmith", - "version": "0.1.0", "description": "A Metalsmith's plugin to get content from Contentful", - "main": "lib/index.js", + "main": "index.js", "repository": { "type": "git", - "url": "git@github.com:contentful/contentful-metalsmith.git" + "url": "https://github.com/contentful-labs/contentful-metalsmith.git" }, "author": "Contentful GmbH", "license": "MIT", - "bugs": { - "url": "https://github.com/contentful/contentful-metalsmith/issues" + "engines": { + "node": ">=4" }, - "homepage": "https://github.com/contentful/contentful-metalsmith", "dependencies": { - "async": "^0.9.0", "contentful": "^0.1.2", - "debug": "^1.0.3", - "lodash": "^2.4.1", + "lodash.pick": "^4.4.0", + "lodash.template": "^4.4.0", "slug-component": "^1.1.0" + }, + "devDependencies": { + "ava": "^0.16.0", + "coveralls": "^2.11.12", + "cz-conventional-changelog": "^1.2.0", + "eslint": "^3.3.1", + "eslint-config-standard": "^6.0.0-beta.3", + "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-standard": "^2.0.0", + "handlebars": "^4.0.5", + "metalsmith": "^2.2.0", + "metalsmith-layouts": "^1.6.5", + "nyc": "^8.1.0", + "proxyquire": "^1.7.10", + "semantic-release": "^4.3.5" + }, + "release": { + "debug": true + }, + "scripts": { + "coveralls": "nyc report --reporter=text-lcov | coveralls", + "lint": "eslint \"**/*.js\"", + "test": "npm run lint && nyc ava **/*.spec.js", + "semantic-release": "semantic-release pre && npm publish && semantic-release post" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } } } diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..f9a2aec --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,173 @@ +import test from 'ava' +import fs from 'fs' +import Metalsmith from 'metalsmith' + +const expectedResults = { + postsCustomFileName: `Down the Rabbit HoleSeven Tips From Ernest Hemingway on How to Write Fiction +test__posts__customFileName-POSTS-CONTENT-CUSTOM-FILENAME\n`, + postsOverview: `Down the Rabbit HoleSeven Tips From Ernest Hemingway on How to Write Fiction +test__posts-POSTS-CONTENT\n`, + postsPermalinkStructure: `Down the Rabbit HoleSeven Tips From Ernest Hemingway on How to Write Fiction +test__posts__permalinks-POSTS-CONTENT-PERMALINKS\n`, + postsExtension: `Down the Rabbit HoleSeven Tips From Ernest Hemingway on How to Write Fiction +test__posts__extentsion-POSTS-CONTENT-EXTENSION\n`, + postsFiltered: `Down the Rabbit Hole +test__posts__filtered-POSTS-CONTENT-FILTERED\n`, + postsLimited: `Down the Rabbit Hole +test__posts__limited-POSTS-CONTENT-LIMITED\n`, + postsOrdered: `Seven Tips From Ernest Hemingway on How to Write FictionDown the Rabbit Hole +test__posts__ordered-POSTS-CONTENT-ORDERED\n`, + posts: { + downTheRabbitHole: 'Down the Rabbit Hole', + sevenTips: 'Seven Tips From Ernest Hemingway on How to Write Fiction' + }, + singlePost: 'Single Post - Seven Tips From Ernest Hemingway on How to Write Fiction' +} + +/** + * create metalsmith instance + * + * @param {Object} config config options + * + * @return {Object} metalsmith instance + * + */ +function createMetalsmith (config = {}) { + const metalsmith = new Metalsmith(__dirname) + + metalsmith.use( + require('..')(config) + ) + metalsmith.use( + require('metalsmith-layouts')({ engine: 'handlebars' }) + ) + + metalsmith.source('src') + metalsmith.destination('build') + + return metalsmith +} + +test.serial.cb('e2e - it propagate errors properly', t => { + const metalsmith = createMetalsmith({}) + + metalsmith.build(error => { + t.is(error instanceof Error, true) + t.end() + }) +}) + +test.serial.cb('e2e - it should render all templates properly', t => { + const metalsmith = createMetalsmith({ + space_id: 'w7sdyslol3fu', + access_token: 'baa905fc9cbfab17b1bc0b556a7e17a3e783a2068c9fd6ccf74ba09331357182' + }) + + metalsmith.build(error => { + if (error) { + throw error + } + + // + // render default posts + // + t.is( + fs.readFileSync(`${__dirname}/build/posts.html`, { encoding: 'utf8' }), + expectedResults.postsOverview + ) + + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-1asN98Ph3mUiCYIYiiqwko.html`, { encoding: 'utf8' }), + expectedResults.posts.downTheRabbitHole + ) + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-A96usFSlY4G0W4kwAqswk.html`, { encoding: 'utf8' }), + expectedResults.posts.sevenTips + ) + + // + // render filtered posts + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-filtered.html`, { encoding: 'utf8' }), + expectedResults.postsFiltered + ) + + // + // render limited posts + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-limited.html`, { encoding: 'utf8' }), + expectedResults.postsLimited + ) + + // + // render ordered posts + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-ordered.html`, { encoding: 'utf8' }), + expectedResults.postsOrdered + ) + + // + // render posts with custom filenames + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-custom-filename.html`, { encoding: 'utf8' }), + expectedResults.postsCustomFileName + ) + + t.is( + fs.readFileSync(`${__dirname}/build/post-down-the-rabbit-hole.html`, { encoding: 'utf8' }), + expectedResults.posts.downTheRabbitHole + ) + t.is( + fs.readFileSync(`${__dirname}/build/post-seven-tips-from-ernest-hemingway-on-how-to-write-fiction.html`, { encoding: 'utf8' }), + expectedResults.posts.sevenTips + ) + + // + // render a post defined with id + // + t.is( + fs.readFileSync(`${__dirname}/build/single-post.html`, { encoding: 'utf8' }), + expectedResults.singlePost + ) + + // + // render posts with permalink structure + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-permalink-structure.html`, { encoding: 'utf8' }), + expectedResults.postsPermalinkStructure + ) + + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-1asN98Ph3mUiCYIYiiqwko/index.html`, { encoding: 'utf8' }), + expectedResults.posts.downTheRabbitHole + ) + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-A96usFSlY4G0W4kwAqswk/index.html`, { encoding: 'utf8' }), + expectedResults.posts.sevenTips + ) + + // + // render posts using the given template extension + // + t.is( + fs.readFileSync(`${__dirname}/build/posts-extension.awesome`, { encoding: 'utf8' }), + expectedResults.postsExtension + ) + + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-1asN98Ph3mUiCYIYiiqwko.awesome`, { encoding: 'utf8' }), + expectedResults.posts.downTheRabbitHole + ) + t.is( + fs.readFileSync(`${__dirname}/build/2wKn6yEnZewu2SCCkus4as-A96usFSlY4G0W4kwAqswk.awesome`, { encoding: 'utf8' }), + expectedResults.posts.sevenTips + ) + + t.end() + }) +}) diff --git a/test/layouts/post.awesome b/test/layouts/post.awesome new file mode 100644 index 0000000..16c9826 --- /dev/null +++ b/test/layouts/post.awesome @@ -0,0 +1 @@ +{{data.fields.title}} \ No newline at end of file diff --git a/test/layouts/post.html b/test/layouts/post.html new file mode 100644 index 0000000..16c9826 --- /dev/null +++ b/test/layouts/post.html @@ -0,0 +1 @@ +{{data.fields.title}} \ No newline at end of file diff --git a/test/layouts/posts.html b/test/layouts/posts.html new file mode 100644 index 0000000..879670f --- /dev/null +++ b/test/layouts/posts.html @@ -0,0 +1,2 @@ +{{#each data.entries }}{{fields.title}}{{/each}} +{{{title}}}-{{{contents}}} \ No newline at end of file diff --git a/test/layouts/single-post.html b/test/layouts/single-post.html new file mode 100644 index 0000000..ab37320 --- /dev/null +++ b/test/layouts/single-post.html @@ -0,0 +1 @@ +Single Post - {{data.fields.title}} \ No newline at end of file diff --git a/test/src/posts-custom-filename.html b/test/src/posts-custom-filename.html new file mode 100644 index 0000000..cf93a40 --- /dev/null +++ b/test/src/posts-custom-filename.html @@ -0,0 +1,9 @@ +--- +title: test__posts__customFileName +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + entry_filename_pattern: post-${ fields.title } + entry_template: post.html +layout: posts.html +--- +POSTS-CONTENT-CUSTOM-FILENAME diff --git a/test/src/posts-extension.awesome b/test/src/posts-extension.awesome new file mode 100644 index 0000000..edb6bad --- /dev/null +++ b/test/src/posts-extension.awesome @@ -0,0 +1,10 @@ +--- +title: test__posts__extentsion +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + use_template_extension: true + entry_template: post.awesome + foo: bar +layout: posts.html +--- +POSTS-CONTENT-EXTENSION diff --git a/test/src/posts-filtered.html b/test/src/posts-filtered.html new file mode 100644 index 0000000..e78868c --- /dev/null +++ b/test/src/posts-filtered.html @@ -0,0 +1,9 @@ +--- +title: test__posts__filtered +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + filter: + query: 'rabbit' +layout: posts.html +--- +POSTS-CONTENT-FILTERED diff --git a/test/src/posts-limited.html b/test/src/posts-limited.html new file mode 100644 index 0000000..e326f3d --- /dev/null +++ b/test/src/posts-limited.html @@ -0,0 +1,8 @@ +--- +title: test__posts__limited +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + limit: 1 +layout: posts.html +--- +POSTS-CONTENT-LIMITED diff --git a/test/src/posts-ordered.html b/test/src/posts-ordered.html new file mode 100644 index 0000000..1fe2bf7 --- /dev/null +++ b/test/src/posts-ordered.html @@ -0,0 +1,8 @@ +--- +title: test__posts__ordered +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + order: -sys.createdAt +layout: posts.html +--- +POSTS-CONTENT-ORDERED diff --git a/test/src/posts-permalink-structure.html b/test/src/posts-permalink-structure.html new file mode 100644 index 0000000..8007883 --- /dev/null +++ b/test/src/posts-permalink-structure.html @@ -0,0 +1,9 @@ +--- +title: test__posts__permalinks +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + entry_template: post.html + create_permalinks: true +layout: posts.html +--- +POSTS-CONTENT-PERMALINKS diff --git a/test/src/posts.html b/test/src/posts.html new file mode 100644 index 0000000..7fabac6 --- /dev/null +++ b/test/src/posts.html @@ -0,0 +1,8 @@ +--- +title: test__posts +contentful: + content_type: 2wKn6yEnZewu2SCCkus4as + entry_template: post.html +layout: posts.html +--- +POSTS-CONTENT diff --git a/test/src/single-post.html b/test/src/single-post.html new file mode 100644 index 0000000..839f377 --- /dev/null +++ b/test/src/single-post.html @@ -0,0 +1,7 @@ +--- +title: test__singlePost +contentful: + entry_id: A96usFSlY4G0W4kwAqswk +layout: single-post.html +--- +POST-SINGLE-POST