Pico CMS – External API Data Rendered With Inline Twig Templates
Pico is a wonderful, tight flat-file CMS that has a plugin-system which makes it easy to add any functionality by hook-in to every step of the page rendering process.
Pages are typically markdown files and the theme is built with Twig templating language. So far so good.
In case you want to include external data from an JSON API, you could use Java Script (something like Alpine.js), fetch()
the data and render it.
But this approach is not always the best choice, in case you have to protect an API key or you don't want to mess with Java Script at all.
I came up with two Pico plugins that take care of this. With the ExternalData plugin, you can set a YAML key with the url to an API Endpoint1.
With the TwigStringLoader plugin you are able to use Twig markup in your content directly (See edge cases).
I expect you to know how to use plugins in Pico and be warned that you should only use this, if you know what you are doing. I can't tell how much of an security risk it is, if you use Twig in your pages, but as long as you are the only one who has access, you should be fine.
Please inspect the settings for the stream_context
, you might want to make it more strict!
Below you'll find the Gist and a live example.
Plugin Code
Edge Cases
Twig tags as {{example.code}}
There are edge cases to take care of, if you want to use Twig templating language in markdown code blocks like I did on this site. You have to convert curly braces in code blocks {}
to html entities before render the Twig string, else the Twig engine tries to render it and might crash your script.
public function onContentParsed(&$content)
{
// I'm using the convenient phpQuery here - DOMDocument will do it!
$doc = phpQuery::newDocumentHTML($content);
foreach ($doc['code'] as $code) {
$html = pq($code)->html();
$html = str_replace(['{','}'], ['{','}'], $html);
pq($code)->text($html);
}
// convert &#123 back to &#
$content = htmlspecialchars_decode($doc);
}
Twig tags in href="{{example.code}}"
Another weird quirk is the urlencode()
of html attributes src
and href
that breaks Twig markup in there.
This happens if you have Parsedown Extra enabled (content_config.extra
).
https://github.com/erusev/parsedown/issues/266#issuecomment-159139099
I took care of that in TwigStringLoader
!
Empty <p></p>
Tags
And if you use {%for%}
loops or other tags that are not wrapped in HTML, you will find empty <p></p>
tags in your output.
This is, because your Twig tags are being replaced after your inline template ran through Parsedown.
To solve this we could:
- Wrap the template code in
<div markdown="0">{%for ... %}{%endfor%}</div>
if Parsedown Extra is enabled (content_config.extra
) or: -
str_replace('<p></p>','',$twigVariables['content'])
inonPageRendering()
or use something like * HTML Purifier.
Live Example
Below is the data from https://jsonplaceholder.typicode.com/users rendered.
If you want to know what's available, dump the variables with this:
{{ dump(meta.data) }}
Smarty | Value |
---|---|
{{ user.id }} |
1 |
{{ user.name }} |
Leanne Graham |
{{ user.address.city }} |
Gwenborough |
array(3) {
["name"]=>
string(15) "Romaguera-Crona"
["catchPhrase"]=>
string(38) "Multi-layered client-server neural-net"
["bs"]=>
string(27) "harness real-time e-markets"
}
Smarty | Value |
---|---|
{{ user.id }} |
2 |
{{ user.name }} |
Ervin Howell |
{{ user.address.city }} |
Wisokyburgh |
array(3) {
["name"]=>
string(12) "Deckow-Crist"
["catchPhrase"]=>
string(30) "Proactive didactic contingency"
["bs"]=>
string(32) "synergize scalable supply-chains"
}
Smarty | Value |
---|---|
{{ user.id }} |
3 |
{{ user.name }} |
Clementine Bauch |
{{ user.address.city }} |
McKenziehaven |
array(3) {
["name"]=>
string(18) "Romaguera-Jacobson"
["catchPhrase"]=>
string(33) "Face to face bifurcated interface"
["bs"]=>
string(31) "e-enable strategic applications"
}
Smarty | Value |
---|---|
{{ user.id }} |
4 |
{{ user.name }} |
Patricia Lebsack |
{{ user.address.city }} |
South Elvis |
array(3) {
["name"]=>
string(13) "Robel-Corkery"
["catchPhrase"]=>
string(40) "Multi-tiered zero tolerance productivity"
["bs"]=>
string(36) "transition cutting-edge web services"
}
Smarty | Value |
---|---|
{{ user.id }} |
5 |
{{ user.name }} |
Chelsey Dietrich |
{{ user.address.city }} |
Roscoeview |
array(3) {
["name"]=>
string(11) "Keebler LLC"
["catchPhrase"]=>
string(36) "User-centric fault-tolerant solution"
["bs"]=>
string(32) "revolutionize end-to-end systems"
}
Smarty | Value |
---|---|
{{ user.id }} |
6 |
{{ user.name }} |
Mrs. Dennis Schulist |
{{ user.address.city }} |
South Christy |
array(3) {
["name"]=>
string(17) "Considine-Lockman"
["catchPhrase"]=>
string(34) "Synchronised bottom-line interface"
["bs"]=>
string(32) "e-enable innovative applications"
}
Smarty | Value |
---|---|
{{ user.id }} |
7 |
{{ user.name }} |
Kurtis Weissnat |
{{ user.address.city }} |
Howemouth |
array(3) {
["name"]=>
string(11) "Johns Group"
["catchPhrase"]=>
string(34) "Configurable multimedia task-force"
["bs"]=>
string(29) "generate enterprise e-tailers"
}
Smarty | Value |
---|---|
{{ user.id }} |
8 |
{{ user.name }} |
Nicholas Runolfsdottir V |
{{ user.address.city }} |
Aliyaview |
array(3) {
["name"]=>
string(15) "Abernathy Group"
["catchPhrase"]=>
string(29) "Implemented secondary concept"
["bs"]=>
string(29) "e-enable extensible e-tailers"
}
Smarty | Value |
---|---|
{{ user.id }} |
9 |
{{ user.name }} |
Glenna Reichert |
{{ user.address.city }} |
Bartholomebury |
array(3) {
["name"]=>
string(13) "Yost and Sons"
["catchPhrase"]=>
string(37) "Switchable contextually-based project"
["bs"]=>
string(32) "aggregate real-time technologies"
}
Smarty | Value |
---|---|
{{ user.id }} |
10 |
{{ user.name }} |
Clementina DuBuque |
{{ user.address.city }} |
Lebsackbury |
array(3) {
["name"]=>
string(10) "Hoeger LLC"
["catchPhrase"]=>
string(33) "Centralized empowering task-force"
["bs"]=>
string(24) "target end-to-end models"
}
you can also set the
data.url
key to a pico page anddata.type
tointernal
orpico
and can include the meta data from that page. See the comment in the ExternalData.php file ↩