Coded something up in Couch in an interesting way? Have a snippet or shortcode to share? Post it here for the community to benefit.
10 posts Page 1 of 1
I've been posed this question a few times by other developers and have today began thinking about it myself, having used the "load more" button that KK kindly released here...

The question pertains to SEO with this button and/or infinite scrolling with couch pages.

Google's recommendation for SEO friendly infinite scrolling can be found here...

So, with all this in mind - is this possible within couch, using cloned pages and pagination?

Specifically, the <link rel="next" href="nextpaginatedpage"> and <link rel="prev">

but also, every other step within this google recommendation, to maintain the best SEO possible.

edit: also, is it possible to make searching for nonexistant pages return the 404 page (example: http://www.example.com/paginate/?pg=999 when there are only 500 pages (usually this would just return no content).

Dave
Image
Pertinent point, Dave.

Thanks for the link. I'll study it and see if we can device a solution.
Hello,
I see that is a long time since this topic.
I want to ask KK - now has a solution this SEO problem?
Thanks
Hello,

From what I could see in the demo provided by Google (http://scrollsample.appspot.com/items), the linked JS code makes AJAX calls to the server asking for specific pages from the paginated set (what, for example, the '?pg=21' does in Couch).

I think it shouldn't be too difficult to make it call a Couch template on the server that returns such data.

JS, unfortunately, is not my forte, so it has to be someone more knowledgeable who will have to port the sample to Couch.
@Cheesypoof happens to be busy at the moment. Perhaps @Bartonsweb would like to try his hand?
Thanks KK, :) :)
Friends let those who can help.
Because it's a good idea to use the opportunity for Infinite scrolling/load more button with AJAX in COUCH!
Thank you all in advance!
Hi all,

Complete tutorial below.

If you have some listing of items with <cms:pages /> or <cms:query /> tags, here is how to add "loading more" functionality with auto-loading on scroll or click.
Here is the button.
ScreenCut-01-03---14-14-32-.png
load more button
ScreenCut-01-03---14-14-32-.png (5.13 KiB) Viewed 13701 times

When visitor scrolls to the bottom of the container with your listing, this button should be 'clicked' via JS script, which follows:
Code: Select all

    // BEST SCROLLER: http://stackoverflow.com/a/15382570
    var _throttleTimer = null;
    var _throttleDelay = 100;
    var $window = $(window);
    var $document = $(document);

    $document.ready(function () {

        $window
            .off('scroll', ScrollHandler)
            .on('scroll', ScrollHandler);

    });

    function ScrollHandler(e) {
        //throttle event:
        clearTimeout(_throttleTimer);
        _throttleTimer = setTimeout(function () {
            console.log('scroll');

            //do work
            if ($(window).scrollTop() >= $("div#listing-wrapper").height() - 200 ) {
                   console.log('Click triggered')
                   $('a#load-more').trigger('click');
            }

        }, _throttleDelay);
    }



Code above looks for the div id="listing-wrapper" and calculates dynamically current position on page. div id="listing-wrapper" is just my imaginary div, that has inside all the items listed by <cms:pages />. If the height from the top of the page goes beyond the height of "listing-wrapper" minus 200px, then script takes our button (<a id="load-more"></a>) and clicks it.

Now here is what happens in JS when we click the button. The following code is self-explanatory for a JS guy, but please ask questions is it's not clear.
Code: Select all


$('a#load-more').on('click', function (event){

    var $_button = $(this);                     // link that is clicked
    var _options = $_button.attr('data-filt');  // custom_fields of cms:pages
    var _current = $_button.attr('data-curr');  // already loaded page
    var _mylimit = $_button.attr('data-limt');  // limit of cms:pages - number of items per page
    var _mytotal = $_button.attr('data-totl');  // how many total pages are there # used to hide button

        $.ajax({
            url: 'assets/php/ajax/connect.php', // template that handles ajax request
            data: {
                // name of the snippet that has all the code that processes ajax request
                // and sends back the complete html for new items:
                snippet: 'ajax-files/load-more.html',
                options: _options,
                curpage: _current,
                mylimit: _mylimit
            },
            timeout: 10000
        })
        .done(function(result) {

                // general utility function
                $('div#listing-wrapper').append( result );
                $(document).ready(function () {
                    // Action after append is completly done
                    console.log('New content added.');
                    // Update the info about current loaded page, by incrementing after each successful ajax request.
                    $_button.attr('data-curr', parseInt( _current ) + 1 );
                    // Completely replace the content of the button #this can be done differently via inner span..
                    $_button.html("LOAD MORE<br><br>[" + _current + "/" + _mytotal + "]");
                    // If this load hits the last page, hide the button, since there is no items left to be loaded again.
                    if ( parseInt( _current ) + 1 >= _mytotal ) $("#load-more-container").fadeOut('fast').remove();
                });

                /* following sample is for disgusting 'Cube Portfolio'
                $('#js-grid-lightbox-gallery').cubeportfolio('appendItems', result, function(){
                    // Upon successful loading of items into Cube, update all infos:
                    $("#load-more").attr('data-curr', parseInt( _current ) + 1 );
                    $("#load-more").html("LOAD MORE<br><br>[" + _current + "/" + _mytotal + "]");
                    if ( parseInt( _current ) + 1 >= _mytotal ) $("#load-more-container").fadeOut('fast').remove();
                });
                */


        })
        .fail(function(msg) {
            console.log('Ajax error: ' + msg.statusText);  // log errors
        });

    return false;   // don't follow href of the link

});


Before we can use these scripts, let's get through the changes to the couch code for listing.
I'll assume that all items are wrapped inside the div id="listing-wrapper" with some default illustrative code:
Code: Select all
<div id="listing-wrapper">
   <cms:pages masterpage='blog.php' custom_field="author=admin" >
        <div class="post">
            <!-- Post Title -->
            <h3 class="title"><a href="<cms:show k_page_link />"><cms:show k_page_title /></a></h3>
            <!-- Post Date -->
            <p class="sub"><cms:date k_page_date format='jS M, y'/></p>
            <!-- Post Image -->
            <img class="thumb" alt="" src="<cms:show blog_image />" />
            <!-- Post Content -->
            <cms:excerptHTML count='75' ignore='img'><cms:show blog_content /></cms:excerptHTML>
            <!-- Read More Button -->
            <p class="clearfix"><a href="<cms:show k_page_link />" class="button right"> Read More...</a></p>
        </div>
   </cms:pages>
</div>

With the code above, for example, you listed all posts in blog.php that were written by author admin. That listing can have pagination or can be a plain list without pagination as in the sample above.
First, let's add pagination and limit our listing to some default value, that administrator may comfortably set, for example, in some variable in globals.php template.
Code: Select all
<!-- Reference code for globals.php template (note: my /couch folder has been renamed to /cms): -->

<?php require_once( 'cms/cms.php' ); ?>
<cms:template title='Global settings' clonable='0' executable='0' >
    <cms:embed "editables/<cms:show k_template_name />.html" />
</cms:template>

<?php COUCH::invoke(); ?>

<!-- /end of template-->

<!-- And code in the embedded file /snippets/editables/globals.php.html is: -->

<cms:editable type='text' name='records_visible' label='Number of visible records on the front page' desc='Enter any number or 0 to display all' order='10' >8</cms:editable>

<!-- /end of snippet -->

So, we let admin to enter some number in 'Global settings' template and start to use it for our listing:
Code: Select all
<!-- before: -->
<cms:pages masterpage='blog.php' custom_field="author=admin" >

<!-- after: -->
<cms:set global_limit = "<cms:get_custom_field 'records_visible' masterpage='globals.php' />" scope='global' />
<cms:pages masterpage='blog.php' custom_field="author=admin"  paginate='1' limit=global_limit >
   ...
</cms:pages>

To make sure more posts are loaded via ajax correctly, with the same 'author=admin' filter, let's also convert custom_field parameter to a variable.
Code: Select all
<!-- before: -->
<cms:set global_limit = "<cms:get_custom_field 'records_visible' masterpage='globals.php' />" scope='global' />
<cms:pages masterpage='blog.php' custom_field="author=admin"  paginate='1' limit=global_limit >
   ...
</cms:pages>

<!-- after: -->
<cms:set global_limit = "<cms:get_custom_field 'records_visible' masterpage='globals.php' />" scope='global' />
<cms:set my_filtering = 'author=admin' scope=global' />
<cms:pages masterpage='blog.php' custom_field=my_filtering  paginate='1' limit=global_limit >
   ...
</cms:pages>

By now we have almost everything in place. Let's add the 'a#load-more' button to our complete listing:
Code: Select all

<div id="listing-wrapper">
   <cms:set global_limit = "<cms:get_custom_field 'records_visible' masterpage='globals.php' />" scope='global' />
   <cms:set my_filtering = 'author=admin' scope=global' />
   <cms:pages masterpage='blog.php' custom_field=my_filtering  paginate='1' limit=global_limit >
        <div class="post">
            <!-- Post Title -->
            <h3 class="title"><a href="<cms:show k_page_link />"><cms:show k_page_title /></a></h3>
            <!-- Post Date -->
            <p class="sub"><cms:date k_page_date format='jS M, y'/></p>
            <!-- Post Image -->
            <img class="thumb" alt="" src="<cms:show blog_image />" />
            <!-- Post Content -->
            <cms:excerptHTML count='75' ignore='img'><cms:show blog_content /></cms:excerptHTML>
            <!-- Read More Button -->
            <p class="clearfix"><a href="<cms:show k_page_link />" class="button right"> Read More...</a></p>
        </div>

        <cms:if k_paginated_bottom>
            <cms:if k_paginator_required >

                <!-- Pagination -->
                <div id="load-more-container" >
                    <div style="margin:0 auto; width:15vw; border:1px solid black; text-align:center; ">
                        <a href="#" id="load-more"
                           data-limt="<cms:show global_limit />"
                           data-curr="<cms:show k_current_page />"
                           data-totl="<cms:show k_total_pages />"
                           data-filt="<cms:show my_filtering />"
                           style="display:block; padding:15px 20px; text-align: center;  letter-spacing: 4px; color: rgb(0, 0, 0);">
                            LOAD MORE
                            <br>
                            <br>
                            [<cms:show k_current_page />/<cms:show k_total_pages />]
                        </a>
                    </div>
                </div>
                <!-- /pagination -->


            </cms:if>
        </cms:if>

   </cms:pages>
</div>


A successful loading of more elements will happen if we send to Ajax-processing code the same parameters that we use for listing the initial items. We should send the my_filtering options, then global_limit and, of course, use offset parameter to skip already visible posts. It means that for offset we will also send the current page number to the code. I used data-* attributes to keep those values and have them easily accessible later from JS script.

In JS script I used the external template, that is very comfortable to use in all ajax scripts.
url: 'assets/php/ajax/connect.php', // template that handles ajax request

Code for this ajax connector is below. Use it for all your ajax things, it's a pretty good one.
Code: Select all
<?php require_once( "../../../cms/cms.php" );  ?>
<cms:template title='Ajax connector' hidden='1' order='1000' />

/** Extra debugging / logging for superadmins
<cms:if k_user_access_level = '10' >
    <cms:php>error_log( print_r( $_REQUEST, true) ); </cms:php>
    <cms:php>if( $_FILES ) error_log( print_r( $_FILES, true) ); </cms:php>
</cms:if>

/** Alow only ajax requests.
<cms:if "<cms:not "<cms:is_ajax />" />" >
    <cms:abort msg="ERROR: Page can't be accessed directly." />
</cms:if>

/**  Get snippet from POST ajax request.
<cms:if "<cms:gpc 'file' />" >
    <cms:set snippet = "<cms:gpc 'file' />" />
</cms:if>

<cms:if "<cms:gpc 'filename' />" >
    <cms:set snippet = "<cms:gpc 'filename' />" />
</cms:if>

<cms:if "<cms:gpc 'snippet' />" >
    <cms:set snippet = "<cms:gpc 'snippet' />" />
</cms:if>

/** Prepare list of allowed snippets
<cms:capture into='whitelist' >
   ajax-files/load-more.html |
</cms:capture>

/** Validate the snippet name
<cms:each whitelist >
    <cms:if item = snippet >
        <cms:if "<cms:exists item />">
            /** Check if file exists on disk
            <cms:set snippet_is_valid='1' scope='global' />
        </cms:if>
    </cms:if>
</cms:each>


/** Store the result of snippet code
<cms:capture into='ajax_output' >
    <cms:if snippet_is_valid >
        <cms:embed snippet />
    <cms:else />ERROR: File not found: '<cms:show snippet />'
    </cms:if>
</cms:capture>

/** Send back to JS only the result
<cms:abort msg=ajax_output is_404='0' />

<?php COUCH::invoke(); ?>


Now our JS script sends data to this connect.php template and this template simply embeds the necessary snippet, that will handle all the processing.
snippet: 'ajax-files/load-more.html',

Create in /snippets folder a folder /ajax-files and place there a file load-more.html with code below. It is a good-practice to have all code in different snippets, since there can be very many different ajax requests that you might want to add to your project. So for each different request it is comfy to simply send the snippet name which does the job.

The best part is that our snippet has almost the same code as we use for listing. Remember, that we sent following data in the ajax request:
mylimit - number of items per 'page', i.e. limit
options - content of custom_field parameter
curpage - current page that was already loaded, i.e. 5th of 12.

We need to calculate offset to skip already loaded items. Formula should help: offset = global_limit X current_page. So complete listing of the snippet goes like this:
Code: Select all
<cms:set global_limit = "<cms:gpc 'mylimit' />" scope='global' />
<cms:set my_filtering = "<cms:gpc 'options' />" scope='global' />
<cms:set current_page = "<cms:gpc 'curpage' />" scope='global' />
<cms:set pages_offset = "<cms:mul global_limit current_page />" scope='global' />

<cms:pages masterpage='blog.php' custom_field=my_filtering  paginate='1' limit=global_limit offset=pages_offset >
    <div class="post">
        <!-- Post Title -->
        <h3 class="title"><a href="<cms:show k_page_link />"><cms:show k_page_title /></a></h3>
        <!-- Post Date -->
        <p class="sub"><cms:date k_page_date format='jS M, y'/></p>
        <!-- Post Image -->
        <img class="thumb" alt="" src="<cms:show blog_image />" />
        <!-- Post Content -->
        <cms:excerptHTML count='75' ignore='img'><cms:show blog_content /></cms:excerptHTML>
        <!-- Read More Button -->
        <p class="clearfix"><a href="<cms:show k_page_link />" class="button right"> Read More...</a></p>
    </div>
</cms:pages>

Now, once this snippet generates the markup for more items with limit, offset and custom_field, our connect.php template aborts the execution with showing of this markup and so it gets sent back to JS script, which appends the result html to the listing wrapper. In the end, the button gets updated with new visible text.

Final part:
Put the content of both scripts from the top of this post to some blog.js like this
Code: Select all
$(document).ready(function () {
   // here goes the clicking part - $('a#load-more').on('click', function (event){...

  // here goes the scrolling part - // BEST SCROLLER ....
});

and load it in your template
Code: Select all
<script src="assets/js/pages/blog.js"></script>

Or maybe with <cms:rel /> tag :) like this:
Code: Select all
<cms:rel src="assets/js/pages/blog.js"  />


Ask any questions.


:)
Very nice work @trendoman :)

If not done correctly, accepting a file-name as user supplied URL parameter to embed a snippet can open up 'directory traversal' vulnerability. I am glad to see the way you have used a white-list of acceptable snippet names and then also made sure the snippet truly exists before passing the name on to <cms:embed>. That is how this needed to be done with full safety.

I am moving this thread to 'Tips and tricks section'.
trendoman wrote: Hi all,

Complete tutorial below.

If you have some listing of items with <cms:pages /> or <cms:query /> tags, here is how to add "loading more" functionality with auto-loading on scroll or click...



Yay, that`s wonderful, added to my 'cool scripts I now can use' colection, thanks a lot!
Hi, thanks for the script, i've implemented it as per the tutorial.
Everything seems to be working fine, however the ajax is returning:
"ERROR: Page can't be accessed directly."

which seems to be coming from connect.php
Code: Select all
/** Alow only ajax requests.
<cms:if "<cms:not "<cms:is_ajax />" />" >
    <cms:abort msg="ERROR: Page can't be accessed directly." />
</cms:if>


Anyone with any ideas?

This is the last bit of an evil overhaul I've been doing, my first real use of couch. Loving it all at the moment, despite my noob difficulties, and the support on the forum has been really helpful.
In the meantime I'll keep on digging about :)

EDIT: I was using my own ajax lib, replaced with jquery and it worked. Am investigating why...

EDIT #2: Have tried replacing jquery $.ajax with axios, atom, etc, writing my own, and trying various methods from SO and the like. No joy.
It would seem I am missing something rather obvious, but I really don't want to include jquery for this one function.
If anybody can help i'll be very grateful.
Thanks in hopeful advance :)

EDIT #3: The final cut - Figured it finally, i'd missed the headers that is_ajax checks for & jquery sets by default, namely:
"X-Requested-With : XMLHttpRequest"


I think that's this one in the bag. :D
Excellent tutorial, congratulations.

Does this "addon" work with the multi language?

Thank you!
10 posts Page 1 of 1