WordPress: Loading Siteorigin Layout into another page with Ajax

Tl; dr;


I needed a way to dynamically load the content of a WordPress page into another. That I managed to do. You can find the code to copy and paste in the end of this post.

Background


In the beginning, this page was not supposed to be a professional homepage, so didn't feature a portfolio, resume, and stuff. Now this changed and the time came where I needed a professional portfolio. As a matter of fact, I don't own the rights to the projects I worked on for my job. That is why I'm not really allowed to present them on a private homepage. My employer made a point of that.


Compromise

Anyone can image that it's rather awful for a job hunt not to be able to show one's work. My boss, a totally rad dude by the way, offered the solution of hiding everything behind a passwort - as a compromise. The most important thing was, that an affiliation between me, the projects, and my firm would not be publicly available.


Yeah, what can I say? That employer is a little old-school. Whatever, I did manage after all.

Alternatives


It is pretty easy to add password protection to a page via the wordpress page settings, so I tried this first. The result was, to put it mildly, a usability disaster! Every page had it's own password, which the user had to input for each of the projects, and navigation was a nightmare.


Next I tried various WordPress plugins to find a way to only password protect certain parts of a page. However, since I'm using the SiteOrigin Pagebuilder to build my pages, that also didn't work.


Eventually I gave into my own drive to tinker and programmed the whole thing myself, even if it took quite some time.

The Path


The path to a functioning portfolio was rocky. Part of it can be seen here. I will go into everything here.


Ajax

Ajax is used to communicate with the server after a homepage has been loaded. Data can be requested, which changes the page without the need of a complete reload.

So I could use Ajax to call a Php function on the server with Javascript, which then would hand me back the SiteOrigin layout of any page I wanted. This I could put into a designated Div container in the HTML and voilĂ .


Php

<?php
    
    // The two following lines belong into the theme's functions.php or wherever scripts are
    // embedded for Plugins or Widgets. "portfolio-page.js" is the script I want to make
    // Ajax calls from.
    
    // This is the wordpress way of embedding scripts into a page.
    wp_enqueue_script('portfolio-page', get_template_directory_uri() . '/js/portfolio-page.js', array('jquery'), false, false);
    
    // This means: "Allow portfolio-page to call stuff with Ajax."
    wp_localize_script('portfolio-page', 'ajaxAdmin', array( 'ajaxurl' => admin_url( 'admin-ajax.php' )));


    
    // These two lines belong into functions.php as well. They map the Ajax calls to the respective
    // Php methods. It's important that the parameters match exactly, e.g. here "tne_load_post_content"
    // (yes, including the wp_ stuff).
    add_action( 'wp_ajax_tnet_load_post_content', 'tnet_load_post_content' );
    add_action( 'wp_ajax_nopriv_tnet_load_post_content', 'tnet_load_post_content' );

   
   
    // This is the actual method that is called from the Javascript.
    function tnet_load_post_content()
    {
        $id = $_POST['pageId']; // Parameters sent by Javascript can be accessed like this.
        
        // Here it is checked, if the current page is one with a Siteorigin layout.
        $meta = get_post_meta( $id, 'panels_data', true );
        if( class_exists( 'SiteOrigin_Panels' ) && $meta )
        {
            // Yes: Use Siteorigin magic to get the layout.
            $renderer = SiteOrigin_Panels::renderer();
            $content = $renderer->render( $id );
        }
        else
        {
            // No: Get the layout the standard Wordpress way.
            $content = apply_filters( 'the_content', get_page($id)->post_content );
        }
        
        // Hand the layout back to the caller.
        echo $content;
        
        // With this wordpress will at a "0" in the response.
        die(); 
    }
?>

Javascript

function loadContent(id)
{
    // Here I build an object that is sent with the Ajax call.
    // It contains the Php method to call, and the Id of the
    // wordpress page to be loaded.
    var data = { 
        action: "tnet_load_post_content",
        pageId: id
        };
    
    // Builds the Ajax call, executes it, and calls onContentLoaded on success.
    useAjax(data, onContentLoaded);
}

function useAjax(data, onSuccess)
{
    $.ajax({
        type: "post",
        cache: false,
        
        // The target for the wordpress Ajax call, was enabled via wp_localize_script() in the Php code.
        url: ajaxAdmin.ajaxurl,
        
        // The parameters that get send along
        data: data, 
        
        // Method that is called on success
        success: onSuccess
    });
}

function onContentLoaded(response)
{
    // The loaded content is put into the designated container.
    contentContainer.html(response);
}

I'm not much of a web developer. To be perfecly honest I find HTML, CSS, and especially Php borderline retarded! I put this page together quite naively and therefore had absolutely no clue what Ajax is or how it works.


Wrong CSS

So I managed to load a page of my choosing into another page. But something was off. Margins were wrong or Images looked weird. What was going on? SiteOrigin is adding classes and ids to elements depending on the page that is visited, and then embeds its own respective styles to the page.

For example, I was on the page with the Id 1234 and want to load the layout for page 9876. When visiting page 1234 SiteOrigin adds the id 1234 to all HTML elements and loads the CSS for page 1234. Now I load 9876 into 1234, which then has no styles, because the styles there correlate to 1234. Have I lost you, yet?


Php

// Get the desired pages's content and the styles.
$renderer = SiteOrigin_Panels::renderer();
$content = $renderer->render( $id );
$css = $renderer->generate_css( $id );

// Sent both to the Javascript as a JSON.
echo json_encode(array($content, $css));

Javascript

function onContentLoaded(response)
{
    // Parse the JSON into a Javascript object.
    var data = JSON.parse(response);

    // The content goes into the designated container.
    contentContainer.html(data[0]);
    
    // The styles are replaced.
    addMissingStyles(data[1]);
}

function addMissingStyles(css)
{
    if(css == '')
    {
        return;
    }
    
    // First I remove the old wrong styles.
    var siteOriginId = "#siteorigin-panels-layouts-head";
    tryRemove(siteOriginId);
    
    // Dann I manually embed the new styles.
    var styles = '<style type="text/css" media="all" id="'+siteOriginId+'">'+css+'</style>';
    $('head').append(styles);
}

function tryRemove(id)
{
    var element = document.getElementById(id);
    if(element !== null)
    {
        element.parentNode.removeChild(element);
    }
}

I even asked a web developer colleague of mine for a better way, but he drew the same conclusions: Not only did I have to load the page layout but also its CSS, and add it manually into the page. This way I beat the two greatest challenges here.

More


After the main features worked, there were a few smaller issues I still wanted to handle.


The Gallery Widget

For this page I added an own gallery widget to the theme, which relies on it's own Javascript. So I had to check the loaded layout for the widget and subsequently added the script in manually.


Javascript

function addMissingScripts()
{
    // I wait a bit because the widget might not be there, yet.
    setTimeout(function()
    {
        if( $(".gallery-widget").length ) //<- This means: Does "gallery-widget" exist?
        {
            if( $("#gallery-widget-js").length ) //<- This means: Does the gallery-widget Javascript already exist?
            {
                return;
            }
            
            // Here I'm embedding the script from the theme manually.
            $("head").append('<script type="text/javascript" src="https://taijj.net/wp-content/themes/tnet/js/gallery-widget.js?ver=5.5" id="gallery-widget-js"></script>');
        }
    }, 1000);
}

Language Switch

I'm using the Polylang Plugin to show buttons for the two languages this homepage supports. Those reload the page via a normal link in another language. I had to adjust their links to point to the respective content.


Javascript

function updateLanguageButtons()
{
    $('.language-switcher .lang-item a').each(updateUrl);
    $('.header-language-section .lang-item a').each(updateUrl);

    function updateUrl()
    {
        var item = $(this);
        var url = item.attr('href');

        var hashIndex = url.indexOf('#');
        if(hashIndex >= 0)
        {
            url = url.substr(0, hashIndex);
        }
        item.attr('href', url+window.location.hash);
    }
}

The Password Feature

This post is already quite long so I maybe write about this in more detail some other time. I had to show the password form, set cookies, and generally validate the password.

Conclusion


It was a long way, but I think it was worth it. Here you'll find the relevant code again in its entirety:


Php

add_action( 'wp_ajax_tnet_load_post_content', 'tnet_load_post_content' );
add_action( 'wp_ajax_nopriv_tnet_load_post_content', 'tnet_load_post_content' );

function tnet_load_post_content() {

    $id = $_POST['pageId'];
    $meta = get_post_meta( $id, 'panels_data', true );

    if( class_exists( 'SiteOrigin_Panels' ) && $meta )
    {
        $renderer = SiteOrigin_Panels::renderer();
        $content = $renderer->render( $id );
        $css = $renderer->generate_css( $id );
    }
    else
    {
        $css = '';
        $content = apply_filters( 'the_content', get_page($id)->post_content );
    }

    echo json_encode(array($content, $css));
    die();
}

Javascript

The complete Javascript is quite long and includes much more. You may see it here.


Troubleshooting

  • When embedding another Javascript, which a loaded in page relies on, I needed a Timeout before embedding for everything to work.
  • This won't work on page layouts that are protected by a WordPress passwords!