Simple SEO metadata and Open Graph in Enonic XP

Posted by

    Here's a working example of an Enonic XP Page Template setting all the correct Open Graph data and other common SEO metadata (except keywords, since that has become more or less obsolete).

    This text is written to work on 5.0, 5.1, 5.2, and 5.3-versions of Enonic eXperience Platform. In 6.0 the folder structure and some code will change. It also uses code from the Lib UTIL project and the Skeleton Module project on GitHub.

    Something we've seen a huge increase in the last few years is issues around Open Graph implementation. It's not complicated to get it working, it just takes you some time to set it all up, and then research a lot - if things go wrong. And it's the same with SEO meta data, though we've had them for many years now so most of us could set that up in our sleep. Anyways, let me save you some time! Here's a working Page Template that creates all that code for you, with a step-by-step walkthrough of the code.

    Create a Mixin

    The first thing we must do for this to work in a EXP module is to create and use a mixin directly in our module.xml. This will let you add input fields on every content on your site automatically. The fields we will put in here are "Overwrite Title" and "Overwrite Description". These field will let any content easily overwrite their metadata, if needed.

    Here's a working example of this mixin. Just create a file called "mixin.xml" and paste this code into it. Place the file inside a folder called "metadata" (but you can really call it anything as long as it explains to you what it does). This new folder goes into the "mixin" folder in the cms root-folder for your module. 

    Module.xml and how to use a Mixin

    And this is how we'll use this mixin in our module.xml making it appear in every content we add to our site (even for the built in base content types). I expect you to at least have created your own module in EXP so that you know where to find this file.

    Example Content Type

    Now we can just create our Content Types as usual. You don't have to add anything extra to their configs as the module.xml will add the mixin everywhere for you. But make sure to use Enonic's common naming convention:

    • The preface field should be named "preface"
    • Image fields should be named "image" or "images" depending on if they allow one or many images

    When you create any content out of this Content Type you'll see at the bottom that our fields from the Mixin automatically popped up. You don't have to fill them in, but you can. Normally you would just use the preface field for meta description, and the display name/title for page title. But some times you need the ability to overwrite this per content, and that's why we set it up this way.

    Let's see a very small "article" Content Type example using this naming convention.

    See, nowhere is the Mixin mention in this Content Type. It's automatically added to all content via the module.xml file. You might feel this is a bit bloated since it's not needed everywhere, but in future versions of EXP we will let you filter this, so let it be like this for now.

    Now, we will create our Page Template. Call it "article" and create an empty "config.xml" (model), an empty "article.html" (view), and an empty "controller.js".

    The View

    Here's an example of our view.

    It's really basic. It just expects a lot of variables coming from our controller.js and outputs their values on the right places.

    The Controller

    And here's an example of our controller without any fancy code in place yet. Just placeholders.

    Our controller.js does all the magic. In the beginning of the function "createModel" we'll request all the data we need from the internal storage engine with "portal:getSite" and "portal:getContent". The first contains data from and about our module, the second one from our content. = execute('portal.getSite');
    me.content = execute('portal.getContent');

    SEO metadata

    To get title and description we will call two functions that we will place at the end of this file later. Calling them simply returns the data we need and return it to our view. Add these two rows instead of the commented text.

    model.pageTitle = getPageTitle();
    model.metaDescription = getMetaDescription();

    These functions will be explained in detail shortly. Just focus on setting this up for now.

    Open Graph

    Next is the time to return some Open Graph values. First we'll gather the simple ones, I guess all of them explains themselves. Add them at the next commented line.

    var og = {};
    og.title = model.pageTitle;
    og.description = model.metaDescription;
    og.sitename =['displayName'];
    og.locale = "nb_NO"; // Work in progress to automate this ...

    We'll then set the "og:type" metadata. According to the documentation on Facebook and Open Graph we'll need to set the type to what we are displaying. For most websites not selling things this will be "website" on the front page, and "article" on all other pages. This simple line will set that for us by checking if the modules root path equals current path/url.

    og.type = ( model.sitePath === me.content["_path"] ) ? "website" : "article";

    Also add the current URL for this page.

    var currentpage = execute('portal.pageUrl', {
      path: me.content["_path"]
    og.url = "" + currentpage;

    For now the protocol and domain name part must be hard coded, we'll expect automatic fetching of this in the near future.

    And so the image that we wants to share. All the magic behind selecting the right one is in the dedicated function for this explained in detail later. We'll send a default images to the function so that we'll always have something to display.

    og.image = getOpenGraphImage('images/og_default.jpg');
    og.image_width = 600;
    og.image_height = 315;

    We send the image dimensions 600 and 315 hard coded without ever checking them because we can actually guarantee the image to always have these dimensions when it's created later. So make sure that your supplied default picture is 600 x 315 since we won't format that in any way for you.

    And then, before returning the model, we'll just add the og object to the model itself.

    model.og = og;

    You should now have the code for the SEO and Open Graph commented sections in your controller.js. Let's add the rest!

    Let's head on to the functions we've been calling. They should all be placed at the end of your "getView" function (but still inside of it) so that it can access all your collected data without sending it around.


    First this function gets a short module name, from our database, that it can work with. It replaces all the dots in it's name with dashes because that is the name of the JSON node where the data is stored, and we want this to be as dynamic as possible. It will then first try to check that extra SEO data we added with our Mixin. Does it even exist? If so, check if it's filled in and if so add it as the return data.

    If we can't find any text overwriting the title we'll "waterfall" down to the actual contents own title, it's "displayName". If that too is empty we'll take the title from our Site settings (the field added to our module.xml called "title").

    When we have some kind of title to start with we'll return that, with the site/module name. You can change the site name easily in EXP by double clicking the Site in Content Manager.


    This function is very much like the getTitle one. We do almost the same things on each row (yes I know, I could probably pretty easy refactor these two into one). But instead of fetching the title we'll fetch the meta description. First from the SEO mixin, then from the content, then from the Site settings in Content Manager.


    Here's the last function. It's a bit different from the other two since it handles images. We'll check if the content in the database has any "image" or "images" node. If so we can assume that we found content with an image we can use on Facebook.

    Inside the if-statement we'll make sure that we'll work with an array no matter if it's just one single image we have to work with. This way we can just take the first image from the list even if the content has multiple images. After checking that the array is not empty we'll send the image ID (that is first in the array) to the EXP imageUrl function. This will scale and return a URL to us.

    When all that is done we'll just check if the returning URL is empty. If yes, no image was found, and we can return the default image instead. So there will always be one image sent to Facebook, and that will always be 600 x 315 (their recommended image size for the largest possible horisontal style sharing box).


    At last, we have the html result code that outputs the correct meta data for title, description (SEO) and Open Graph (for sharing on Facebook and LinkedIn). Just set your Page Template controller.js up as a Page Template in Enonic admin and tell it to support the "article" Content Type.

    A lot can be improved in this example, but this can get you started with Open Graph and SEO metadata in EXP with just some copy and paste.

    The entire controller.js file: