Plugin generating modular page

I write a plugin which generates a login profile page the same way as the login plugin does. But it should work with a modular page.

It registers the page using the same code

$route = $this->config->get('plugins.login.route_profile');
$pages = $this->grav['pages'];

$page = new Page();
// $page->init(new \SplFileInfo(__DIR__ . '/pages/profile.md'));
$page->init(new \SplFileInfo(__DIR__ . '/pages/modular.md'));
$page->slug(basename($route));

$pages->addPage($page, $route);

// Profile page may not have the correct Cache-Control header set,
// force no-store for the proxies.
$page->expires(0);

If it is a normal page, it works perfect. If it is a modular page, it does not render the modular subpages. The same page (including subpages) renders, when I save it as a normal page (like 04.profile).

It looks like

content:
    items: '@self.modular'

does not find the modular pages. My Twig template using page.collection() does not evaluate to an array, the {% for … loop does not render the loop body.

How do I register the modular subpages?
Is there a way to do that in the frontmatter as fully named items?
Do I have to do it programmatically in my plugin?
Is there any example code?

Thanks,
Birger

As far as I know there is no need to register the other pages. Your plugin creates the primary modular page which actually is just a regular page. The only difference with a normal Grav page is that a primary modular page does not have any content. Instead it specifies which other modular pages to include as it were by creating a collection in the primary page frontmatter (or header).

This is what you already do. So it must be something very simple which is causing troubles.

Since you specify the collection as the set of all child pages below the modular primary page, I wonder how do these pages get created? If they are somewhere else in the site structure you need to create a different collection.

Also make sure the other modular pages (the so called Module-folders) all start with an underscore character.

Thanks for the feedback. But I’m afraid, this does not work as expected.

If you create a normal page in /user/pages, this does work. That’s the normal way to create pages. But if you add a plugin dependent page and put it into your plugin folder like the login-plugin does, this does not work out of the box.

I examined the files in /system/src/Grav/Common/Page and found the protected function recurse(), which does a filesystem traversal to register all pages, subpages and modulars.

Unfortunately there is no public function to add/register a page, which does the same just for one page. Something like addModularPage() or addPageWithChildren(). At least I didn’t find one so far.

As I didn’t find any plugin, which registers it’s own modular page, I try to learn from GRAV source and pick the necessary parts only. But I’m not already there…

Are you talking about page templates, that are in plugin’s structure? If so, do you have templates passed to Grav in your plugin? Eg.:

    public function onPluginsInitialized(): void
    {
        if ($this->isAdmin()) {
            $this->enable(
                [
                    'onGetPageTemplates'  => ['onGetPageTemplates', 0],
                ]
            );

            return;
        }

        $this->enable(
            [
                'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
            ]
        );
    }

    public function onTwigTemplatePaths()
    {
        $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
    }

    public function onGetPageTemplates(Event $event)
    {
        $locator = Grav::instance()['locator'];
        $event->types->scanTemplates($locator->findResource('plugin://' . $this->name . '/templates'));
    }

I do not talk about templates, I talk about the page itself. I use standard templates only.

My page (it’s a replacement for /login/profile) does not get written to /user/pages, it’s beneath /user/plugin/my-plugin/pages.

Like login-plugin I’m able to allocate any standard page including forms. I want to replace that with a modular page which consists of several elements and multiple forms within one browser page. This does work perfectly when I allocate this modular page beneath /user/pages, as the standard filesystem traversal finds all the subpages and modulars.

If I add the page programmatically, there is no such function. addPage() registers one page only. I have to implement something like file traversal, registering all found subpages and modulars and linking them together.

As I know all components in advance, I don’t need any full fledged filesystem traversal. I just look for hints to get the right sequence of addPage() and how to link together the children.

1 Like

I see Login plugin creates page dynamically and adds it to $pages. You said you know all your modules. Can you create all module pages using same logic and set parent to each of them?

Something like

    public function addModularPage()
    {
        /** @var Pages $pages */
        $pages = $this->grav['pages'];
        $page = $pages->dispatch($this->route);

        if (!$page) {
            // Only add login page if it hasn't already been defined.
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/my_modular.md'));
            $page->slug(basename($this->route));

            $pages->addPage($page, $this->route);
        }

        // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }

        foreach (['my', 'modules'] as $module) {
            $this->addModulePage($module, $page)
        }
    }

    protected function addModulePage($module, $parent)
    {
        /** @var Pages $pages */
        $pages = $this->grav['pages'];
        $page = $pages->dispatch($this->route . '/_' . $module); // << Not sure about this part how module routes should be if it's needed at all

        if (!$page) {
            // Only add login page if it hasn't already been defined.
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modules/_' . $module . '.md'));
            $page->slug(basename($this->route));

            $page->parent($parent); // << here goes your main modular page as a parent

            $pages->addPage($page, $this->route);
        }

        // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }
    }

Keep in mind I didn’t test it, so it might need some adjustments if it works at all

Thanks for this snippet. Unfortunately it throws an error in /system/src/Grav/Common/Page/Page.php

“Call to a member function root() on null” on line 2508

2499    $uri = Grav::instance()['uri'];
2500    $pages = Grav::instance()['pages'];
2501    $uri_path = rtrim(urldecode($uri->path()), '/');
2502    $routes = Grav::instance()['pages']->routes();
2503
2504    if (isset($routes[$uri_path])) {
2505        /** @var PageInterface|null $child_page */
2506        $child_page = $pages->find($uri->route())->parent();
2507        if ($child_page) {
2508            while (!$child_page->root()) {
2509                if ($this->path() === $child_page->path()) {
2510                    return true;
2511                }
2512                $child_page = $child_page->parent();
2513            }
2514        }
2515    }

There is still something missing for setting up the parent/child relationship.

Looks like some paths are wrong. Notice in my provided snippet you need to change all paths and routes to the ones you have.

I did that. Instead of having my-plugin/pages/_module.md I have my-plugin/pages/_module/text.md according to the documentation of modular pages.

If I change that to pages _module.md, I do not get the error. But instead I get several times the parent (modular) page instead of the module page. And I get this page alternating twig processed and raw.

Interlinking pages and access to child items do still not work correctly. Everything I try is like digging in the dark, still havo no idea, how it should work.

Just discoverd a minor glitch (or bug?) in /system/src/Grav/Common/Page/Page.php (see code from above line 2508). You find the same function activeChild() in /system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php with code

109  $child_page = $page ? $page->parent() : null;
110  while ($child_page && !$child_page->root()) {
111      if ($this->path() === $child_page->path()) {
112         return true;
113      }
114      $child_page = $child_page->parent();
115  }

After changing that in Page.php, it does not throw errors anymore. But still no success rendering the modules.

Could you maybe share your plugin with info how to use it and what’s the expected result? I might give it a try

Sorry, this would not work. I’ll explain:

I run a (privately organised) cloud based service for email, webhosting and some predefined webservices with some hundreds of users of a closed community. Part of that is a portal, where people can apply for services and manage their accounts.

The first portal was self programmed in Perl (back in 1999), the current portal is based on Drupal 6. I try to give GRAV a chance to become the next portal.

My module handles managing your own account and some services, the user database is a backend LDAP server. For that I combine Login-Plugin with LDAP-Login-Plugin and some glue in my own plugin.

Already working I have

  • registering/applying for a new account (sorry, not open. closed community)
  • removing an account
  • changing account parameters (at least programmatically)

My intent is to have a profile page with three forms, one for managing account data, one for service parameters, one for changing the password. Organized as one page.

I have a normal modular page beneath /user/pages with three forms. But this one does not get filled with account data, and it does not work together with Login-Plugin for changing the user profile.

My plugin should be the glue to have my own profile page based on three modular forms. I want to implement minimally invasive instead of building a full clone of Login-Plugin. Just to be save on GRAV enhancements.

My hope was that programmatically creating modular pages was already solved by others.

I see…
Do you have to have modular page there or maybe three AJAX forms would do?

I just got the right inspiration - have it working.

Solution:
I have my own pages directory /user/plugin/my-plugin/pages with all pages and modules like written in the documentation. Every module is a subdirectory _module-xx with its .md-file.

The suggested code of Karmalakas works perfectly.

In my modular page (parent page, modular.md within /user/plugin/my-plugin/pages) I have to use @self.all for the page collection. Neither @self.modular nor @self.children nor @self works.

Thanks for your help.

As I suspected, my snippet just needed a bit of touch :slight_smile:

Plugin class:

    // @modular.php

    protected $route = '/modular';

    public function onPluginsInitialized(): void
    {
        /** @var Uri $uri */
        $uri = $this->grav['uri'];

        if ($uri->path() === $this->route) {
            $this->enable(
                [
                    'onPagesInitialized' => ['addModularPage', 0],
                ]
            );

            return;
        }
    }

    public function addModularPage()
    {
        /** @var Pages $pages */
        $pages = $this->grav->get('pages');
        $page  = $pages->dispatch($this->route);

        if (!$page) {
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modular.md'));
            $page->slug(basename($this->route));

            $pages->addPage($page, $this->route);
        }

        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }

        foreach (['module_1', 'module_2'] as $module) {
            $this->addModulePage($module, $page);
        }
    }

    protected function addModulePage($module, $parent)
    {
        $route = $this->route . '/_' . $module;

        /** @var Pages $pages */
        $pages = $this->grav->get('pages');
        $page  = $pages->dispatch($route);

        if (!$page) {
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modules/_' . $module . '/text.md'));
            $page->slug($module);
            $page->parent($parent);
            $page->modularTwig(true);

            $pages->addPage($page, $route);
        }

        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }
    }

Pages structure:

pages
 |- modules
 |   |- _module_1
 |   |   '- text.md
 |   '- _module_2
 |       '- text.md
 '- modular.md

text.md

---
title: Module 1/2
---

Module 1/2 content

modular.md

---
title: Modular title
content:
    items: '@self.modular'
---

If you navigate to /modular, both modules are outputted
Notice $page->modularTwig(true) on each module

You have to add $modulePage->modularTwig(true) and then you can use @self.modular