solidusio-contrib / solidus_content

BSD 3-Clause "New" or "Revised" License
8 stars 9 forks source link

SolidusContent

CircleCI codecov

An extremely modular and extensible CMS for Solidus.

It can consume content from different sources such as Contentful, DatoCMS, Prismic.io or custom JSON, YAML and raw formats. It makes it super easy to render and customize any content.

Installation

Add solidus_content to your Gemfile:

gem 'solidus_content'

Bundle your dependencies and run the installation generator:

bundle
bin/rails generate solidus_content:install

Usage

Create an entry type for the home page:

home_entry_type = SolidusContent::EntryType.create!(
  name: :home,
  provider_name: :json,
  options: { path: 'data/home' }
)

Create a default entry for the home page:

home = SolidusContent::Entry.create!(
  entry_type: home_entry_type,
  slug: :default,
)

And then write a file inside your app root under data/home/default.json:

{"title":"Hello World!"}

Within an existing view

Use the content inside an existing view, e.g. app/views/spree/home/index.html.erb:

<% data = SolidusContent::Entry.data_for(:home, 'default') %>

<h1><%= data[:title] %></h1>

With the default route

SolidusContent will add a default route that starts with /c/, by adding a view inside app/views/spree/solidus_content/ with the name of the entry type you'll be able to render your content.

E.g. app/views/spree/solidus_content/home.html.erb:

<h1><%= @entry.data[:title] %></h1>

Then, visit /c/home/default or even just /c/home (when the content slug is "default" it can be omitted).

With a custom route

You can also define a custom route in your Application routes file and use the SolidusContent controller to render your content from a dedicated view:

# config/routes.rb
Rails.application.routes.draw do
  # Will render app/views/spree/solidus_content/home.html.erb
  root to: 'spree/solidus_content#show', type: :home, id: :default

  # Will render app/views/spree/solidus_content/info.html.erb
  get "privacy", to: 'spree/solidus_content#show', type: :info, id: :privacy
  get "legal", to: 'spree/solidus_content#show', type: :info, id: :legal

  # Will render app/views/spree/solidus_content/post.html.erb
  get "blog/:id", to: 'spree/solidus_content#show', type: :post

  mount Spree::Core::Engine, at: '/'
end

Configuration

Configure SolidusContent in an initializer:

# config/initializers/solidus_content.rb

SolidusContent.configure do |config|
  # Your configuration goes here, please refer to the examples provided in the
  # initializer generated by `bin/rails g solidus_content:install`
end

Available Content Providers

RAW

This is the most simple provider, its data will come directly from the entry options.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'raw',
)
entry = SolidusContent::Entry.create(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { title: "Hello World!", body: "My first post!" }
)

JSON

Will fetch the data from a JSON file within the directory specified by the path entry-type option and with a basename corresponding to the entry slug.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'json',
  options: { path: 'data/posts' }
)
entry = SolidusContent::Entry.create(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
)
// [RAILS_ROOT]/data/posts/2020-03-27-hello-world.json
{"title": "Hello World!", "body": "My first post!"}

NOTE: Absolute paths are taken as they are and won't be joined to Rails.root.

YAML

Will fetch the data from a YAML file within the directory specified by the path entry-type option and with a basename corresponding to the entry slug.

If there isn't a file with the yml extension, the yaml extension will be tried.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'yaml',
  options: { path: 'data/posts' }
)
entry = SolidusContent::Entry.create(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
)
# [RAILS_ROOT]/data/posts/2020-03-27-hello-world.yml

title: Hello World!
body: My first post!

NOTE: Absolute paths are taken as they are and won't be joined to Rails.root.

Solidus static content

To retrieve the page we have to pass the page slug to the entry options. If the page slug is the same of the entry one, we can avoid passing the options.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'solidus_static_content'
)

entry = SolidusContent::Entry.create!(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { slug: 'XXX' } # Can be omitted if the page slug is the same of the entry
)

_Be sure to have added gem "solidus_static_content" to your Gemfile._

Contentful

To fetch the data we have to create a connection with Contentful passing the contentful_space_id and the contentful_access_token to the entry-type options.

Will fetch the data from Contentful passing the entry_id entry option.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'contentful',
  options: {
    contentful_space_id: 'XXX',
    contentful_access_token: 'XXX'
  }
)

entry = SolidusContent::Entry.create!(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { entry_id: 'XXX' }
)

Be sure to have added gem "contentful" to your Gemfile.

DatoCMS

To fetch the data we have to create a connection with DatoCMS passing the api_token to the entry-type options.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'datocms',
  options: {
    api_token: 'XXX'
  }
)

If we need to work on a sandbox environment, add the environment option:

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'datocms',
  options: {
    api_token: 'XXX',
    environment: 'my-sandbox'
  }
)

Will fetch the data from DatoCMS passing the item_id entry option:

entry = SolidusContent::Entry.create!(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { item_id: 'XXX', version: 'published' }
)

If we want to retrieve the latest available version of the record instead of the currently published version, remove the version option:

entry = SolidusContent::Entry.create!(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { item_id: 'XXX' }
)

Be sure to have added gem "dato" to your Gemfile.

Prismic

To fetch the data we have to create a connection with Prismic passing the api_entry_point to the entry-type options.

If the repository is private, you have to also pass the api_token to the entry-type options.

Will fetch the data from Prismic passing the id entry option.

posts = SolidusContent::EntryType.create(
  name: 'posts',
  provider_name: 'prismic',
  options: {
    api_entry_point: 'XXX',
    api_token: 'XXX' # Only if the repository is private
  }
)

entry = SolidusContent::Entry.create!(
  slug: '2020-03-27-hello-world',
  entry_type: posts,
  options: { id: 'XXX' }
)

Be sure to have added gem "prismic.io" to your Gemfile.

Registering a content provider

To register a content-provider, add a callable to the configuration under the name you prefer. The

SolidusContent.config.register_provider :json, ->(input) {
  dir = Rails.root.join(input.dig(:type_options, :path))
  file = dir.join(input[:slug] + '.json')
  data = JSON.parse(file.read, symbolize_names: true)

  input.merge(data: data)
}

The input passed to the content-provider will have the following keys:

The output of the content-provider is the input hash augmented with the following keys:

In both the input and output all keys should be symbolized.

Connecting Webhooks

Many content providers such as Contentful or Prismic can send payloads via webhooks when content changes, those can be very useful in a number of ways.

We suggest using the solidus_webhooks extension to get the most out of solidus_content, let's see some examples.

Add this to your Gemfile:

gem "solidus_webhooks"

Using Webhooks to Auto-Create entries

In this example we setup a webhook that will create or update Contentful entries whenever they're changed or created.

# config/initializers/webhooks.rb

SolidusWebhooks.config.register_webhook_handler :contentful, -> payload {
  next unless payload.dig(:sys, :Type) == "Entry"
  entry_type = SolidusContent::EntryType.find_or_create_by(
    name: payload.dig(:sys, :ContentType, :sys, :id),
    provider_name: :raw
  )
  entry = entry_type.entries.find_or_initialize_by(slug: payload.dig(:sys, :id))
  entry.options = payload.fetch(:fields)
}

Using Webhooks to expire caches

When caching the content of app/views/spree/home/index.html.erb as in this example:

<% cache(@entry) do %>
  <h1><%= @entry.data[:title] %></h1>
<% end %>

You may want to setup a webhook that will touch the entry every time it's modified:

# config/initializers/webhooks.rb

SolidusWebhooks.config.register_webhook_handler :prismic, -> payload {
  prismic_entry_types = SolidusContent::EntryType.where(provider_name: :prismic)

  # Prismic doesn't give much informations about which entries have been changed,
  # so we're touching them all.
  SolidusContent::Entry.where(entry_type: prismic_entry_types).touch_all
}

NOTE: touch_all was introduced in Rails 6, for earlier versions use find_each(&:touch).

Development

Testing the extension

First bundle your dependencies, then run bin/rake. bin/rake will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using bin/rake extension:test_app.

bundle
bin/rake

To run Rubocop static code analysis run

bundle exec rubocop

When testing your application's integration with this extension you may use its factories. Simply add this require statement to your spec_helper:

require 'solidus_content/factories'

Running the sandbox

To run this extension in a sandboxed Solidus application, you can run bin/sandbox. The path for the sandbox app is ./sandbox and bin/rails will forward any Rails commands to sandbox/bin/rails.

Here's an example:

$ bin/rails server
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop

Releasing new versions

Your new extension version can be released using gem-release like this:

bundle exec gem bump -v VERSION --tag --push --remote origin && gem release

License

Copyright (c) 2020 Nebulab, released under the New BSD License