node package manager

contentful-aggregator

contentful-aggregator

Configures content type entries for use in static site generators. Supports linked entries and localization. Built on top of Contentful.js

​⚠️ Please Note: this project is in early stages and follows Readme-Driven Development practices, so beware - not all the functionality below works just yet and the documentation is subject to change at a rapid rate. Consider this project as unstable for now.

🔋 Status

GitHub licenseGitHub releasenpmnpmnode

Build Statuscodecov.iobitHound Overall ScorebitHound Code

Dependency StatusdevDependency StatusbitHound DependenciesbitHound Dev Dependencies

semantic-releaseCommitizen friendlyjs-standard-stylegreenkeeper

​☝️ Please click on any of these to see more information/context

📖 Table of Contents

⏬ Installation

Requirements

  • Node.js v4.0.0 or higher
  • NPM (v3.0.0+ highly recommended) (this comes with Node.js)

Instructions

contentful-aggregator is a Node module. So, as long as you have Node.js and NPM, installing contentful-aggregator is as simple as running this in a terminal at the root of your project:

$ npm install contentful-aggregator --save

📚 Usage

contentful-aggregator uses a Future-based syntax. If you are not familiar with Futures, please read this short primer.

contentful-aggregator exposes a single curried function that receives two arguments. The first argument is your Contentful API Key, and the second argument is the ID of the space you want to obtain your data from.

import ca from 'contentful-aggregator'
 
const space = ca('apiToken', 'space')

Since this function is curried, you may omit the second argument. Doing so will return a function that has the apiToken, but expects the space ID.

const client = ca('apiToken')
const myPortfolioSpace = client('portfolio site space id')
const myBlogSpace = client('blog site space id')

This will return an instance of the Space class.

API

Util

import util from 'contentful-aggregator/util'

util.pluralize()

pluralize :: String -> String

Returns the plural form of the String str

util.pluralize('octopus') // -> 'octopi' 
util.pluralize('book') // -> 'books' 

util.underscored()

underscored :: String -> String

Returns the underscored form of the String str

util.underscored('foo bar') // -> 'foo_bar' 
util.underscored('fooBar') // -> 'foo_bar' 

util.slugify()

slugify :: String -> String

Returns the slugified form of the String str

util.slugify('foo bar') // -> 'foo-bar' 
util.slugify('foo_bar') // -> 'foo-bar 

Space()

An instance of this class is returned once the contentful-aggregator module is fully setup.

Space::getClient()

getClient :: -> Object

Returns a direct reference to the Contentful.js client, in case you need it to do anything that contentful-aggregator doesn't already offer out of the box (psst... send us a pull request! 😄)

const client = space.getClient()
 
client.entries(...)... 
client.contentType(...)... // etc 

Space::fetchInfo()

fetchInfo :: -> Future Object

Asynchronously fetches information about the current space from Contentful's content delivery API.

space.fetchInfo().fork(null, (info) => {
  return info === {
    sys: {
      type: 'Space',
        id: 'cfexampleapi'
    },
    name: 'Contentful Example API',
    locales: [
      { code: 'en-US', name: 'English' },
      { code: 'tlh', name: 'Klingon' }
    ]
  }
})

Space::fetchLocales()

fetchLocales :: -> Future [Object]

Asynchronously fetches an array of the locales you have configured for your space on Contentful.

space.fetchLocales().fork(null, (locales) => {
  return locales === [
   { code: 'en-US', name: 'English' },
    { code: 'tlh', name: 'Klingon' }
  ]
})

Space::fetchLocaleCodes()

fetchLocaleCodes :: -> Future [String]

Asynchronously fetches an array of the locale codes for every locale you have configured for your space on Contentful.

space.fetchLocaleCodes().fork(null, (localeCodes) => {
  return localeCodes === ['en-US', 'tlh']
}

Space::fetchContentType()

fetchContentType :: String -> [ContentType]

Asynchronously fetches information about a single content type with an id property that matches id from Contentful. Resolves to an instance of ContentType

space.fetchContentType('content type id').map((contentType) => {
  // contentType === instanceof ContentType 
  contentType.setName('blog_posts')
})

ContentType()

ContentType::fetchEntries()

fetchEntries :: Object -> Future [Entry]

Asynchronously fetch all entries that match search parameter opts and belong to this ContentType. Resolves to an array of Entry instances

blogPosts.fetchEntries({
  include: 3,
  'fields.title[exists]': true
}).fork(posts => {
  console.log(posts[0].path())
})

ContentType::path()

setEntryPath :: (Object -> any) -> [Entry] -> [Entry]

Convenience method for setting the path() method on each entry that belongs to this ContentType. Returns an array of entries with the newly added/updated path() method.

blogPosts.fetchEntries()
  .map(blogPosts.path(post => `posts/${post.name}.html`))

Entry()

[WIP]


🎓 Required Knowledge

A Short Primer on Futures

A careful design decision was made to use Futures in place of Promises for asynchronous control flow in contentful-aggregator, and as such, a lot of the documented methods will return a Future. So, for those not familiar with them, here's a short primer.

Futures, otherwise known as Tasks, are similar to Promises, but are a more pure and functional alternative. This means they are easily composed. They look like this:

// fetch :: String -> Future String 
const fetch = (url) => new Future((resolve, reject) => {
  const request = new XMLHttpRequest()
  request.addEventListener('load', resolve, false)
  request.addEventListener('error', reject, false)
  request.addEventListener('abort', reject, false)
  request.open('get', url, true)
  request.send()
})

Like Promises, Futures need to be unwrapped if you want to access their value. The difference from Promises, however, is that instead of using .then() to unwrap a Future, you use either one of .chain() or .map().

.chain() creates a dependent Future. Here, we create a Future fetchJSON() that depends on the result of sequencing fetch() (from above) and then parseJSON():

// parseJSON :: String -> Future Object 
const parseJSON = (str) => new Future((resolve, reject) => {
  try {
    resolve(JSON.parse(str))
  } catch (error) {
    reject(error)
  }
})
 
// fetchJSON :: String -> Future Object 
const fetchJSON = fetch.chain(parseJSON)

We can use .map() similarly to unwrap the value and run any function on it. So, if we wanted to only get the items property of the JSON response, that would look like this:

// fetchItems :: Future Object -> Future [Object] 
const fetchItems = fetchJSON.map(json => json.items)

If we wanted to filter out only the items that are new, then we can do that too:

// getNewItems :: Future [Object] -> Future [Object] 
const getNewItems = fetchItems.map(items => items.filter(item => item.new))

An important aspect of Futures that's definitely worth noting is that, unlike Promises, which execute as soon as they are created, none of the above Futures have executed yet or sent any requests over the wire. This is because a Future will only execute once you have explicitly made a call to .fork(). This reduces side-effects and contains them to a single place. Like .then(), .fork() accepts an onResolved as well as an onRejected handler, but note the difference in order:

getNewItems('/products.json').fork(console.error, console.log) // -> onRejected, onResolved 

One other interesting aspect is the ability to easily pipe logic - say we need to hit a JSON endpoint, grab a url property, and then send another request to that url to grab it's HTML. Simple:

// fetchUrlFromUrl :: Future Object -> Future String 
const fetchUrlFromUrl = fetchJSON.map(json => json.url).chain(fetch)

The biggest strength, especially in the context of contentful-aggregator, is the ability for Futures to be easily cached. This means you can take a Future that executes an HTTP request, but wrap it in another Future that will return any subsequent calls to .fork() from a cache:

// getCachedNewItems :: Future [Object] -> Future [Object] 
const getCachedNewItems = Future.cache(getNewItems)
 
// makes an HTTP request 
getCachedNewItems('/products.json').fork(console.error, console.log)
 
// doesn't make an HTTP request, just returns from cache: 
getCachedNewItems('/products.json').fork(console.error, console.log)

Futures in contentful-aggregator are based on the Ramda-Fantasy library which include other methods like ap, of, chainReject, biMap, and reject.