Start up your website with manageable content astutely using Rails and prismic.io

Written by Rudy Rigot, Developer Evangelist in Engineering

A prismic.io content repository comes with a REST API from which your Rails app can query your content, and a writing-room where you can edit and manage your content.

Lately, I've been enjoying coding Rails apps using prismic.io, because I feel like they agree on quite a few values. Most importantly, they both believe that doing things right can be done very efficiently, so they appreciate not wasting each other's time (and incidently, mine either!) Also, Rails having been an early fighter on the web-oriented architecture front, they both believe in separation of concerns, so they enjoy constantly getting each other's back. Pretty nice!

So I thought it made sense to sit down a bit and talk about how a typical project based on Rails and prismic.io usually goes, step by step, to get you started on the right foot.

Introducing our project today: the WorldChanger startup's new website

Meet WorldChanger. Just like any world-improving startup, WorldChanger needs the best of both worlds: the flexibility to impact its audience with a carefully handcrafted design and finely optimized content, and the ability to get it done in a lean and quick way. Time-to-market is essential, and this goes for their website too!

They quickly realized that any design they decide to show the world will go way beyond what a CMS can do out-of-the-box, so they decided to build it with an MVC framework, and they chose to use Rails. Manageable content being essential for their copywriting optimizations, they decided to integrate it with prismic.io. Let's go!

Creating the new content repository

This is one of the cool things about prismic.io: getting started is fast and painless. From the prismic.io dashboard, just create a new blank repository, and... voila! Your brand new writing-room and your content query API are both up and running. Next...

Creating document types

Deciding on document types

First, you need to decide what your website will say, and therefore the types of documents you will work with. After careful thinking, WorldChanger chose to manage their marketing arguments (which they use in different forms on their homepage and on their product tour page), their pricing plans, their blog posts, their authors (in which the staff members will be tagged as "staff"), and questions for their FAQs. They will also introduce a document type for their site-level articles (the "about" page, the introduction of the "pricing" page, etc.), and another one for the homepage, which is a very specific page.

Something new to think about with this new way of doing things: you used to have to get your design entirely done before anything else, then decide on your document types depending on what your CMS would let you do. Now you can actually get started on the document types before design even gets started at all, and our friends over at WorldChanger could be writing content before the designer even had time to open Photoshop!

With any luck, you might even be able to provide the designer with actual, non-lorem-ipsum, content to play with for his design!

Composing document masks

Document types in prismic.io are expressed with document masks; they're called this way because that's indeed what they are, masks to see and edit documents in the back-office, rather than a rigid way to define content. Basically, you get to describe documents fragment by fragment using the JSON syntax, whether it is an image fragment, a date fragment, a color fragment, a number fragment, a link fragment, etc. or the very special "structured text" fragment, that stores formatted content you can retrieve as reusable JSON.

If you want to know how writing document masks gets done, you can read the full documentation over there, that takes just about a few minutes to get through.

For instance, the author document type could look something like this for our WorldChanger website:

{
"Main" : {
"fullname" : {
"type" : "StructuredText",
"fieldset" : "Full name",
"config" : {
"single" : "heading1"
}
},
"first_name" : {
"fieldset" : "Identity",
"type" : "Text",
"config" : {
"label" : "First name"
}
},
"middle_name" : {
"type" : "Text",
"config" : {
"label" : "Middle name"
}
},
"surname" : {
"type" : "Text",
"config" : {
"label" : "Surname"
}
},
"title" : {
"type" : "Text",
"config" : {
"label" : "Title"
}
},
"company" : {
"type" : "Text",
"config" : {
"label" : "Company"
}
},
"level" : {
"type" : "Number",
"config" : {
"label" : "Level in company"
}
},
"description" : {
"type" : "StructuredText",
"fieldset" : "Description",
"config" : {
"minHeight" : 50.0,
"placeholder" : "Something fun and no more than 2 sentences long"
}
},
"photo" : {
"type" : "Image",
"fieldset" : "Photo",
"config" : {
"thumbnails" : [ {
"name" : "large",
"width" : 200.0,
"height" : 300.0
}, {
"name" : "icon",
"width" : 100.0,
"height" : 100.0
} ]
}
}
}
}

It will look like this in the writing-room:

And here's how this document looks like as returned by prismic.io's API (although, you'll see we'll be using a Ruby native wrapper kit in the project, which will make it even more trivial):

{
"id": "UpUwTQEAAMxb1wvB",
"type": "author",
"href": "https://worldchanger.prismic.io/api/documents/search?ref=UxAF1AEAAFcRi4-B&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22UpUwTQEAAMxb1wvB%22%29+%5D%5D",
"tags": [ "staff" ],
"slugs": [ "jenny-mcarthur" ],
"data": {
"author": {
"fullname": {
"type": "StructuredText",
"value": [
{
"type": "heading1",
"text": "Jenny McArthur",
"spans": [ ]
}
]
},
"first_name": {
"type": "Text",
"value": "Jenny"
},
"middle_name": {
"type": "Text",
"value": "Marie"
},
"surname": {
"type": "Text",
"value": "McArthur"
},
"title": {
"type": "Text",
"value": "Happiness engineer"
},
"company": {
"type": "Text",
"value": "WorldChanger"
},
"level": {
"type": "Number",
"value": 3
},
"description": {
"type": "StructuredText",
"value": [
{
"type": "paragraph",
"text": "Jenny daily makes your life better, and is the safety net you can find in your new life with WorldChanger. You can reach her at all time to get help and advice around how to change you world for the better.",
"spans": [ ]
}
]
},
"photo": {
"type": "Image",
"value": {
"main": {
"url": "https://prismic-io.s3.amazonaws.com/worldchanger/7451632d6ed761d20996370619baedd65ac8a102.jpg",
"alt": "",
"copyright": "",
"dimensions": {
"width": 3744,
"height": 5616
}
},
"views": {
"large": {
"url": "https://prismic-io.s3.amazonaws.com/worldchanger/484844561f6832e298c8b3f40dbc1d325dc70623.jpg",
"alt": "",
"copyright": "",
"dimensions": {
"width": 200,
"height": 300
}
},
"icon": {
"url": "https://prismic-io.s3.amazonaws.com/worldchanger/ea1029ddddbd1bfaecae82774aece139af24aee8.jpg",
"alt": "",
"copyright": "",
"dimensions": {
"width": 100,
"height": 100
}
}
}
}
}
}
}
}

Design and front-end development

Here, it's all about your skills! Use all the creativity and tooling you want, both Rails and prismic.io are meant to adapt to them.

Here's another new thing to think about: you can either decide to start with the back-end development (turning prismic.io content into HTML code), or the front-end development (turning designs into CSS and JS code). Actually, you can even do both at the same time, if back-end developers and front-end developers agree beforehand on what the HTML code will look like.

Getting your Ruby code started

Bootstrapping your Rails website

Start with this:

git clone https://github.com/prismicio/ruby-rails-starter.git

You just cloned prismic.io's starter project, which includes the prismic.io Ruby kit and quite a few helper functions to get started. It works out of the box, but it simply displays all the documents in the repository with no styling. It was basically built to be iterated upon.

To boot it up, go to the directory, and run "bundle install" to install all Ruby dependencies, and then "rails server". Your brand new website is now live on your machine!

Configuring your application

It all happens in config/prismic.yml. By default, the starter project gets its content from our Les Bonnes Choses example repository, and its prismic.yml file looks like this:

url: https://lesbonneschoses.prismic.io/api
# If specified this token is used for all "guest" requests
# token: ""
# OAuth2 configuration
# client_id: ""
# client_secret: ""

If you set your API access to "public" in your repository settings, all you need to do to get set is change the "url" field into your own API endpoint, so that your project connects to your own content repository. Restart your Rails server, and your local project now displays your documents!

You don't need to configure anything else for your content website to be used in production, which is sweet! However, you should probably set the secured access to your repository from you app right away, so that you don't need to think about doing it later. It will be useful in two situations:

  • if you set your API access to "private" in your repository settings, forbidding other people to access your document through your API.
  • and to preview future content releases. This is a pretty cool feature: prismic.io allows you to manage multiple content releases, and to securely preview each of them exactly as they will be seen in your application. Whether you set your API access to "public" or "private", this only applies to your "master ref" (your content that is currently live); your future content releases will always be private, so that people who don't have the proper rights in your prismic.io repository don't see what you mean to publish in the future. Icing on the cake: with the proper configuration, this whole thing works out-of-the-box in the official starter project!

To set this up, simply go to your "Applications" panel in your repository settings, and create a new application, granting it rights to access the master+releases. As a result, prismic.io generates a client ID and a client secret. (You don't need to set a callback URL as long as you're only using localhost)

Once you copy-pasted them in your prismic.yml file, it should look like this:

url: https://worldchanger.prismic.io/api
# If specified this token is used for all "guest" requests
# token: ""
# OAuth2 configuration
client_id: "UoVRQ0nM08sENOPw"
client_secret: "5b9350aebc02a575c9e3881e9e72d568"

Don't forget to restart your Rails server, so your new configuration is taken into account!

Developing the "pricing" page

WorldChanger need for the pricing page to display:

  • a title and a catcher, which are stored in the "pricing" document of type "site-level article"
  • all of the pricing plans
  • some of the FAQ questions, only those that are tagged "pricing"!

Route

Simple: since there is only one URL to get this kind of webpage, you simply can add this line to the config/routes.rb file:

get '/pricing' => 'application#pricing'

Controller

The site-level article dedicated to the pricing page (carrying its title and catcher) is a very unique document in the repository; in fact, it is so unique, that you may be thinking to query it from its ID, but are concerned that this is not a very clean way to do it. That is a legitimate concern to have! This is what prismic.io's "bookmarking" system is here for, allowing you to assign bookmark names to specific documents, which you can use to retrieve them in the API. To set a new bookmark, you can do it through the "Bookmark" panel in your repository's settings.

To retrieve your bookmarked document, you can run :

@document = PrismicService.get_document(api.bookmark("pricing"), api, ref)

Some explanations:

  • Your Rails starter project comes with a PrismicService.get_document helper method out-of-the-box, to simply retrieve a document from its ID.
  • In your Rails starter kit, your controller already carries an "api" function returning the Prismic::API object depending on what you set in your prismic.yml file; this is why you can directly get your bookmarked document's ID just like that: api.bookmark("bookmark_name").
  • Also, ref is the ref being currently queried through the whole page. A ref is a point in the repository's history, in the past, present, or future; for instance, we call the "master ref" the one to query to get all the documents that are currently live (so, this is the one queried when your users view content). It is stored in an instance variable, because provided you have the proper rights, you may want your ref to be a ref in the future for the whole page you're viewing, in order for you to preview an upcoming content release.

To retrieve the questions that are tagged "pricing", you will need to design your content query, and the best tool to do that is your prismic.io repository's API browser, using the documentation about query predicates for reference. Here, we'll need two predicates: one stating that the queried documents must be of the type "faq", and one stating that the documents must contain the tag "pricing".

Once we're happy with our predicates and what they return, we can copy-paste them into our code, as a query:

@questions = api.form("everything").query('[[:d = at(document.type, "faq")][:d = any(document.tags, ["pricing"])]]').submit(ref)

Here, we query the form "everything" (which contains all the documents in the repository, and exists in all prismic.io repository APIs), and we add predicates to our query, which we pass in the query method.

Finally, to get all the pricing plans (the documents whose type is "pricing"), you can go like this:

@plans = api.form("plans").orderings("[my.pricing.price]").submit(ref)

Sure, we could have called the form "everything" again, and added a predicate filtering on the type; but it turns out we created a collection in the repository which is called "plans", and it already filters only the documents whose type is "pricing". This makes our life easier here, as all collections in the writing-room can be called as forms in the API, so this is what we're doing here. (Note that it is also possible to call any form, and add extra predicates with the query method to filter upon it.)

Also, we could have just written api.form("plans").submit(ref), but the order of the documents is important here, as we want those pricing plans from the cheapest to the most expensive. This is why we're adding an ordering statement in the chained query, declaring the fragment on which the ordering should be performed as [my.pricing.price]. You can read more about orderings in prismic.io's documentation.

Now, in just 3 lines our controller action is done, and we are passing 3 variables to the view: @document, @questions and @plans.

View

Let's consider that you got your layout right in app/views/layouts/application.html.erb, and can now focus on the view that is specific to the pricing page.

So, first things first, you want to display the page's title; but you don't necessarily remember the exact type of your document, and the ID of the fragment in which your document keeps its title; the API Browser will again be of some nice help here:

So, to sum it up, you've got that @document variable, which you know is a document of type "article", and you want to get its "title" structured-text fragment. Well, that's simple:

@document['article.title']

Now what can you do with that structured-text fragment? Your native prismic.io kit comes with basic serialization into plain text or in html, which means, since you know it's a level-1 header in there, you can do both those things, and get the same output:

<h1><%= @document['article.title'].as_text %></h1>
<%= @document['article.title'].as_html %>
<%# Both are intended to output: <h1>Try WorldChanger free for 30 days</h1> %>

But wait, what will happen if there are hyperlinks in the structured text fragment, and they link to documents on your website? On its side, prismic.io can't know of the right URLs on your website! Right, so you'll need to teach your as_html method, by passing it a link_resolver object that you knows to tell a URL on your website from a document on prismic.io (but more on link_resolver objects later!)

<%= @document['article.title'].as_html(link_resolver(ref)) %>

Also, because of Rails security policy, the result will be in escaped HTML, so you need to use the "html_safe" Rails keyword, telling it that it is safe not to escape it, as is done in the first line below; but the starter project also makes it possible to write it all at once, just like the second line below.

<%= @document['article.title'].as_html(link_resolver(ref)).html_safe %>
<%= @document['article.title'].as_html_safe(link_resolver(ref)) %>

One last thing: are you sure your "title" fragment will not be empty? If it might be, then prismic.io will not return it in the API, and @document['article.title'] will be nil. Therefore, you may want to test it first:

<%= @document['article.title'] ? @document['article.title'].as_html_safe(link_resolver(ref)) : '' %>

Now that you know how to display a fragment exactly as you wish on a given document, looping through your pricing plans to display them will also seem very easy to you:

<% @plans.each do |plan| %>
<div class="plan" id="<%= plan.id %>">
<h2><%= plan['pricing.name'] ? plan['pricing.name'].as_text : '' %></h2>
<%= plan['pricing.bestfor'] ? plan['pricing.bestfor'].as_html_safe(link_resolver(maybe_ref)) : '' %>
<h2>$<%= plan['pricing.price'] ? plan['pricing.price'].value.to_i : '' %> /mo</h2>
<%# etc... %>
</div>
<% end %>

Here is the complete code for this view, and you can learn more about what you can do with all fragments types in the prismic.io Ruby kit's API documentation.

Link resolver

The link resolver is the object you teach how to take a prismic.io document description, and turn it into a URL on your website. Only you know how to do that! You can read more about link resolvers, asHtml, and URLs at the very bottom of prismic.io's API documentation.

We know we want the user to be directed to the pricing page when those are linked to by a content writer:

  • either the site-level article dedicated to pricing;
  • or one of the pricing plans documents.

More generally, we know that in this project (and in most projects), we'll usually decide on our URL building depending on an document's type, except for site-level articles, where we'll use their bookmarks. By default, as you can see, the link_resolver object is very trivial in the starter project, and we'll make richer so it becomes able to handle our new use cases:

case doc.link_type

when "article" # This type is special: the URL is built depending on the document's prismic.io bookmark
case doc.id
when api.bookmark("pricing")
pricing_path(ref: maybe_ref)
end

when "pricing"
pricing_path(ref: maybe_ref) + "#" + doc.id

end

You can find right here the full link resolver object after the whole WorldChanger website is built. Note that the link_resolver object always takes a "maybe_ref" parameter (which contains the ref if it is not the master ref, or null if it is). This allows for the ref parameter to be passed on when building URLs, so that you don't lose it as you navigate the site while previewing future content releases.

Developing a blog post page

Well this is pretty similar to the pricing page, with one big difference: the page itself is not built from a bookmarked document, but from a blog post just like any other blog post; we will therefore use its ID and slug in the URL, as per SEO best practice, in order to retrieve that specific blog post.

Routes

Quite unsurprisingly, you need to tell Rails to expect the ID and the slug:

get '/blog/:id/:slug', to: 'application#blogpost', constraints: {id: /[-_a-zA-Z0-9]{16}/}

Controller

To be on top of your SEO best practice, you need to return a 404 is the slug is wrong, and redirect the document if the slug used to be right but has changed since. This sounds complicated, but there's a helpful PrismicService.slug_checker method for that in the starter kit.

First, get the ID and the slug that have been passed through the route, and get the document from the ID (you already know how to do that!):

id = params[:id]
slug = params[:slug]
@document = PrismicService.get_document(id, api, ref)

Then get the slug checked, and take the appropriate action (feel free to change the behavior, if you feel like it):

# Checking if the doc / slug combination is right, and doing what needs to be done
@slug_checker = PrismicService.slug_checker(@document, slug)
if !@slug_checker[:correct]
render status: :not_found, file: "#{Rails.root}/public/404", layout: false if !@slug_checker[:redirect]
redirect_to blogpost_path(id, @document.slug), status: :moved_permanently if @slug_checker[:redirect]
else
# Slug is right, do what you want!
end

View

Well, there's nothing really more complicated than with the pricing page we went through before; you can know use the @document object you just made sure is the right one, to get its fragments and do whatever you want to display them.

Link resolver

Quite unsurprisingly, you will want this page to be the target when the hyperlink found links to a "blog post" object in a structured text fragment. Simply remember to pass the ID and the slug of the linked document, this time, so Rails can build all of the URL for you:

when "blog"
blogpost_path(doc.id, doc.slug, ref: maybe_ref)

Deploy to production

After all of the pages have been developed one by one, WorldChanger's brand new website is ready to meet the real world!

The starter project we built our website upon is made with Rails 4, so it is compatible with any Rails-4-compatible hosting provider. For instance, if you wish to host it on Heroku, install the Heroku Toolbelt, then create an account on Heroku and login in your terminal with "heroku login". Finally, go to your application's directory with your Terminal, and after committing all your changes to your master branch with Git, run:

heroku create
git push heroku master
heroku open

Congratulations, your website is now out there!

Epilogue

Now that the website is deployed, the WorldChanger people will be able to focus on finely optimizing they copy, enjoying the flexibility of prismic.io's approach to content management.

As for you, if you want to understand more comprehensively how this code works, feel free to browse through the full Rails open-source code for this app; and if you're not sure yet how you'll all apply this to your project, you know you can always reach out for help and advice, through the "Ask us anything" feature in your repository's writing-room, or by e-mail.

Have fun with your project!