WP Page Builder From Scratch #4: Front-End Output, Theme Dependency, Saving Data to Content

This is Part #4 (Final) of Custom Page Builder Tutorials Series. Read the intro here.

You can download / check github repo for example plugin:

And here’s the video of the plugin in action:

Let’s start.

Output Page Builder Data

It’s very similar to fxpb_render_rows() function to display rows control in earlier tutorial, but this time we are going to create a function for front-end.

/**
 * Page Builder Content Output
 * This need to be in the loop.
 * @since 1.0.0
**/
function fxpb_get_content(){

    /* Get saved rows data and sanitize it */
    $row_datas = fxpb_sanitize( get_post_meta( get_the_ID(), 'fxpb', true ) );

    /* return if no rows data */
    if( !$row_datas ){
        return '';
    }

    /* Content */
    $content = '';

    /* Loop for each rows */
    foreach( $row_datas as $order => $row_data ){
        $order = intval( $order );

        /* === Row with 1 column === */
        if( 'col-1' == $row_data['type'] ){
            $content .= '<div class="fxpb-row fxpb-row-' . $order . ' fxpb-col-1">' . "\r\n";
            $content .= '<div class="row-content">' . "\r\n\r\n";
            $content .= $row_data['content'] . "\r\n\r\n";
            $content .= '</div>' . "\r\n";
            $content .= '</div>' . "\r\n\r\n";
        }
        /* === Row with 2 columns === */
        elseif( 'col-2' == $row_data['type'] ){
            $content .= '<div class="fxpb-row fxpb-row-' . $order . ' fxpb-col-2">' . "\r\n";
            $content .= '<div class="row-content-1">' . "\r\n\r\n";
            $content .= $row_data['content-1'] . "\r\n\r\n";
            $content .= '</div>' . "\r\n";
            $content .= '<div class="row-content-2">' . "\r\n\r\n";
            $content .= $row_data['content-2'] . "\r\n\r\n";
            $content .= '</div>' . "\r\n";
            $content .= '</div>' . "\r\n\r\n";
        }
    }
    return $content;
}

It’s crucial to format the data with proper line break when using it in content.

To use this plugin, we need to create a page template “templates/page-builder.php” in our theme and replace the_content() with echo fxpb_get_content() above.

And to make sure we display the rows and columns correctly, we load CSS in that page template:

/* Enqueue Script */
add_action( 'wp_enqueue_scripts', 'fx_pbbase_front_end_scripts' );

/**
 * Admin Scripts
 * @since 1.0.0
 */
function fx_pbbase_front_end_scripts(){

    /* In a page using page builder */
    if( is_page() && ( 'templates/page-builder.php' == get_page_template_slug( get_queried_object_id() ) ) ){

        /* Enqueue CSS & JS For Page Builder */
        wp_enqueue_style( 'fx-page-builder', FX_PBBASE_URI. 'assets/page-builder-front.css', array(), FX_PBBASE_VERSION );
    }
}

And here’ s a very simple CSS to display column:

.fxpb-row:after{
    content:".";display:block;height:0;clear:both;visibility:hidden;
}
.fxpb-col-1 .row-content{
    width: 100%;
}
.fxpb-col-2 .row-content-1{
    width: 48%;
    float: left;
}
.fxpb-col-2 .row-content-2{
    width: 48%;
    float: right;
}

And after that, we’ll have this: nice!

page-builder-back-front

So, it works. But not really, because of several reasons:

Problem with Page Builder

Require theme template modification
It’s not ideal, It should work without theme mod.

Default content filter not working
Content filter such as shortcode, auto paragraph, auto embed, responsive image, etc are not working.

Not searchable
WordPress search will not check Page Builder content (custom fields). It will only search for post_title and post_content.

Original content still the same
When no longer using the plugin, all content will be gone/inaccessible.

Now: delete that page template “templates/page-builder.php” from your theme. We no longer need that 🙂

Solution Strategy

To solve this I decided to do this:

  1. On save_post, clone page builder data to post_content without rows/columns wrapper formatting.
  2. Filter the_content to output page builder content with the rows and columns wrapper (as above).

Note:
That is the strategy I decided for this plugin. You may agree/disagree with it. But I’ll try to explain the reason below.

Save Page Builder Data in Post Content

To do this, we add a little code in our save post function fx_pbbase_save_post() :

/* == Page Builder Template Selected, Save to Post Content == */
if( 'templates/page-builder.php' == $page_template ){

    /* Page builder content without row/column wrapper */
    $pb_content = fxpb_format_post_content_data( $submitted_data );

    /* Post Data To Save */
    $this_post = array(
        'ID'           => $post_id,
        'post_content' => sanitize_post_field( 'post_content', $pb_content, $post_id, 'db' ),
    );

    /**
     * Prevent infinite loop.
     * @link https://developer.wordpress.org/reference/functions/wp_update_post/
     */
    remove_action( 'save_post', 'fx_pbbase_save_post' );
    wp_update_post( $this_post );
    add_action( 'save_post', 'fx_pbbase_save_post' );
}

/* == Always delete page builder data if page template not selected == */
else{
    delete_post_meta( $post_id, 'fxpb' );
}

In the code above I create a function to format page builder data as plain text string.

/**
 * Format Page Builder Content Without Wrapper Div.
 * This is added to post content.
 * @since 1.0.0
**/
function fxpb_format_post_content_data( $row_datas ){

    /* return if no rows data */
    if( !$row_datas ){
        return '';
    }

    /* Output */
    $content = '';

    /* Loop for each rows */
    foreach( $row_datas as $order => $row_data ){
        $order = intval( $order );

        /* === Row with 1 column === */
        if( 'col-1' == $row_data['type'] ){
            $content .= $row_data['content'] . "\r\n\r\n";
        }
        /* === Row with 2 columns === */
        elseif( 'col-2' == $row_data['type'] ){
            $content .= $row_data['content-1'] . "\r\n\r\n";
            $content .= $row_data['content-2'] . "\r\n\r\n";
        }
    }
    return $content;
}

So the code above will “clone” page builder data to post content without any wrapper.

And if we re-save our page builder we will have this in our post content:

page-builder-back-front-clean-data

Why Save it without the row/columns div wrapper?

This data  stored as post content for two reason:

  1. To make the data search-able.
  2. So user don’t lose the data when they deactivate/no longer use the plugin.

If we add div wrapper (like many other page builder plugins) we will have this after we no longer use the plugin/theme:

Make Theme
After no longer use the theme:

make-theme-content
Sliders data is the worst.

Site Origin Page Builder
After deactivate the plugin:

site-origin-pb-content
All your dynamic data (such as categories list) now become static HTML.

That’s awful right?

It’s true that the content is there. but the user would not be able to edit that. I prefer to have just the content with proper content markup so I can edit it comfortably.

For dynamic/complex HTML? use shortcode to save it in post content.

I personally think, theme that do this is as bad as theme with content shortcodes.

That divs wrapper is useless without the theme/plugin. We can style it, but to add the CSS to your theme for back-compat is terrible idea, pretty much cannot be edited by user, the divs and classes is not visible in visual editor, and definitely cause buggy experience to user. (just a thoughts)

Filter the_content to display page builder data

So, the final thing to do is to filter post content with the “real” page builder data, full content with all rows and columns div so we can style it.

/* Filter Content as early as possible, but after all WP code filter runs. */
add_filter( 'the_content', 'fxpb_filter_content', 10.5 );

/**
 * Filter Content
 * @since 1.0.0
**/
function fxpb_filter_content( $content ){

    /* In single page when page builder template selected. */
    if( !is_admin() && is_page() && 'templates/page-builder.php' == get_page_template_slug( get_the_ID() ) ){

        /* Add content with shortcode, autoembed, responsive image, etc. */
        $content = fxpb_do_content_filter( fxpb_get_content(), get_the_ID() );
    }

    /* Return content */
    return $content;
}

And the function to add default filter to content, so it will work like normal content (shortcode, oembed, etc):

/**
 * Enable Default Content Filter
 * @since 1.0.0
 */
function fxpb_do_content_filter( $content, $post_id ){
    if( $content ){
        global $wp_embed;
        $content = $wp_embed->run_shortcode( $content );
        $content = $wp_embed->autoembed( $content );
        $content = wptexturize( $content );
        $content = convert_smilies( $content );
        $content = convert_chars( $content );
        $content = wptexturize( $content );
        $content = do_shortcode( $content );
        $content = shortcode_unautop( $content );
        if( function_exists('wp_make_content_images_responsive') ) { /* WP 4.4+ */
            $content = wp_make_content_images_responsive( $content );
        }
        $content = wpautop( $content );
    }
    return $content;
}

And done.

Note: Cons for this method
If you switch to regular editor, (edit the content) and save the post, all your page builder rows is gone. and you will need to re-do all the rows. (all page builder works this way)

One solution is to not delete page builder data when user switch to visual editor. (but then user will see old and maybe un relevant data)

The better solution is to utilize page revision so this problem will not exist in the first place. (user will be able to restore deleted page builder data anyway).

This tutorial series is already too long (and boring). I’ll probably cover how to utilize page builder revision in another day 🙂

And that’s it. All done. It’s a wrap!

I hope this plugin is useful. Download, Fork, do whatever you want, and build great stuff with it.

Sorry for the long post. Here’s a potato:

potato-thanks-for-reading

11 Comments

  1. Donna

    That was an extraordinarily remarkable series of posts. I very much appreciate the detailed explanations throughout. Thank you! I have downloaded the plugin to test and learn.

    Reply
  2. Ahmad Awais

    Hey, David!
    Incredible series of posts. Truly enjoyed it and your effort is evident.

    Can you add GPL or MIT license to the code on GitHub so that I could use it in my projects without any fear of running into licensing issues? That’d be great.

    Looking forward!

    Reply
  3. Shak

    Hi David,

    I love the way you explain the builder and specially save content without wrapper. I have two question here for you.

    1. If I am using Visual Composer it has too many shortcodes how to save the content without wrappers.
    2. as you have your own data structure and you are uisng fxpb_get_content function to render data.
    2.a. Can we pre-render the function and set the global $post->post_content = fxpb_get_content(); So the content will be available for all the functions which you manually did in fxpb_do_content_filter()

    Please explain. Thanks in advance

    Reply
  4. vimal

    Hi David,

    this article was extremely helpful for my magazine blog in wordpress.
    I have small doubt about adding drag and drop instead of existing clicking function to add row template in target blank template..

    I hope you will help me.

    Advance thanks.

    Reply
  5. sharojit

    Hey David,

    Really helpful article and you made my day. Wondering if any more tutorial published on this topic or any helpful resource like you gave :). That would be very much appreciated. Wish you all the very best.

    Thanks

    Reply
  6. moses

    thank`s for this helpful article. i wish for you more better potatto articles. thanks thanks

    Reply
  7. Raunak Hajela

    Nice article. I want to make a page builder plugin but instead of selecting page builder option from dropdown I want to add just a button. When you click on that it opens a drag on drop page builder having widget library on sidebar and design at right side just like WordPress customizer or elementor. How can I do that?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *