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:

  1. Wrap the template code in <div markdown="0">{%for ... %}{%endfor%}</div> if Parsedown Extra is enabled (content_config.extra) or:
  2. str_replace('<p></p>','',$twigVariables['content']) in onPageRendering() 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"
}


  1. you can also set the data.url key to a pico page and data.type to internal or pico and can include the meta data from that page. See the comment in the ExternalData.php file