Grav as Headless CMS

Sorry if it’s off topic, but what did you find frustrating with wordpress API ?

For grav with Gatsby I think that you should be able to get both frontmatter and content.
But I have no clue if there is an API.

I don’t know either how you could handle the images if you want to use grav media.

1 Like

I was going through various tutorials using the WP REST API. Which in fact didn’t work out too bad.
What sucked was, stuff like images in posts or pages. And then of course, now with Gutenberg Blocks it’s a whole different ball game.

So then I came across the WP GraphQL plugin and various offshoots, WP GraphQL Gutenberg, etc. These are all still under development. I started working on something, but then after a recent update I’m running into issues.

It’s all a little above my experience level. I feel like a checkers player at a chess game. :laughing:

By creating Twig templates which output Grav pages as JSON you could come a long way. Learn how in https://learn.getgrav.org/16/content/content-types

Recently a new REST API plugin for GravCMS was released. Unfortunately you’ll have to dive into it’s code to find out how to use it though.

2 Likes

Ah schweet! Thanks so much @bleutzinn.

Both those options look promising.

By creating Twig templates which output Grav pages as JSON you could come a long way. Learn how in https://learn.getgrav.org/16/content/content-types

That’s pretty neat!
Thank you!

1 Like

I kind of just stumbled upon Awesome Grav Stuff which includes a link to a rather lengthy and detailed article “Grav as Headless CMS Tied to Gatsby with GraphQL Schema”.

Thanks for that. I’ve seen that repository before. Also went through that article. It uses an extension for Grav that hasn’t been updated in 4 years though.

Getting some projects out the way, then I’m going to take fresh dive into Grav & Gatsby.

You are referring to Grav Pages as Data Plugin I assume. What it does is similar to outputting JSON via a template but instead of just the Markdown page content it outputs the entire Grav page object as a JSON encoded array.

Indeed that plugin hasn’t been updated in 4 years now but that need not be bad. It’s a very simple and stable plugin I think :wink:

Ah oki. Thanks for clearing that up.

If you go through the Gatsby docs and tutorials on www.gatsbyjs.org, their “introduction to Gatsby” tutorial describes using Markdown files as a source for content.

As far as I can make out, there are ways to fetch these files remotely.

I assume there must be a way to have Grav give access to it’s Markdown files?

There are some good suggestions in URL for plain markdown and I tried “alternative 2” but without luck.

I think there is no way to prevent Grav from parsing content from Markdown to HTML without resorting to a custom plugin.

But if I wanted to access the Markdown directly and use Grav just as a headless CMS, is this not possible?

Yes it is possible. But I think you need some custom PHP code to do it. In Grav custom PHP goes into a plugin.

So there’s no way to just gain direct access to the folder containing the markdown files?

@bjorn, Although I do not know much about your specific requirements, the rest-API of WP comes to mind.

Unfortunately, Grav doesn’t provide something similar to WP’s rest-api. The following is a simple attempt to create a ‘kind of’ api, using a plugin:

I tried the following using a fresh Grav 1.6.23 installation:

  • Install plugin devtools: $ bin/gpm install devtools
  • Create a new plugin: $ bin/plugin devtools newplugin
    • Answered some questions. I called the plugin ‘headless’
    • $ cd user/plugins/headless
    • $ composer update
  • The gist of ‘/user/plugins/headless/headless.php’ is:
    class HeadlessPlugin extends Plugin {
       const api = '/api/v1/pages/';
    
       /** The url of page being called */
       protected $pageUrl;
    
       // standard code omitted
    
      /**
       * Initialize the plugin
       */
      public function onPluginsInitialized()
      {
         // Don't proceed if we are in the admin plugin
         if ($this->isAdmin()) {
            return;
         }
    
         // Enable the main events we are interested in
         $lengthApi = strlen(self::api);
         $url = $this->grav['uri']->url();
    
         // If the API is being called
         if (substr($url, 0, strlen(self::api)) === self::api) {
            $this->pageUrl = substr($url, $lengthApi - 1);
    
            $this->enable([
               // Put your main events here
               'onPagesInitialized' => ['onPagesInitialized', 0]
            ]);
         }
      }
    
      /**
       * All pages have been initialized
       */
      public function onPagesInitialized($event)
      {
         $pages = $event['pages'];
         $page = $pages->find($this->pageUrl);
         $content = $page->content();
         echo ($content);
         die();
      }
    }
    
  • Browse to ‘localhost/api/v1/pages/typography’
  • The bare processed markdown of page '02.typography/default.md` should be send to client:
    <div class="notices yellow">
    <p>Details on the full capabilities of Spectre.css can be found in the <a href="https://picturepan2.github.io/spectre/elements.html">Official Spectre Documentation</a></p>
    </div>
    <p>The <a href="https://github.com/getgrav/grav-theme-quark">Quark theme</a> is the new default theme for Grav built with <a href="https://picturepan2.github.io/spectre/">Spectre.css</a> the lightweight, responsive and modern CSS framework. Spectre provides  basic styles for typography, elements, and a responsive layout system that utilizes best practices and consistent language design.</p>
    <h3>Headings</h3>
    <h1>H1 Heading <code>40px</code></h1>
    <h2>H2 Heading <code>32px</code></h2>
    <h3>H3 Heading <code>28px</code></h3>
    <h4>H4 Heading <code>24px</code></h4>
    <h5>H5 Heading <code>20px</code></h5>
    <h6>H6 Heading <code>16px</code></h6>
    
    etc.
    

Note:
This is only a simple starter to show what a plugin with a few lines of code could do.

1 Like

Thanks @pamtbaau, that is very helpful!

If I am unable to remotely access the markdown files directly, it seems this is the kind of route I would need to take.

Learn how in https://learn.getgrav.org/16/content/content-types

After finally getting some time to RTFM a bit. This actually seems like the answer and looks fairly simple in theory.

Did u accomplished to access the md files with gatsby? That would be great. I mean it should be possible right?

I’ve been looking at this from all angles and coming to the conclusion it might just be easier to work with md files directly in Gatsby. As mush as I want to use Grav!

I was thinking for instance to make a symlink from the md folder in Grav to Gatsby, but then the way Grav arranges it’s md files doesn’t play well with Gatsby. Unless you can change how Gatsby looks at the md file arrangement?

Sadly, as much as I like Grav and want this to work with Gatsby, it seems the only way is going to have to be through some kind of json api setup. Similar to WordPress’s WP REST API.

After playing with Grav a bit and using it without the Admin plugin, one thing Grav showed me was, maybe it’s not that bad to work with raw md files for content. Combine that with Gatsby’s JSX in Markdown features…

This could be a pretty schweet union though if someone clever has some ideas on how to marry the two?

Well, hello.
I don’t know if you figure it out but thanks to this topic I did.

Thanks to pages as data plugin and this comment

I was able to make plugin which shows all of the information from the page child.
The plugin:

<?php
namespace Grav\Plugin;

use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;

class PageAsDataPlugin extends Plugin {
  const api = '/api/';
  /** The url of page being called */
  protected $pageUrl;

  public static function getSubscribedEvents() {
    return [
      'onPluginsInitialized' => [
        ['onPluginsInitialized', 0]
      ],
    ];
  }

  public function onPluginsInitialized() {
    if ($this->isAdmin()) {
      return;
    }
    // Enable the main events we are interested in
    $lengthApi = strlen(self::api);
    $url = $this->grav['uri']->url();

    // If the API is being called
    if (substr($url, 0, strlen(self::api)) === self::api) {
      $this->pageUrl = substr($url, $lengthApi - 1);
      $this->enable([
        'onPageInitialized' => ['deliverFormatAs', 0]
      ]);
    }
  }

  /**
   * Delivers the page as a different format.
   */
  public function deliverFormatAs($event) {
    /**
     * @var \Grav\Common\Page\Page $page
     */
    $pages = $this->grav['pages'];
    $page = $pages->find($this->pageUrl);
    $collection = $page->collection('content');
    $pageArray = $page->toArray();
    $children = array();
    foreach ($collection as $item) {
      $children[] = $item->toArray();
    }
    $pageArray['children'] = $children;
    header("Content-Type: application/json");
    echo json_encode($pageArray);
    exit();
  }
}

/**
 * Converts an array to XML
 *  http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
 */
/**
 * Extends the DOMDocument to implement personal (utility) methods.
 * - From: http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
 * - parent:: See http://www.php.net/manual/en/class.domdocument.php
 *
 * @throws   DOMException   http://www.php.net/manual/en/class.domexception.php
 *
 * @author Toni Van de Voorde
 */
class PageAsDataXmlDomConstructor extends \DOMDocument {
  public function fromMixed($mixed, \DOMElement $domElement = null) {
    $domElement = is_null($domElement) ? $this : $domElement;
    if (is_array($mixed)) {
      foreach ($mixed as $index => $mixedElement) {
        if (is_int($index)) {
          if ($index == 0) {
            $node = $domElement;
          } else {
            $node = $this->createElement($domElement->tagName);
            $domElement->parentNode->appendChild($node);
          }
        } else {
          $node = $this->createElement($index);
          $domElement->appendChild($node);
        }
        $this->fromMixed($mixedElement, $node);
      }
    } else {
      $domElement->appendChild($this->createTextNode($mixed));
    }
  }
}

Everytime someone checks the page (but with the URL /api/ prefix) those 2 lines are called:

header("Content-Type: application/json");
echo json_encode($pageArray);

They transform the page and its child into the JSON (which is exactly what we need).

Then the Gatsby.
This was really tricky because I tried to repair and edit the plugin described in the article @bleutzinn posted:

Sadly, some libraries didn’t work. Then I found something amazing. Gatsby has a plugin called Gatsby Source Custom API which is created for you to link it with your CMS.

Thanks to this plugin I was finally able to connect to my domain.com/api/page

And weeeell, that’s all. This do all the magic.
I know it’s after 3 years but maybe, maybe someone like me will find it useful.

1 Like