No commit activity in last 3 years
No release in over 3 years
Contentful api wrapper which caches responses from contentful
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

 Project Readme

ContentfulRedis

A lightweight read-only contentful API wrapper which caches your responses in Redis.

Features

  • Lightweight easy to configure ruby contentful integration.
  • Faster load times due to having a Redis cache.
  • All content models responses are cached.
  • Webhooks update
  • Multiple space support
  • Preview and production API support on a single environment

WIP

  • logger
  • Experiment Redis size optimization
  • auto clean up of dead Redis keys
  • code clean up

ContentfulRedis also supports multiple API endpoints(preview and published) within a single application.

Installation

Add this line to your application's Gemfile:

gem 'redis-store' # optional
gem 'contentful_redis'

And then execute:

$ bundle

Or install it yourself as:

$ gem install contentful_redis

Configuration

Heres a default example, however, I will go over all of the individual configurations options below

# config/initializers/contentful_redis.rb
ContentfulRedis.configure do |config|
  config.default_env = :preview # unless production env
  config.model_scope = 'Contentful' # models live in a Contentful module
  config.logging = true

  config.spaces = { 
    test_space: {
      id: 'xxxx',
      access_token: 'xxxx',
      preview_access_token: 'xxxx'
    }
  }

  config.redis = Redis::Store.new(
    host: (ENV['REDIS_HOST']) || 'localhost',
    port: 6379,
    db: 1,
    namespace: 'contentful'
  )

Spaces (required)

Contentful Redis supports multiple space configurations with your first space being the default

# config/initializers/contentful_redis.rb
ContentfulRedis.configure do |config|
  config.spaces = { 
    test_space: {
      id: 'xxxx',
      access_token: 'xxxx',
      preview_access_token: 'xxxx'
    },

    test_space_2: {
      id: 'xxxy',
      access_token: 'xxxx',
      preview_access_token: 'xxxx'
    }
  }

To use a different space for a model override the classes #space method

# app/models/my_model.rb
class MyModel < ContentfulRedis::ModelBase
  
  # override default space
  def self.space
    ContentfulRedis.configuration.spaces[:test_space_2]
  end
end

Redis (required)

There are various ways you can integrate with Redis. I suggest using redis-store unless your application already has Redis adapter installed. I recommend having a separate Redis database for all of your contentful data so that you can isolate your application Redis from your content.

# config/initializers/contentful_redis.rb
ContentfulRedis.configure do |config|
  config.redis = Redis::Store.new(
    host: (ENV['REDIS_HOST']) || 'localhost',
    port: 6379,
    db: 1,
    namespace: 'contentful'
  )
end

Default env

If unset the default call is to the :published data. however, setting default_env to :preview will request to the preview API. The Find methods can have an additional argument to force the non-default endpoint.

# config/initializers/contentful_redis.rb
ContentfulRedis.configure do |config|
  # if unset defaults to :published
  config.default_env = :preview
end

Model scope

Set the scope for where your models live.

# config/initializers/contentful_redis.rb
ContentfulRedis.configure do |config|
  config.model_scope = 'Contentful'
end

# app/models/contentful/page.rb
module Contentful
  class Page < ContentfulRedis::ModelBase

  end
end

Models

All content models will need to be defined, prior to integration especially when using references. The example model we are going to define has a slug(input field) and a body(references other content models)

# app/models/page.rb
class Page < ContentfulRedis::ModelBase
  # allows the field to be queried from
  define_searchable_fields :slug

  # Set default readers which can return nil
  attr_reader: :slug
  
  # define your desired return types manually
  def body
    @body || []
  end
end

Querying

All content models are found by their contentful ID. Contentful Redis only stores only one cache of the content model This Redis key is generated and is unique to a content model, space and endpoint.

In these examples within my application I have created a class called Contentful::Page

  Contentful::Page.find('<contentful_uid>')

Contentful Redis does not store a duplicate object from searchable attributes, Instead, it builds a glossary of searchable attributes mapping to their content models ids. These attributes are defined in the class declaration as define_searchable_fields :slug

  Contentful::Page.find_by(slug: 'about-us') 

Deleting

An entry is done by calling destroy on a ContentfulRedis model object or destroy by passing id. This will delete all the redis keys, find and search keys for the entry.

In these examples within my application I have created a class called Contentful::Page

  Contentful::Page.destroy('<contentful_uid>')

or

  page = Contentful::Page.find('<contentful_uid>')
  page.destroy

Query optimization

There are three optimizations that can be made on a query. Due to the nature of content trees and Contentful's references, we need to be able to control which references to follow.

In these examples within my application I have created a class called Contentful::Page

Only

Fetch all selected reference(s)

  # Content model which has child references called descendants
  Contentful::Page.find('<contentful_uid>', only: :descendants)
  Contentful::Page.find_by(slug: '<contentful_slug>', options: { only: :descendants } )

  # Supports Arrays as well
  Contentful::Page.find('<contentful_uid>', only: [:descendants])
  Contentful::Page.find_by(slug: '<contentful_slug>', options: { only: [:descendants] } )

Except

Fetch all references except the targeted field(s)

  # Content model which has many references including child references called descendants

  Contentful::Page.find('<contentful_uid>', except: :descendants)
  Contentful::Page.find_by(slug: <contentful_slug>, options: { except: :descendants } )

  # Supports Arrays as well
  Contentful::Page.find('<contentful_uid>', except: [:descendants])
  Contentful::Page.find_by(slug: <contentful_slug>, options: { except: [:descendants] } )

Depth

Limit the reference traversal depth. The value is the amount of edges which to travese down.

  Contentful::Page.find(<contentful_uid>, depth: 1 )
  Contentful::Page.find_by(slug: <contentful_slug>, options: { depth: 1 })

Content model overriding

Classes should match their content model name, however, if they don't you can override the classes #name method.

# app/models/page.rb
class Page < ContentfulRedis::ModelBase

  # Overwrite to match contentful model using ruby class syntax
  def self.name
    'NameThatMatchesContentfulModel'
  end
end

Webhooks

Instead of creating rails specific implementation it is up to the developers to create your controllers and manage your webhook into your applications. See the Contentful webhooks docs creating your own

These are the setings which we have found to work the best We do need need any of the assests as they are handled by contentfuls image server.

Staging / UAT Update Settings

Create Save Autosave Archive Unarchive Publish Unpublish Delete
Content Type ✔︎ ✔︎
Entry ✔︎ ✔︎ ✔︎ ✔︎
Asset

Staging / UAT Update Delete

Create Save Autosave Archive Unarchive Publish Unpublish Delete
Content Type ✔︎
Entry ✔︎ ✔︎
Asset

Production Update Settings

Create Save Autosave Archive Unarchive Publish Unpublish Delete
Content Type ✔︎
Entry ✔︎
Asset

Production Delete Settings

Create Save Autosave Archive Unarchive Publish Unpublish Delete
Content Type ✔︎ ✔︎
Entry ✔︎ ✔︎ ✔︎
Asset

Required Contentful webhooks to update the Redis cache are:

{
  "id": "{ /payload/sys/id }",
  "environment": "{ /payload/sys/environment/sys/id }",
  "model": "{ /payload/sys/contentType/sys/id }"
}

When pushing text attributes make sure you are using the correct language endpoint.

{
  "title": "{ /payload/fields/title/en-US }",
  "slug": "{ /payload/fields/slug/en-US }",
}

Webhook Controllers

Rails

# app/controllers/contentful/webhook_controller.rb
module Contentful
  class WebhookController < ApplicationController
    # before_action :some_auth_layer

    def update
      payload = JSON.parse request.raw_post
      
      contentful_model = ContentfulRedis::ClassFinder.search(payload['model'])
      contentful_model.update(payload['id'])

      render json: { status: :ok }
    end

    def delete
      payload = JSON.parse request.raw_post
      
      contentful_model = ContentfulRedis::ClassFinder.search(payload['model'])
      contentful_model.destroy(payload['id'])

      render json: { status: :ok }
    end
  end
end

# config/routes
#...
namespace :contentful do
  resource 'webhooks', only: :update, :delete
end
# ...

Other

Feel free to create a PR for other ruby frameworks :)

Content Seeding

Seeding the data is a great way to get started in building your content models, there is a couple of ways this can be done.

Create a service object inside your application and get it to fetch the root pages of your content tree by their ID. The find method will build your Redis cache as well as link your content models with their searchable fields.

# app/services/seed_content.rb
class SeedContent
  # trigger a cascading content model seeding process
  def call
    ['xxContentfulModelIdxx'].each do |page|
      Contentful::Page.find(page)
    end
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a Git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/contentful-redis.

License

The gem is available as open source under the terms of the GNU General Public License