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.
Add solidus_content to your Gemfile:
gem 'solidus_content'
Bundle your dependencies and run the installation generator:
bundle
bin/rails generate solidus_content:install
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!"}
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>
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).
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
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
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!" }
)
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
.
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
.
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.
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.
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.
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.
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-entrytype
: the name of the content-typeprovider
: the name of the content-provideroptions
: the entry optionstype_options
: the content-type options
The output
of the content-provider is the input
hash augmented with the
following keys:
data
: the content itselfprovider_client
: (optional) the client of the external serviceprovider_entry
: (optional) the object retrieved from the external service
In both the input and output all keys should be symbolized.
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"
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)
}
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)
.
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'
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
Your new extension version can be released using gem-release
like this:
bundle exec gem bump -v VERSION --tag --push --remote origin && gem release
Copyright (c) 2020 Nebulab, released under the New BSD License