SolidusContent
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:
-
slug
: the slug of the content-entry -
type
: the name of the content-type -
provider
: the name of the content-provider -
options
: the entry options -
type_options
: the content-type options
The output
of the content-provider is the input
hash augmented with the
following keys:
-
data
: the content itself -
provider_client
: (optional) the client of the external service -
provider_entry
: (optional) the object retrieved from the external service
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