Skip to content

Commit

Permalink
Merge 57ef89d into 37bfd6c
Browse files Browse the repository at this point in the history
  • Loading branch information
zspecza committed Jan 21, 2016
2 parents 37bfd6c + 57ef89d commit dca5c55
Show file tree
Hide file tree
Showing 31 changed files with 650 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .istanbul.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
instrumentation:
root: lib
root: src
40 changes: 26 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,41 @@
"description": "An extension for using roots with the API-driven Contentful CMS",
"version": "0.0.9",
"author": "Carrot Creative",
"ava": {
"serial": true,
"verbose": true,
"require": [
"babel-core/register",
"coffee-script/register"
]
},
"bugs": {
"url": "https://github.com/carrot/roots-contentful/issues"
},
"dependencies": {
"ava": "^0.9.2",
"babel-runtime": "^6.3.13",
"contentful": "^2.1.0",
"contentful": "^2.1.1",
"deepcopy": "^0.6.1",
"pluralize": "^1.2.1",
"roots-util": "0.2.x",
"underscore.string": "^3.2.2"
"underscore.string": "^3.2.3"
},
"devDependencies": {
"ava": "^0.9.1",
"babel-cli": "^6.3.15",
"babel-core": "^6.3.26",
"ava": "^0.10.0",
"babel-cli": "^6.4.5",
"babel-core": "^6.4.5",
"babel-eslint": "^5.0.0-beta6",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-transform-runtime": "^6.3.13",
"babel-preset-es2015-node5": "^1.1.1",
"babel-plugin-add-module-exports": "^0.1.3-alpha",
"babel-plugin-transform-runtime": "^6.4.3",
"babel-preset-es2015-node5": "^1.1.2",
"babel-preset-stage-0": "^6.3.13",
"babel-runtime": "^6.3.19",
"coffee-script": "^1.10.0",
"coveralls": "^2.11.6",
"husky": "^0.10.2",
"mockery": "1.4.x",
"nyc": "^5.2.0",
"nyc": "^5.3.0",
"roots": "3.1.0",
"snazzy": "^2.0.1",
"standard": "^5.4.1"
Expand Down Expand Up @@ -62,22 +72,24 @@
"scripts": {
"build": "babel src -d lib",
"coverage": "nyc report --reporter=lcov",
"coveralls": "coveralls < coverage/lcov.info",
"debug-test": "NODE_DEBUG=request npm run test -- --serial --verbose --fail-fast",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"debug-test": "npm run test -- --fail-fast",
"lint": "standard --verbose | snazzy",
"postpublish": "git push --follow-tags",
"posttest": "node test/_teardown.js",
"prebuild": "npm test",
"precommit": "npm run lint -s",
"precoverage": "npm run test",
"precoveralls": "npm run coverage",
"prerelease": "npm run build",
"pretest": "npm run lint -s && node test/_setup.js",
"release": "npm publish",
"test": "nyc ava --require babel-core/register --require coffee-script/register"
"test": "nyc ava --verbose --serial --require babel-core/register --require coffee-script/register"
},
"standard": {
"parser": "babel-eslint",
"ignore": ["test/_setup.js", "test/_teardown.js"]
"ignore": [
"test/_setup.js",
"test/_teardown.js"
]
}
}
46 changes: 37 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ module.exports =
contentful
access_token: 'YOUR_ACCESS_TOKEN'
space_id: 'xxxxxx'
locale: 'tlh'
locales_prefix:
tlh: 'klingon_'
content_types:
blog_posts:
id: 'xxxxxx'
Expand Down Expand Up @@ -76,15 +79,17 @@ If a `template` option is defined for a Content Type in `app.coffee`, roots will

Contentful's [documentation](https://www.contentful.com/developers/documentation/content-delivery-api/#getting-entry) shows the API response when fetching an entry. Your content fields are nested in a `fields` key on the `entry` object. As a convenience, the entry object roots-contentful makes available in your views will have the `fields` key's value set one level higher on the object. System metadata remains accessible on the `sys` key and roots-contentful will raise an error if you have a field named `sys`. Inside your views, the entry object will have this structure:

```json
"entry": {
"title": "Wow. Such title. Much viral",
"author": "The Doge of Venice"
# ... the rest of the fields
"sys": {
"type": "Entry",
"id": "cat"
# ...
```js
{
"entry": {
"title": "Wow. Such title. Much viral",
"author": "The Doge of Venice"
// ... the rest of the fields
"sys": {
"type": "Entry",
"id": "cat"
# ...
}
}
}
```
Expand All @@ -109,6 +114,29 @@ Required. The space ID containing the content you wish to retrieve.

Optional. (Boolean) Allows you use the Contentful Preview API. Also able to be accessed by setting the environment variable `CONTENTFUL_ENV` to `"develop"` (preview api) or `"production"` (default cdn).

#### locales
Locales allow you to request your content in a different language, `tlh` is Klingon.

##### Global locale
Optional. (String or Array) Defines locale for all content_types.
String: `'tlh'`
Array: `['en-es', 'tlh']`
Wildcard: `'*'` - grabs all locales from contentful

##### content_type specific locale
Optional. (String) Define content_types locale, will override global locale. Add `locale: 'tlh'` to any of your content types and you'll retrieve that post in the specific locale.

#### locales_prefix
Optional. (Object) Defines the prefix given to a group of locales.

```
locales_prefix:
'tlh': 'klingon_'
'en-es': 'spanish_'
```

Lets say you have 3 global locales defined, `['tlh', 'en-us', 'en-es']`, and above is our defined locales_prefix. Since we did not declare `'en-us'` you can access it with `contentful.en_us_blog_posts`. Similarly you can access each preix according to what you've set in the object above. `'tlh'` would be accessible by `contentful.klingon_blog_posts` and `en-es` by `contentful.spanish_blog_posts`.

#### content_types

An object whose key-value pairs correspond to a Contentful Content Types. Each
Expand Down
149 changes: 121 additions & 28 deletions src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import path from 'path'
import querystring from 'querystring'
import contentful from 'contentful'
import pluralize from 'pluralize'
import deepcopy from 'deepcopy'
import slugify from 'underscore.string/slugify'
import underscored from 'underscore.string/underscored'
import RootsUtil from 'roots-util'
import errors from './errors'
import hosts from './hosts'
import is_plain_object from './util/is-plain-object'
import exists from './util/exists'
import isUndefined from './util/is-undefined'

let client = null // init contentful client

Expand Down Expand Up @@ -55,17 +58,17 @@ export default class RootsContentful {
* @return {Promise} an array for the sorted contentful data
*/
async setup () {
const { opts: { cache, content_types } } = this
const { opts, opts: { cache } } = this
let locals = this.roots.config.locals.contentful
// return cached locals if possible
if (cache && Object.keys(locals).length) {
return locals
}
let configuration = await configure_content(content_types)
let content = await get_all_content(configuration)
await set_urls(content)
let entries = await transform_entries(content)
let sorted = await sort_entries(entries)
let configuration = await this::configure_content(opts)
let content = await this::get_all_content(configuration)
await this::set_urls(content)
let entries = await this::transform_entries(content)
let sorted = await this::sort_entries(entries)
await this::set_locals(sorted)
await this::compile_entries(sorted)
await this::write_entries(sorted)
Expand All @@ -77,26 +80,96 @@ export default class RootsContentful {
/**
* Configures content types set in app.coffee. Sets default values if
* optional config options are missing.
* @param {Array} types - content_types set in app.coffee extension config
* @param {Object} opts - app.coffee extension config
* @return {Promise} - returns an array of configured content types
*/
async function configure_content (types) {
async function configure_content (opts) {
let types = opts.content_types
let locales = opts.locale
let locale_prefixes = opts.locales_prefix
// consumes types after adding locale and prefixes to types
let _types = []
// consumes types after adding type paths to types
// & locale prefixes to type names
let localized_types = []
let global_locale

// if locales is wildcard, fetch & set locales
if (locales === '*') {
locales = await fetch_all_locales()
}

// converts type config to an array if
// it is specified as an object
if (is_plain_object(types)) {
types = convert_types_to_array(types)
}
return types.map(async type => {
const { id, name, filters, template, path } = type
if (!id) throw new Error(errors.no_type_id)
type.filters = filters || {}
if (!name || (template && !path)) {
let content_type = await client.contentType(id)
type.name = name || pluralize(underscored(content_type.name))
if (template) {
type.path = path || (entry => `${name}/${slugify(entry[content_type.displayField])}`)

// update types to contain locale data
// and prefixes (null checks === ಠ_ಠ)
if (Array.isArray(locales)) {
for (let locale of locales) {
// if locale_prefixes is defined...
let existing_prefix = locale_prefixes != null
// set prefix as locale_prefixes[locale]
// if it exists else...
? locale_prefixes[locale]
: null
// ...set prefix as underscored locale
let prefix = existing_prefix || `${underscored(locale)}_`

for (let type of types) {
// type's locale overrides global locale
if (type.locale == null) {
let tmp = deepcopy(type)
tmp.locale = locale
tmp.prefix = prefix
_types.push(tmp)
} else if (type.prefix == null) {
// set prefix, only if it isn't set
type.prefix = prefix
_types.push(type)
}
}
}
return type
})
types = _types
} else if (typeof locales === 'string') {
global_locale = true
}

// validate type ids, set type paths
// and type names, possibly including
// type locale prefixes in type names
for (let type of types) {
if (!type.id) {
throw new Error(errors.no_type_id)
}
if (type.filters == null) {
type.filters = {}
}
if (!type.name || (type.template && !type.path)) {
let content_type = await client.contentType(type.id)
if (type.name == null) {
type.name = pluralize(underscored(content_type.name)).toLowerCase()
}
if (!isUndefined(locale_prefixes)) {
type.name = type.prefix + type.name
}
if (type.template || (locale_prefixes != null)) {
if (type.path == null) {
type.path = entry => `${type.name}/${slugify(entry[content_type.displayField])}`
}
}
} else if (!isUndefined(locale_prefixes)) {
type.name = type.prefix + type.name
}
if (exists(global_locale)) {
type.locale || (type.locale = opts.locale)
}
localized_types.push(Promise.resolve(type))
}

return await Promise.all(localized_types)
}

/**
Expand All @@ -106,10 +179,11 @@ async function configure_content (types) {
* @return {Promise} - returns an array of content types
*/
function convert_types_to_array (types) {
return Object.keys(types).reduce((results, key) => {
types = Object.keys(types).reduce((results, key) => {
results.push({ ...types[key], name: key })
return results
}, [])
return types
}

/**
Expand All @@ -119,27 +193,38 @@ function convert_types_to_array (types) {
*/
async function get_all_content (types) {
types = await Promise.all(types)
return types.map(async type => {
let content = await fetch_content(type)
for (const type of types) {
const content = await fetch_content(type)
type.content = await format_content(content)
return type
})
}
return types
}

/**
* Fetch entries for a single content type object
* @param {Object} type - content type object
* @return {Promise} - returns response from Contentful API
*/
async function fetch_content ({ id, filters }) {
async function fetch_content ({ id, filters, locale }) {
let entries = await client.entries({
...filters,
content_type: id,
include: 10
include: 10,
locale
})
return entries
}

/**
* Fetch all locales in space
* Used when `*` is used in opts.locales
* @return {Array} locales
*/
async function fetch_all_locales () {
let res = await client.space()
return res.locales.map(locale => locale.code)
}

/**
* Formats raw response from Contentful
* @param {Object} content - entries API response for a content type
Expand All @@ -160,6 +245,9 @@ function format_entry (entry) {
throw new Error(errors.sys_conflict)
}
let formatted = { ...entry, ...entry.fields }
if (formatted.sys != null) {
delete formatted.sys
}
delete formatted.fields
return formatted
}
Expand Down Expand Up @@ -194,10 +282,15 @@ async function set_urls (types) {
* @return {Promise} - promise for when complete
*/
async function set_locals (types) {
let contentful = this.roots.config.locals.contentful
types = await Promise.all(types)
return types.map(({ name, content }) => {
this.roots.config.locals.contentful[name] = content
return content
if (contentful[name]) {
contentful[name].push(content[0])
} else {
contentful[name] = content
}
return contentful[name]
})
}

Expand Down
3 changes: 3 additions & 0 deletions src/util/exists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function exists (thing) {
return typeof thing !== 'undefined' && thing !== null
}
3 changes: 3 additions & 0 deletions src/util/is-undefined.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function isUndefined (thing) {
return thing === void 0
}

0 comments on commit dca5c55

Please sign in to comment.