Instead of delving into an explanation of meta tags and their significance for SEO, let's dive straight into implementing an invaluable feature in Scriptor. Our goal is to generate metadata for each page and seamlessly incorporate it into the frontend template using meta tags.

To achieve this, we will leverage the hooking functions provided by Scriptor, specifically designed for version 1.4.6 and above. These functions empower us to make slight modifications to the Pages class within the CMS backend, enabling the desired functionality.

Now, let's explore the organized file structure of the PagesMetatags module:

PagesMetatags/
└───css/
│  └───styles.css
└───lang/  
│  └───en_US.php
└───PagesMetatags.php

To create our module, we will proceed by creating a PHP class called PagesMetatags that extends the Module class. This file should be located in the /site/modules/PagesMetatags/ directory and have the same name as the module itself, PagesMetatags.php. Here's an example of the initial class structure:

<?php
namespace Scriptor\Modules\PagesMetatags;

use Imanager\Field;
use Imanager\TemplateParser;
use Scriptor\Core\Module;
use Scriptor\Core\Scriptor;
use Scriptor\Core\Site;

class PagesMetatags extends Module {
    /**
     * @var object Instance of  Scriptor\Core\Site
     */
    private Site $site;

    /**
     * @var object Instance of Imanager\TemplateParser 
     */
    private TemplateParser $templateParser;

   /**
    * @var string - Name of the meta field
    */
    const FIELD_NAME = 'meta';

    /** 
     * @var array - Embed custom style sheet
     */
    const FIELD_KEYS = [
        'title' => '',
        'description' => '',
        'keywords' => ''
    ];

    /**
     * Returns information about the module.
     * 
     * @return array
     */
    public static function moduleInfo() : array
    {
       // ...
    }

    /**
     * Returns the module hooks.
     * 
     * @return array
     */
    public static function moduleHooks() : array
    {
       // ...
    }

    /**
     * Initializes the module.
     */
    public function init()
    {
        // ...
    }

    /**
    * Generates and displays markup of meta fields in the page editor.
    * 
    * @param object $event - Hook event
    */
    public function renderPageMetaFields($event)
    {
        // ...
    }

   /**
     * Save the metadata if the page was saved successfully.
     * 
     * @param object $event - Hook event
     * 
     */
    public function savePageMeta($event)
    {
        // ...
    }

    /**
     * Installs the module.
     * 
     * @return bool
     */
    public function install() : bool
    {
       // ...
    }

    /**
     * Uninstalls the custom meta field from the site's pages.
     *
     * @return bool Returns true if the field was successfully uninstalled, false otherwise.
     */
    public function uninstall() : bool
    {
        // ...
    }
}

Within the PagesMetatags class, we have defined the functions and will fill them with the logic later. We have also imported several classes that allow us to work with different objects or data within the module:

use Imanager\Field;
use Imanager\TemplateParser;
use Scriptor\Core\Module;
use Scriptor\Core\Scriptor;
use Scriptor\Core\Site;

Let's start with the moduleInfo() method, which is responsible for providing information about the module to the module manager:

public static function moduleInfo(): array
{
    return [
        'name' => 'PagesMetatags',
        'version' => '1.0.0',
        'description' => 'The PagesMetatags module is a Scriptor editor extension for '.
        'storing and managing page metadata. Optimize your pages for search engines '.
        'and improve visibility by easily adding information like titles, '.
        'descriptions, and keywords.',
        'author' => 'Your Name',
        'author_website' => 'https://your-website.com'
    ];
}

Since we want our module to hook into the page editor and display an additional area for entering metadata under "Create/Edit Page," we need to define the moduleHooks() method. It should look like this:

public static function moduleHooks(): array
{
    return [
        'Pages::afterRenderEditorTemplateField' => [
            [
                'module' => 'PagesMetatags',
                'method' => 'renderPageMetaFields'
            ]
        ],
        'Pages::afterSavePage' => [
            [
                'module' => 'PagesMetatags',
                'method' => 'savePageMeta'
            ]
        ]
    ];
}

Let's incorporate the logic of the installation method into our PagesMetatags class. This method will be utilized by the Module Manager during the installation process to ensure that the required field named "meta" is present when the page editor is opened in the admin area:

public function install() : bool
{
    $this->init();
    // Get the Pages category object from the hooked class.
    $pages = $this->site->pages()->category;
    // Check the field name already exists
    $exists = $pages->getField('name='.self::FIELD_NAME);
    // If the meta field already exists, display the corresponding message.
    if ($exists) {
        $this->addMsg('error', $this->templateParser->render(
            $this->i18n['meta_field_exists_error'], [
                'field_name' => self::FIELD_NAME
            ])
        );
        return false;
    }

    // If the meta field does not exist yet, try to install it
    try {
        $field = new Field($pages->id);

        $field->set('name', self::FIELD_NAME)
                ->set('type', 'array')
                ->set('label', 'Page Metadata')
                ->save();

        $this->addMsg('success', $this->templateParser->render(
            $this->i18n['meta_field_succesful_created'], [
                'field_name' => self::FIELD_NAME
            ])
        );

        return true;

    } catch (\Exception $e) {
        $this->addMsg('error', $this->templateParser->render(
                $this->i18n['meta_field_install_error'], [
                    'error' => $e->getMessage()
                ])
        );
        return false;
    }
}

The same applies to the uninstallation method, uninstall(). This method is called during the uninstallation process using the Module Manager and ensures that the "meta" field is removed from the Page object:

public function uninstall() : bool
{
    $this->init();

    $pages = $this->site->pages()->category;
    $field = $pages->getField('name='.self::FIELD_NAME);

    if ($field) {
        if ($pages->remove($field)) {
            $this->addMsg('success', $this->templateParser->render(
                $this->i18n['meta_field_succesful_uninstalled'], [
                    'field_name' => self::FIELD_NAME
                ])
            );
            return true;
        }
    }

    $this->addMsg('error', $this->templateParser->render(
        $this->i18n['meta_field_not_exists_error'], [
            'field_name' => self::FIELD_NAME
        ])
    );
    return false;
}

So far, I believe it's clear, and we have now added almost all the functions to our module to enable its installation. However, we still need to implement the logic for the init() method, which is required for both the installation and the usage of our module. This method first calls the init() method of the parent class by using the parent::init() syntax. Then, it retrieves the instance of the Site class using Scriptor::getSite(). Finally, it creates a new instance of the TemplateParser class, which is responsible for parsing templates and user notifications and replacing placeholders with values:

public function init()
{
    parent::init();
    $this->site = Scriptor::getSite();
    $this->templateParser = new TemplateParser();
}

Now, add the logic for another method called renderPageMetaFields(). We will hook this method to the standard Pages function in the editor, specifically Pages::afterRenderEditorTemplateField. This means that our method will be executed when the field for entering the template is rendered in the Pages edit section. Our method will generate the markup for the metadata field and add it to the Pages output:

<?php
public function renderPageMetaFields($event)
{
    // Get current page
    $page = $event->object->page;

    // Embed custom style sheet
    $this->addResource('link', [
        'rel' => 'stylesheet', 
        'href' => dirname($this->siteUrl).'/site/modules/PagesMetatags/css/styles.css'
    ], 'header');

    // Use metafield data of the current page, if available 
    $meta = isset($page->meta) ? $page->meta : self::FIELD_KEYS;    

    // Build meta fields markup
    ob_start(); ?>
    <div class="pmt form-control">
        <fieldset>
            <legend><?php echo $this->i18n['metadata_legend']; ?></legend>
            <div class="form-control">
                <label for="metatitle"><?php echo $this->i18n['metatitle_label']; ?></label>
                <p class="info-text i-wrapp"><i class="gg-danger"></i> <?php
                    echo $this->i18n['metatitle_field_infotext'] ?></p>
                <input name="metatitle" id="metatitle" type="text" value="<?php 
                    echo $meta['title']; ?>">
            </div>
            <div class="form-control">
                <label for="metadescription"><?php 
                    echo $this->i18n['metadescription_label']; ?></label>
                <p class="info-text i-wrapp"><i class="gg-danger"></i> <?php
                    echo $this->i18n['metadescription_field_infotext'] ?></p>
                <input name="metadescription" id="metadescription" type="text" 
                    value="<?php echo $meta['description']; ?>">
            </div>
            <div class="form-control">
                <label for="metakeywords"><?php 
                    echo $this->i18n['metakeywords_label']; ?></label>
                <p class="info-text i-wrapp"><i class="gg-danger"></i> <?php
                    echo $this->i18n['metakeywords_field_infotext'] ?></p>
                <input name="metakeywords" id="metakeywords" type="text" value="<?php 
                    echo $meta['keywords']; ?>">
            </div>
        </fieldset>
    </div>
    <?php
    // Add the markup to the hooked methods return
    $event->return .= ob_get_clean();
}

Next, let's incorporate the logic for the final method, savePageMeta(), into our class. This method will be hooked to the Pages::afterSavePage function and will ensure that the metadata entered by the administrator in the meta fields is saved when the save button is clicked during page editing or creation:

public function savePageMeta($event)
{
    // Check if saving the page was successful, if not just leave
    if (!$event->return) return;
    $page = $event->object->page;
    // Prepare meta before saving
    $meta = [
        'title' => $this->sanitizer->text($this->input->post->metatitle),
        'description' => $this->sanitizer->text($this->input->post->metadescription),
        'keywords' => $this->sanitizer->text($this->input->post->metakeywords)
    ];
    // Set metadata and save the page
    $event->return = $page->set('meta', $meta)->save();
    // Display error message if saving did not succeed
    if (!$event->return) {
        $this->addMsg('error', $this->templateParser->render(
            $this->i18n['error_saving_meta'])
        );
    }
}

Furthermore, we need to add styles to ensure that the appearance of the meta fields aligns with the design of the page editor. You can achieve this by inserting the following CSS code into the CSS file located at site/modules/PagesMetatags/css/styles.css:

.pmt fieldset {
    box-sizing: inherit;
    padding: 20px;
    border: 1px solid rgba(0,0,0,.07);
    border-radius: 3px;
}

.pmt fieldset legend {
    font-weight: 700;
    padding: 0 5px;
}

Lastly, we will generate the translation file for the module, beginning with en_US.php. You have the option to expand it by including translations for other languages as required:

<?php
return [
    'metadata_legend' => 'Meta Data',
    'metatitle_label' => 'Meta title',
    'metatitle_field_infotext' => 'Enter a meta title (also called title tag).',
    'metadescription_label' => 'Meta description',
    'metadescription_field_infotext' => 'Enter the meta description (also called a meta description attribute or tag).',
    'metakeywords_label' => 'Meta keywords',
    'metakeywords_field_infotext' => 'Enter your keywords separated by commas.',
    'error_saving_meta' => 'The metadata could not be saved!',
    'meta_field_exists_error' => 'A field with the name <strong>[[field_name]]</strong> already exists in Scriptor\'s Page object.',
    'meta_field_succesful_created' => 'The Scriptor\'s Page object has been successfully extended with a new field <strong>[[field_name]]</strong>.',
    'meta_field_install_error' => 'An exception occurred during field installation: [[error_msg]]',
    'meta_field_succesful_uninstalled' => 'The field [[field_name]] was successfully removed from the Scriptor\'s Page object.',
    'meta_field_not_exists_error' => 'A field named <strong>[[field name]]</strong> was not found in the Scriptor\'s Page object.'
];

Begin the installation process by clicking on the "Modules" menu in the editor and selecting the PagesMetatags module from the list. Then, click on the "Install" button.

When you open a page in edit mode, you will observe the existence of an extra Meta Data field.

How to retrieve stored meta values in the frontend of your website

To access the metadata of a page, use the following code:

echo $site->page->meta[~YOUR_FIELD_NAME~];

To retrieve the meta title, simply incorporate the following code snippet into your template, for example:

<title><?php echo $site->page->meta['title'] ?? ''; ?></title>

To retrieve the meta description, use this code:

<meta name="description" 
    content="<?php echo $site->page->meta['description'] ?? ''; ?>">

Similarly, you can retrieve the keywords using the this:

<meta name="keywords" content="<?php echo $site->page->meta['keywords'] ?? '' ; ?>">

If you happen to forget to specify the meta title, it is crucial to display the default page title as an alternative. This is essential because the page title carries significant importance and should never be left empty:

<title><?php echo !empty($site->page->meta['title']) ? $site->page->meta['title'] : 
        $site->name; ?> - <?php echo $site->config['site_name']; ?></title>


Here you can download files used in this tutorial: PagesMetatags.zip