HTTP Requests and the Document Builder
New in Bridgetown 0.14 as part of the Builder API is the ability to make web requests and easily parse the response to save site data or construct new documents like blog posts or collection entries.
Here’s an example of making an HTTP GET request to a remote API, looping through an array parsed from the JSON response, and saving new posts based on each item:
class LoadPostsFromAPI < SiteBuilder
def build
get "https://domain.com/posts.json" do |data|
data.each do |post|
doc "#{post[:slug]}.md" do
front_matter post
categories post[:taxonomy][:category].map { |category| category[:slug] }
date Bridgetown::Utils.parse_date(post[:date])
content post[:body]
end
end
end
end
end
Table of Contents
- Making a Request
- Customizing the Connection Object
- The Document Builder
- Builder Lifecycle and Data Files
- What About GraphQL?
- Conclusion
Making a Request
To make a request, simply call the get
method inside of build
in your builder class:
def build
get url do |data|
site.data[:remote_api_info] = data
end
end
By default, the request will expect and parse JSON data from the remote endpoint. To bypass this and access raw text instead, set the parse_json
keyword to false
:
def build
get url, parse_json: false do |data|
# do something with the raw text, HTML, CSV, etc.
end
end
You can also customize the HTTP headers sent with the request. For example, you might want to use an auth token to access protected resources:
def build
get url, headers: {"Authorization" => "Bearer #{config["api_key"]}"} do |data|
# data for your eyes only
end
end
Customizing the Connection Object
Bridgetown uses the Faraday gem under the hood to make web requests. If you need to customize the default usage of Faraday—perhaps to set additional defaults or inject middleware to adjust the request or response logic—simply override the connection
method in your builder class.
Here’s an example of using Retry middleware to ensure requests are attempted multiple times before admitting defeat:
def connection(headers: {}, parse_json: true)
retry_options = {
max: 2,
interval: 0.05,
interval_randomness: 0.5,
backoff_factor: 2
}
super do |faraday|
faraday.request :retry, retry_options
end
end
Bridgetown comes with the Faraday Middleware gem out-of-the-box and utilizes a few of its options such as following redirects (if necessary). You can require
additional middleware to add to your Faraday connection if you like. You can also write your own Faraday middleware, but that’s an advanced usage and typically not needed.
The Document Builder
Adding content from an API to the site.data
object is certainly useful, but an even more powerful feature recently added to Bridgetown is the Document Builder. All you need to do is call the doc
method to generate Post and Collection documents which function in exactly the same way as if those files were already stored in your repository.
Here’s a simple example of creating a new blog post:
def build
doc "2020-05-17-way-to-go-bridgetown.md" do
title "Way to Go, Bridgetown!"
author "rlstevenson"
content "It's pretty _nifty_ that you can add **new blog posts** this way."
end
end
This is the programmatic equivalent of saving a new file src/_posts/2020-05-17-way-to-go-bridgetown.md
with the following contents:
---
title: Way to Go, Bridgetown!
author: rlstevenson
---
It's pretty _nifty_ that you can add **new blog posts** this way.
Collections
By default, documents are saved in the posts collection, but you can save a document in any collection:
doc "rlstevenson.md" do
collection "authors"
name "Robert Louis Stevenson"
born 1850
nationality "Scottish"
end
You don’t even need to use a collection that’s previously been configured in bridgetown.config.yml
. You can make up new collections and use existing layouts to place your content within the appropriate templates, assuming the expected front matter is compatible.
doc "fake-blog-post.html" do
collection "blogish"
layout "post"
title "I'm a blog post…sort of"
date "2020-05-17"
content "<p>I might look like a blog post, but I'm <em>not!</em></p>"
end
That document would then get written out to the /blogish/fake-blog-post
URL.
Another aspect of the Document Builder to keep in mind is that content
is a “special” variable. Everything except content
is considered front matter, and content
is everything you’d add to a file after the front matter.
Customizing Permalinks
If you’d like to customize the permalink of a new document, you can specifically set the permalink
front matter variable…but an even easier way to do it is just start your filename with a path. For example:
doc "/path/to/the/blog-post.md" do
title "Strange Paths"
date "2019-07-23"
content "…"
end
The post would then be accessible via /path/to/the/blog-post
.
Merging Hashes Directly into Front Matter
If you have a hash of variables you’d like to merge into a document’s front matter, you can use the front_matter
method.
vars = {
title: "I'm a Draft",
categories: ["category1", "category2"],
published: false
}
doc "post.html" do
front_matter vars
end
This is great when you have data coming in from external APIs and you’d just like to inject all of that data into the front matter with a single method call.
Bear in mind that this doesn’t include your content
variable. So you’ll still need to set that separately when using the front_matter
method, for example:
get article_url do |data|
doc "new-article.html" do
front_matter data
content data[:body]
end
end
Builder Lifecycle and Data Files
Something to bear in mind is that that code in your build
method is run as part of the site’s pre_read
hook, which means that no data or content in your site repository has yet been loaded at that point. So you can’t, say, build documents based on existing data files as you might assume:
def build
# THIS WON'T WORK!!!
site.data[:stuff_from_the_repo].each do |k, v|
doc "#{k}.md" do
collection "stuff"
front_matter v
content v[:content]
end
end
end
Instead, what you can do is define a post_read
custom hook and then read in the data:
def build
hook :site, :post_read do
site.data[:stuff_from_the_repo].each do |k, v|
doc "#{k}.md" do
collection "stuff"
front_matter v
content v[:content]
end
end
end
end
You also can’t use the doc
method inside of a generator block/method or a Liquid tag/filter, because by the time your code executes, the build process has already run the internal generator which handles doc
-based documents.
def build
generator do
doc "this-wont-work.html" do
title "Oops…"
end
end
end
So basically you’ll want to contain usage of the Document Builder to either directly in the build
method or inside of a post_read
hook.
What About GraphQL?
Bridgetown has first-class support for GraphQL using a plugin called Graphtown.
Graphtown allows you to consume GraphQL APIs for your Bridgetown website using a tidy Builder DSL on top of the Graphlient gem.
Get started by simply running bundle add graphtown -g
bridgetown_plugins
in your bridgetown site.
Then, navigate to your plugins/site_builder.rb
file and add the
Graphtown mixin.
# plugins/site_builder.rb
class SiteBuilder < Bridgetown::Builder
include Graphtown::QueryBuilder
end
Setup your graphql_endpoint
in your bridgetown.config.yml
and
you’re ready to rock and roll.
# bridgetown.config.yml
graphql_endpoint: http://localhost:1337/graphql
For more details on how to use the Graphtown gem to pull in your data from a CMS, check out the project on Github. https://github.com/whitefusionhq/graphtown
Conclusion
As you’ve seen from these examples, using data from external APIs to create new content for your Bridgetown website is easy and straightforward with the get
and doc
methods provided by the Builder API. While there are numerous benefits to storing content directly in your site repository, Bridgetown gives you the best of both worlds—leaving you simply to decide where you want your content to live and how you’ll put it to good use as you build your site.