I am wondering how you do those resource-items on your download page for skeletons, themes and plugins? Is it done Grav (markdown) or by including html or by a custom plugin?

The download section is probably our most complicated bit in the whole getgrav site. It serves as the repository for GPM as well as regular downloads.
The structure of it is all Grav, we have a main downloads page that is the top part with the Grav download details and which is not routable, then we have Plugins, Themes and Skeletons sub pages that use a resources twig template for providing the html structure we want. This subpages load the items dynamically. They aren’t hardcoded.
We built a repository plugin that list all the available resources and generates a json out of it, the same json is used for feeding the download page as well as GPM when you use it for installing remotely or listing.

Hope this is what you were asking about and that I answered it.

Thanks for the fast response (all always!). I am looking to do something similar but for webapps. With an icon, title, buttons to open the webapp and a button for more information about it. I have currently only made en example in static html with bootstrap panel and popover. The data was planned to be stored in an database but I think a json-file i probably better now that I’m planning to use Grav.

Unfortunately I don’t think I manage to build a plugin to load the data from a json to Grav.

The repository plugin we use for parsing the JSON and passing it to twig is quite simple (you can see the plugin at the end of the post). All it does is reading the json file and parsing it and pass it to the twig as variable repository.
It also has some extra logic to get only a subset of the json if it’s specified in the frontmatter, like:

    enabled: true
    type: skeletons

Then from the twig template you have access to this repository variable which is the parsed json. So for instance to render the count of Themes we do like this:

<em>{{ attribute(repository, 'themes') | length }}</em>

It also caches it all so makes things extremely fast. :slight_smile:

This below is the plugin itself:

namespace Grav\Plugin;

use Grav\Common\Plugin;

class SiterepoPlugin extends Plugin
    protected $active = false;
    protected $repository;
    protected $cache;
    protected $valid_types = array('json');

     * @return array
    public static function getSubscribedEvents()
        return [
            'onPageInitialized' => ['onPageInitialized', 0],
            'onTwigSiteVariables' => ['onTwigSiteVariables', 0]

     * Activate plugin if path matches to the configured one.
    public function onPageInitialized()
        $uri   = $this->grav['uri'];
        $page  = $this->grav['page'];

        $type = $uri->extension();
        $isJSON = $type && in_array($type, $this->valid_types);

        if (!$page) {

        if (isset($page->header()->siterepo) || $isJSON) {
            $this->active = true;

            $file = USER_DIR . '/repository/repository.json';
            if (!file_exists($file)) {
                $this->active = false;
            } else {

                $this->repository = $this->cache['array'];
                $type = $page->header()->siterepo['type'];
                $data = isset($this->repository[$type]) ? $this->repository[$type] : [];

                if (isset($type) && $isJSON) {
                    $this->repository = json_encode($data);

     * Make repository accessible from twig.
    public function onTwigSiteVariables()
        if (!$this->active) {

        // in Twig template: {{ repository.plugins.breadcrumbs }}
        $this->grav['twig']->twig_vars['repository'] = $this->repository;

    private function initializeCache($file)
        $cache = $this->grav['cache'];

        $this->cache = $cache->fetch(md5($file.filemtime($file)));

        if (!$this->cache) {
            $content = file_get_contents($file);
            $this->cache = [
                'json'  => $content,
                'array' => json_decode($content, true)

            $cache->save(md5($file.filemtime($file)), $this->cache);

        return $this->cache;

Ok. Thanks for sharing! I will try it out.