Skip to content
This repository has been archived by the owner on Nov 26, 2021. It is now read-only.

Adds support for global key #42

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ To get an idea on how this works, you can check out an [example blog site](https
### Install

```bash
$ npm install contentful-metalsmith
$ npm install contentful-metalsmith --save
```

### Configure required globals

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.
When using the [metalsmith cli](https://github.com/metalsmith/metalsmith#cli) you can edit your `metalsmith.json` and add `contentful-metalsmith` in the plugins section.

```javascript
// metalsmith.json
Expand All @@ -33,7 +33,12 @@ When you use metalsmith using the [cli](https://github.com/metalsmith/metalsmith
"plugins": {
"contentful-metalsmith": {
"access_token": "YOUR_CONTENTFUL_ACCESS_TOKEN",
"space_id": "YOUR_CONTENTFUL_SPACE_ID"
"space_id": "YOUR_CONTENTFUL_SPACE_ID",
"entry_key": "_key",
"entry_extension": "md",
"contentful": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~~Is the contentful property correct here?~~~

Found it.

"content_type": "2wKn6yEnZewu2SCCkus4as"
}
}
}
}
Expand All @@ -45,13 +50,22 @@ When you use the [JavaScript Api](https://github.com/metalsmith/metalsmith#api)
metalsmith.source('src')
metalsmith.destination('build')

metalsmith.use(require('contentful-metalsmith')({ 'access_token' : 'YOUR_CONTENTFUL_ACCESS_TOKEN' }))
metalsmith.use(require('contentful-metalsmith')({
'access_token': 'YOUR_CONTENTFUL_ACCESS_TOKEN',
'entry_key': '_key',
'entry_extension': 'md',
'contentful': {
'content_type': '2wKn6yEnZewu2SCCkus4as'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change this to CONTENT_TYPE_ID to make it clear to users that this field can be configurable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, was wondering about that. Is that fetching all the data under that content_type_id?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cameronroe yes it is fetching entries under that contentType

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. What would be the best way to configure fetching all data from Contentful for users who want to build a site with multiple content_types? Should that be an array then instead of a string?

}
}))
```

**Global parameters:**

- `acccess_token`
- `space_id`
- `entry_key`
- `entry_extension`

You can find the `access_token` and `space_id` in your [app](https://app.contentful.com) at `APIs -> Content delivery API Keys`.

Expand Down
19 changes: 18 additions & 1 deletion docs/global-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ You can find these in your [app](https://app.contentful.com) at `APIs -> Content
"contentful-metalsmith": {
"access_token" : "YOUR_CONTENTFUL_ACCESS_TOKEN",
"space_id": "YOUR_CONTENTFUL_SPACE_ID",
"host": "preview.contentful.com"
"host": "preview.contentful.com",
"entry_key": "_key",
"entry_extension": "md",
"contentful": {
"content_type": "2wKn6yEnZewu2SCCkus4as"
}
}
}
}
Expand Down Expand Up @@ -50,6 +55,16 @@ For using the [Content Delivery API](https://www.contentful.com/developers/docs/

*Recommended way here is to set the `host` in the `metalsmith.json` or global config and overwrite it if needed in depending source files.*

### `entry_key` *(optional)*

If you want to transform Contentful data into pages you can specify a `entry_key` which will be used to replace the key of all file objects with the stored Contentful key path. You must specify a path to the file as if it was referenced from your `src` directory and exclude the extension.

For example, if you specify `entry_key`: `_key`, then create a Contentful entry with a `_key` property set to `pages/index` a file will be referenced in Metalsmith with `pages/index.md` (assuming you specify `entry_extension` as `md`)

### `entry_extension` *(optional)*

If you specify `entry_key`, you will need to specify the entry extension for all file keys. This will be appended to the key on Contentful entries that contain the `entry_key`.

### `filterTransforms` *(optional)*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we need a lot more documentation now... And maybe update the getting started guide.
This addition makes total sense and is probably the way to go, but it's not easy to understand in the beginning.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, do you think there should be another section similar to this one? I could write up a longer description under this section and then link it to the docs/global-settings.md?


If you want to use dynamic values in a source file's filter query, you can provide an object containing named functions that will be invoked during the metalsmith build. For example, if you provide a configuration like this:
Expand Down Expand Up @@ -82,6 +97,8 @@ layout: posts.html
---
```

### [`contentful` *(optional)*](source-file-settings.md)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs definitely more explanation. :)


### `common` *(optional)*

The results of queries placed in this property will be made available in all templates
Expand Down
39 changes: 28 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

const processor = require('./lib/processor')

/**
* Handles errors
* @param {Function} done done callback
* @param {object} error error object
*/
function handleError (done, 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) : ''}
`)
)
}

/**
* Plugin function
*
Expand All @@ -22,6 +39,16 @@ function plugin (options) {
return function (files, metalsmith, done) {
options.metadata = metalsmith.metadata()

if (options.entry_key) {
return processor.createFilesFromEntries(options)
.then((fileMaps) => {
Object.assign(files, fileMaps)

done()
})
.catch(handleError.bind(null, done))
}

return new Promise(resolve => {
resolve(Object.keys(files))
})
Expand All @@ -41,17 +68,7 @@ function plugin (options) {

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) : ''}
`)
)
})
.catch(handleError.bind(null, done))
}
}

Expand Down
53 changes: 48 additions & 5 deletions lib/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ function mapEntriesForFile (entries, file, options) {
return entries.map(entry => {
entry._fileName = util.getFileName(entry, file.contentful, options)

if (options.entry_key) {
file._fileName = entry._fileName
}

return entry
})
}
Expand Down Expand Up @@ -117,16 +121,16 @@ function processEntriesForFile (file, entries, options) {
file.data = { entries, contentTypes }
}

if (contentfulOptions.entry_template) {
if (contentfulOptions.entry_template || options.entry_key) {
return entries.reduce((fileMap, entry) => {
fileMap[ entry._fileName ] = Object.assign({
// `contents` need to be defined because there
// might be other plugins that expect it
contents: '',
contents: options.entry_key ? Buffer.from(entry.fields.contents || '') : '',
data: entry,
id: entry.sys.id,
contentType: contentfulOptions.content_type,
layout: contentfulOptions.entry_template,
contentType: contentfulOptions.content_type || entry.sys.contentType.sys.id,
layout: options.entry_key ? entry.fields.layout : contentfulOptions.entry_template,

_fileName: entry._fileName,
_parentFileName: file._fileName
Expand All @@ -139,6 +143,44 @@ function processEntriesForFile (file, entries, options) {
return files
}

/**
* Filter out entries without `entry_key` in fields
*
* @param {Array} entries entries fetched from contentful
* @param {object} options global options
*
* @return {Array} filtered entries
*/
function filterEntries (entries, options) {
return entries.filter(entry => {
return entry.fields && entry.fields[options.entry_key]
})
}

/**
* Create new files from Contentful entries
*
* @param {object} options global options
*
* @return {object|promise}
*/
function createFilesFromEntries (options) {
const spaceId = options.space_id
const accessToken = options.access_token
const host = options.host

const query = util.getEntriesQuery(options.contentful, options.filterTransforms)
const file = { contentful: options.contentful }

const client = getContentfulClient(accessToken, spaceId, host)

return client.getEntries(query)
.then(entries => filterEntries(entries.items, options))
.then(entries => mapEntriesForFile(entries, file, options))
.then(entries => processEntriesForFile(file, entries, options))
.then(entries => getCommonContentForSpace(entries, options))
}

/**
* Process one file and connect it with contentful data
*
Expand Down Expand Up @@ -169,5 +211,6 @@ function processFile (file, options) {
}

module.exports = {
processFile
processFile,
createFilesFromEntries
}
57 changes: 57 additions & 0 deletions lib/processor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,60 @@ test('processor.getCommonContentForSpace - load a set of common content', t => {
t.is(fileMap['turnip.html'].common.doublets, entriesToBeReturned)
})
})

test.cb('processor.createFilesFromEntries - create new files from global options', t => {
const options = {
access_token: 'global-bar',
host: 'global-baz',
space_id: 'global-foo',
entry_key: '_key',
entry_extension: 'md',
contentful: {
entry_id: 'A96usFSlY4G0W4kwAqswk'
}
}

const entriesToBeReturned = [
{
sys: {
id: 'A96usFSlY4G0W4kwAqswk',
contentType: {
sys: {
id: 'boing'
}
}
},
fields: {
name: 'John Doe',
_key: 'pages/index',
layout: 'home.html',
contents: 'Home Page Content'
}
}
]

const processor = proxyquire(
'./processor',
{
contentful: {
createClient: function () {
return {
getEntries: function () {
return new Promise(resolve => resolve({items: entriesToBeReturned}))
}
}
}
}
}
)

processor.createFilesFromEntries(options)
.then(fileMap => {
// add the correct file name
t.is(typeof fileMap['pages/index.md'], 'object')
t.is(fileMap['pages/index.md'].layout, 'home.html')
t.is(fileMap['pages/index.md'].contents.toString(), 'Home Page Content')

t.end()
})
})
11 changes: 10 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ function getFileName (entry, fileOptions, globalOptions) {

const extension = fileOptions.use_template_extension
? fileOptions.entry_template.split('.').slice(1).pop()
: 'html'
: (
globalOptions.entry_extension
? globalOptions.entry_extension
: 'html'
)

let renderedPattern = `${entry.sys.contentType.sys.id}-${entry.sys.id}`

Expand All @@ -101,6 +105,11 @@ function getFileName (entry, fileOptions, globalOptions) {
return `${renderedPattern}/index.${extension}`
}

// use the entry key described from the global `key` option
if (globalOptions.entry_key && typeof globalOptions.entry_key === 'string' && entry.fields[globalOptions.entry_key]) {
return `${entry.fields[globalOptions.entry_key]}.${extension}`
}

return `${renderedPattern}.${extension}`
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"handlebars": "^4.0.5",
"metalsmith": "^2.2.0",
"metalsmith-layouts": "^1.6.5",
"metalsmith-markdown": "0.2.1",
"nock": "9.0.2",
"nyc": "^8.1.0",
"proxyquire": "^1.7.10",
"semantic-release": "^4.3.5"
Expand Down
Empty file added test/entry_key_build/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions test/entry_key_build/pages/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>Home Page Content</p>
Empty file added test/entry_key_src/.gitkeep
Empty file.
28 changes: 28 additions & 0 deletions test/fixtures/res.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = {
items: [{
sys: {
id: '1asN98Ph3mUiCYIYiiqwko',
space: 'w7sdyslol3fu',
type: 'Entry',
createdAt: '2015-01-30T12:45:03.282Z',
updatedAt: '2015-01-30T13:01:37.936Z',
revision: 2,
locale: 'en-US',
contentType: {
sys: {
id: 'boing'
}
}
},
fields: {
name: 'John Doe',
_key: 'pages/index',
layout: 'post.html',
contents: 'Home Page Content',
category: [],
tags: [],
date: '1865-11-26',
comments: false
}
}]
}
Loading