Generating JSON with Kirby
With Kirby, you cannot only generate HTML output. In this recipe, we will explain how to generate JSON with Kirby.
If you've never heard about JSON, this tutorial is probably not for you, but you should definitely learn more about it – it's awesome!
Kirby's template system does not really care what you build with it. In fact you could even generate an Excel spreadsheet with the data of your site – though nobody wants that :)
For this tutorial we are going to build a little JSON API for our blog, which will provide a list of articles as JSON string. You could use that to add endless scrolling for your blog for example, or to load blog articles dynamically in some other fancy way.
Content structure
The content structure for the blog will be something like this:
Please read the article about how to build a blog with Kirby, if this seems weird to you.
Invisible API folder
What we are going to do is to add an invisible api folder to our blog folder, so we can access the API with the URL http://yourdomain.com/blog/api
, but it won't be listed in our Menu or list of articles.
Add a blogapi.txt
file to it and create a blogapi.php
file in site/templates
The template
As I mentioned before, it doesn't really matter what kind of content you create in your templates. Normally it will be HTML, but it could also be plain text, XML, javascript or anything else.
Open the new site/templates/blogapi.php
and add the following code:
<?php
$data = $pages->find('blog')->children()->visible()->flip()->paginate(10);
$json = array();
foreach($data as $article) {
$json[] = array(
'url' => (string)$article->url(),
'title' => (string)$article->title(),
'text' => (string)$article->text(),
'date' => (string)$article->date()
);
}
echo json_encode($json);
?>
It might look a bit frightening first, but let me explain this a bit more.
1. Fetching the data
$data = $pages->find('blog')->children()->visible()->flip()->paginate(10);
What we want is to fetch the same list of articles, which we use to build our regular blog template. First we are going to find the blog page -> then we select all children -> make sure to get the visible children only -> flip them to get the latest article first -> and finally we add pagination and limit the result of articles to 10.
This will make it possible to get the next page of data from our API like this:
http://yourdomain.com/blog/api/page:2
- easy, peasy!
2. Building the result
$json = array();
foreach($data as $article) {
$json[] = array(
'url' => (string)$article->url(),
'title' => (string)$article->title(),
'text' => (string)$article->text(),
'date' => (string)$article->date()
);
}
Once we've got our list of articles, we should build a nice array out of it, which we can use to generate JSON. The main goal of JSON is to reduce the amount of data, which is transfered between the client and the server, so you should only go on with the data you really need. In this case I create one big array of arrays ($json), with an nested array for each article.
There's one little thing you need to consider, when building that array. All the custom variables (title, text, date, etc.) are not just simple strings in Kirby, but objects. It's beyond the scope of this article to explain why, but what you need to do is to make sure those objects will be converted to strings by using (string):
(string)$article->title()
Once your $json
array is ready, you need to convert this to a json string with the native php json_encode function.
echo json_encode($json);
This will echo the entire array as a JSON encoded string. The template does not generate any HTML at all.
Adding a content type header
To make sure the output is considered as JSON by all clients, you should send a proper content type header. It's not really required, but it will make sure your JSON output is working everywhere. Add the following line at the beginning of your template code:
<?php
header('Content-type: application/json; charset=utf-8');
…
Using your API
You can now access your API with Javascript or however you plan to use it. I'm using jQuery here to make a little example call. I guess it will be the most common tool to do this.
jQuery example call
// call our api
$.getJSON('/blog/api', function(r) {
// loop through the result
$.each(r, function(i, article) {
console.log(article);
// do some other fancy js/jquery magic here.
});
});
Since we added ->paginate(10)
to our code earlier, you can now easily jump to different pages like this:
$.getJSON('/blog/api/page:2')
…
$.getJSON('/blog/api/page:3')
…
$.getJSON('/blog/api/page:4')
Securing your API
If you want to avoid that someone else is accessing your little API, you can simply add a check if the page has been requested with an Ajax call. There's a built-in function for this in the Kirby Toolkit
Add this at the top of your template code:
<?php
if(!r::ajax()) go(url('error'));
…
r::ajax()
asks for $_SERVER["HTTP_X_REQUESTED_WITH"]
which will be xmlhttprequest
in case of an Ajax call. So whenever a regular request is coming in, go(url('error'))
will be called, which redirects the visitor to the error page.
Ajax calls can only be made from the same domain unless you are using some cross-domain call magic, so it's a fairly good trick to protect your API from unwanted calls.
The entire code
Here's the entire code for this example:
<?php
if(!r::ajax()) go(url('error'));
header('Content-type: application/json; charset=utf-8');
$data = $pages->find('blog')->children()->visible()->flip()->paginate(10);
$json = array();
foreach($data as $article) {
$json[] = array(
'url' => (string)$article->url(),
'title' => (string)$article->title(),
'text' => (string)$article->text(),
'date' => (string)$article->date()
);
}
echo json_encode($json);
?>
Next steps
It's up to you what you build with this on the front-end. I'm sure you already got some great ideas :)
This tutorial is not limited to a blog-scenario. You can easily adapt this to make other parts of your site available as JSON. You could even build a port to XML or CSV. There are hardly any limits.
Update: Better pagination
As suggested in the comments, you might want to improve pagination a bit more by adding the current page and the number of pages to the result. You should then also move the articles to a separated data array:
<?php
if(!r::ajax()) go(url('error'));
header('Content-type: application/json; charset=utf-8');
$data = $pages->find('blog')->children()->visible()->paginate(10);
$json = array();
$json['data'] = array();
$json['pages'] = $data->pagination()->countPages();
$json['page'] = $data->pagination()->page();
foreach($data as $article) {
$json['data'][] = array(
'url' => (string)$article->url(),
'title' => (string)$article->title(),
'text' => (string)$article->text(),
'date' => (string)$article->date()
);
}
echo json_encode($json);
?>