Custom pagination with unique URLs

The Wix Blog has built-in pagination with links. Links are required for good SEO.

I have reproduced this pagination element that uses unique URLs and creates my own implementation. (Lots of code :flushed:)

For that, I use a wix-router on the server part and I generating custom pagination by Repeater on the front-end part.

So pagination is a difficult component. I share my solution I hope it will be helpful (interesting) to take a look at how it works.

DEMO : https://alexanderz5.wixsite.com/pagination/custom-blog

backend/routers.js
Route logic for pagination

import wixData from 'wix-data';
import { ok, redirect, WixRouterSitemapEntry } from 'wix-router';
import urlJoin from 'url-join';

const hasContent = (val) => typeof val === 'string' && val.trim() !== '';
const isNumeric = (val) => hasContent(val) && /^[\d]+$/.test(val);
const parseNumber = (val) => ~~Math.abs(+val);

const getCategories = () => {
    return wixData
        .query('Blog/Categories')
        .find()
        .then((result) => result.items);
};

const getCategory = (label) => {
    return wixData
        .query('Blog/Categories')
        .eq('label', label)
        .limit(1)
        .find()
        .then((reslut) => reslut.items[0]);
};

const getPosts = async (pageSize, skipPages, categoryId = null) => {
    let dataQuery = wixData.query('Blog/Posts');

    if (hasContent(categoryId)) {
        dataQuery = dataQuery.hasAll('categories', categoryId);
    }

    return dataQuery
        .skip(skipPages)
        .limit(pageSize)
        .find();
};

const getParams = async (path) => {
    const length = path.length;

    const [one, two] = path.map((i) => i.toLowerCase());

    if (length === 1) {
        if (one === '') {
            return {
                page: 0,
                label: '',
            };
        }

        if (isNumeric(one)) {
            return {
                page: parseNumber(one),
                label: '',
            };
        }

        const category = await getCategory(one);

        if (typeof category !== 'undefined') {
            return {
                page: 0,
                categoryId: category._id,
                label: category.label,
            };
        }
    }

    if (length === 2 && isNumeric(two)) {
        const category = await getCategory(one);

        if (typeof category !== 'undefined') {
            return {
                page: parseNumber(two),
                categoryId: category._id,
                label: category.label,
            };
        }
    }

    return { hasError: true };
};

/**
 * @param {wix_router.WixRouterRequest} request
 */
export async function custom_blog_Router({ path, baseUrl, prefix }) {
    const params = await getParams(path);

    if (params.hasError) {
        return redirect(urlJoin(baseUrl, prefix), '301');
    }

    const pageSize = 2;
    const skip = (params.page === 0 ? 0 : params.page - 1) * pageSize;

    const postsData = await getPosts(
        pageSize,
        skip,
        params.categoryId,
    );

    return ok('custom-blog-page', {
        pageSize,
        posts: postsData.items,
        currentPage: postsData.currentPage,
        totalCount: postsData.totalCount,
        totalPages: postsData.totalPages,
        label: params.label,
    });
}

/**
 * @param {wix_router.WixRouterSitemapRequest} sitemapRequest
 * @returns {Promise<wix_router.WixRouterSitemapEntry[]>}
 */
export async function custom_blog_SiteMap(sitemapRequest) {
    const categories = await getCategories();

    return categories.map((i) => {
        const entry = new WixRouterSitemapEntry(i.label);

        return Object.assign(entry, {
            title: i.label,
            pageName: i.label,
            url: urlJoin('/', sitemapRequest.prefix, i.label),
        });
    });
}

Custom Blog Page (code):
Route page with repeater and custom pagination.

import { getRouterData } from 'wix-window';
import { prefix } from 'wix-location';
import urlJoin from 'url-join';

import { paginate } from 'public/paginate';

$w.onReady(function () {
    const {
        posts,
        currentPage,
        totalCount,
        pageSize,
        label,
    } = getRouterData();

    const { data } = paginate({
        totalCount,
        currentPage,
        maxPages: 4,
        pageSize,
    });

    $w('#repeaterPosts').data = posts;
    $w('#repeaterPosts').forEachItem(($item, itemData) => {
        $item('#textTitle').text = itemData.title;
    });

    $w('#repeaterPagination').data = data;
    $w('#repeaterPagination').forEachItem(($item, itemData) => {
        $item('#button1').label = itemData.label;

        if (itemData.isActive) {
            $item('#button1').link = urlJoin('/', prefix, label, String(itemData.number));
        } else {
            $item('#button1').disable();
        }
    });

    $w('#buttonLinkAll').link = urlJoin('/', prefix);
    $w('#buttonLinkCss').link = urlJoin('/', prefix, 'css');
    $w('#buttonLinkHtml').link = urlJoin('/', prefix, 'html');
    $w('#buttonLinkJs').link = urlJoin('/', prefix, 'js');
});

public/paginate.js
Here is a logic for creating repeater items for custom pagination

export const paginate = ({
    totalCount,
    currentPage,
    pageSize,
    maxPages,
}) => {
    const totalPages = Math.ceil(totalCount / pageSize);

    let startPage = 1;
    let endPage = totalPages;

    if (currentPage > totalPages) {
        currentPage = totalPages;
    }

    if (totalPages > maxPages) {
        const maxPagesBeforeCurrentPage = Math.floor(maxPages / 2);
        const maxPagesAfterCurrentPage = Math.ceil(maxPages / 2);

        if (currentPage <= maxPagesBeforeCurrentPage) {
            endPage = maxPages;
        } else if (currentPage + maxPagesAfterCurrentPage >= totalPages) {
            startPage = totalPages - maxPages + 1;
        } else {
            startPage = currentPage - maxPagesBeforeCurrentPage;
            endPage = currentPage + maxPagesAfterCurrentPage;
        }
    }

    const length = (endPage + 1) - startPage;

    const data = Array.from({ length },
        (_, index) => {
            const number = startPage + index;
            const id = String(number);

            return {
                _id: id,
                label: id,
                number,
                isActive: number !== (currentPage + 1),
            };
        },
    );

    data.unshift({
        _id: 'first',
        label: '<<',
        number: 1,
        isActive: currentPage > 0,
    }, {
        _id: 'prev',
        label: '<',
        number: currentPage,
        isActive: currentPage > 0,
    });

    data.push({
        _id: 'next',
        label: '>',
        number: currentPage + 2,
        isActive: (currentPage + 1) < totalPages,
    }, {
        _id: 'last',
        label: '>>',
        number: totalPages,
        isActive: (currentPage + 1) < totalPages,
    });

    return {
        data,
    };
};
8 Likes