Leaderboard
Popular Content
Showing content with the highest reputation on 10/03/2020 in all areas
-
This week I’ve been working on updates for ProcessWire 3.0.168 as well as a new Stripe payments module for FormBuilder. In the core, the ProcessPageView module that handles all requests got a pretty significant refactoring. While it was largely focused on optimization, I also just like to go in every few years and rewrite stuff in high traffic modules like this to keep the module fresh, as well as keep my mind fresh on how it all works. I find there’s often room to do something better. The ProcessLogin module also got some upgrades this week that are primarily focused on maintaining the contents of a request URL in the admin between guest and admin user states. Now it can retain most query strings in a request URL after login. There’s still more to do in 3.0.168 so the version bump isn’t being made this week but may be ready next week. So far there are also a couple of GitHub issue resolutions and $sanitizer method improvements as well. On the FormBuilder side, we have the Stripe payments Inputfield, but since that module has been built, Stripe has released new APIs to handle SCA payments (soon required for EU transactions). With these types of payments, as I understand it, a multi-factor type process takes place where the user confirms the transaction with their credit provider. So it changes the payment flow quite a bit… enough that I’ve found the current Stripe payments module can’t really be adapted for it, at least not in a reliable way. That’s because the new API requires that the payment take place interactively before the form is submitted, since the user has to manually confirm it outside the form. So this will inevitably lead to cases where a payment has been charged but the final form isn’t submitted for one reason or another. Maybe it would work most of the time, but it just doesn’t seem like a reliable transaction flow to me. For this reason, I’m building a separate module for FormBuilder that provides a better alternative for the SCA transactions. With this module, the user submits the form, but FormBuilder saves it as a partial/pending form submission. Following form submission, the user goes through the SCA payment process with Stripe. When that completes, Stripe sends a webhook to FormBuilder that converts the form submission from pending to completed, at which point emails are sent, FormBuilder actions executed, etc. So it's Stripe that submits the final form, rather than the user. I’ve got a lot of work still to do here, but since a few people have contacted me looking for SCA support with Stripe payments, I just wanted to keep you up to date and let you know it’s in progress. The existing InputfieldFormBuilderStripe module will of course continue to be developed as well, as most regions outside the EU do not require SCA. Thanks for reading and have a great weekend!10 points
-
BayTech360 is a System Integrations Specialist serving the US out of San Francisco. They wanted to revamp their website and asked Pigtail Pundits to help with it. The new website is built to storytelling standards of StoryBrand and other direct response advertising frameworks. This site features, sell-focussed copy which is our trademark. This is mapped to an elegant, clean design. Built atop ProcessWire, our favourite CMS, with ProCache for speed. Check it out at: https://www.baytech360.com/5 points
-
Well, you can still create a page in your source PW installation that outputs either a JSON of your n latest articles, or complete with markup. On the other PW installation, just fetch that page in some way and include it in your template file, which can be as easy as file_get_contents('http://pw-site-with-articles.com.test/latest-articles.php'); No need to install and configure a full-fledged REST API module for that.5 points
-
@Macrura has created a couple of modules for adding help documents to the admin: Myself, I just create a PW page (hidden from non-superusers in the admin) that contains instructions for users. I use some custom jQuery in the admin to append an "Instructions" view link to the admin footer so the document is always easily accessible from any page. To provide some intervention against duplicate pages you could try a hook like this: $pages->addHookAfter('added', function(HookEvent $event) { $page = $event->arguments(0); $pages = $event->object; // For pages with a particular template... if($page->template == 'basic_page') { // Check for existing page with the same title // You could get fancy by making this more fuzzy // E.g. exclude short words and then look for titles that contain the remaining words $existing_page = $pages->get("template=basic_page, title={$page->title}"); if($existing_page->id) { // You could use the edit URL or view URL as appropriate $url = $existing_page->editURL; $event->wire()->warning("There is an existing page with this title <a href='$url' target='_blank'>here</a>. If you have added a duplicate page by mistake please delete this page.", Notice::allowMarkup); } } });2 points
-
I'm really starting to think that camel case / pascal case should be avoided... well, just about everywhere ? Recently ran into a similar issue where a piece of code worked flawlessly on my machine, yet on the live server it spat out nonsensical errors. Turns out my local (Mac) env is case insensitive (which also applies to the virtual machine I use to run Linux on it) while the server is case sensitive. Spent half an hour or so debugging the issue and tearing things apart just to find a tiny typo in the filename. So frustrating! ?1 point
-
1 point
-
Yes. This will be more versatile. Alternatively, if you were working with a known set of icons that won't change or wont' change much, you could use the separate value|title feature of FieldtypeOptions. Looks a bit hacky but you could do something like this (assuming you are using font awesome): fa-coffee|Breakfast fa-cutlery|Ristorante fa-bed|Bed fa-wifi|Wifi fa-car|Car Park fa-wheelchair| Accessibility // OR... coffee|Breakfast cutlery|Ristorante bed|Bed wifi|Wifi car|Car Park wheelchair| Accessibility You would then be able to access the icons names using $item->value. Obviously this means it is only the developer who will be able to determine and set icons (which may not be a bad thing actually). Just a thought...1 point
-
Thanks @Pixrael I think the best solution is from @louisstephens for buying the profields plugin: https://processwire.com/blog/posts/fieldtype-pagination/#example-profields-table. I looking now 1 hour for this issue and this plugin coast only 129,-€ Thanks all. closed1 point
-
I see two potential suspects here; Windows IIS and MySQL 8. I haven't tested MM in Windows IIS. However, I think we have reached a point where it is best that I have access to a test server. This could either be a test site where you can reproduce the problem or the production site itself. I have never been able to reproduce the issue, albeit I have not tried in a Windows IIS environment. I don't have access to such an environment either, sorry. No other users have reported similar UNRESOLVED issues, so I have nothing else to go on regarding your particular case. That's not great! Please consider giving me temporary access to a site where I can debug the issue. Thanks.1 point
-
Unfortunately, I do not believe the pagination has yet made it to repeater fields yet. There was a blog post made in 2016 regarding it, and I believe it is only possible with the Profields Table field atm (though the blog mentions the possibility of adding it in the future). I am sure other items were deemed more important, and pushed repeater pagination down the line. I have read that AJAX loading repeaters might help, though it is the default setting.1 point
-
This module is an SMTP extension for the WireMail class. It can only handle SMTP transport which is for outgoing mail. There is no module that I know of which can handle incoming mail (IMAP, POP3). If you want to handle incoming mail, you could use a service for transactional emails like mailgun (and the WireMailgun module) and use webhooks. But this still involves quite a lot of programming.1 point
-
Google Pay picks Flutter to drive its global product development1 point
-
Had an extreme example to deal with recently. Multiple field replacements for various fieldtypes. May not be the best approach but worked for me. Private function is in a custom module function. <?php /** * Takes a page field, eg 'body' as a template and replaces tags eg {age} with the same field value * from the supplied data page * @param $tplPage * @param $tplField * @param $dataPage * @param string $startTag * @param string $endTag * @param array $other * @return string|string[] * @throws WireException * @throws WirePermissionException */ private function _compileFieldTags ($tplPage, $tplField, $dataPage, $startTag = '{', $endTag = '}', $formatDate = false, $other = []) { $allowedFieldtypes = [ 'FieldtypeText', 'FieldtypeTextarea', 'FieldtypeInteger', 'FieldtypeFloat', 'FieldtypeDatetime', 'FieldtypeToggle', 'FieldtypeCheckbox', 'FieldtypePage' ]; $replacementNames = []; $replacementValues = []; // Sort out what to do with each inputfield type foreach ($dataPage->fieldgroup as $replacement) { if (!in_array($replacement->type, $allowedFieldtypes)) continue; switch ($replacement->type) { case 'FieldtypeDatetime': $fldData = $formatDate == false ? $dataPage->$replacement : $dataPage->getFormatted($replacement); break; case 'FieldtypePage': $fldData = $dataPage->$replacement->title; break; case 'FieldtypeCheckbox': $fldData = $dataPage->$replacement == true ? "Yes" : "No"; break; default: $fldData = $dataPage->$replacement; break; } $replacementNames[] = $replacement->name; $replacementValues[] = $fldData; } // Prepare replacement arrays foreach ($dataPage as $k => $v) { $replacementNames[] = $startTag . $k . $endTag; $replacementValues[] = $v; } $replacementNames['url'] = $startTag . 'url' . $endTag; $replacementValues[] = $dataPage->httpUrl; $result = str_ireplace($replacementNames, $replacementValues, $tplPage->$tplField); return $result; }1 point
-
ProcessWire 3.0.167 is the newest version on the dev branch and contains the updates mentioned last week, as well as the following: Improvements and optimizations to several database fulltext index-based text-searching operators (such as *=, ~=, *+=, ~+=) so that they can better match words too short to index, as well as many stopwords. (code changes) New $input->queryStringClean() method is like the existing $input->queryString() method except that it enables you to get a cleaned up version according to rules you specify in an $options argument. I added this method primarily because it’s something I need in the core for some planned updates. But it’s in the public API because I thought it might be useful for some other cases as well. Kind of technical, but some minor improvements to the $sanitizer array methods were made so that they can now support associative arrays with a ‘keySanitizer’ option that lets you specify which sanitizer method to clean up array indexes/keys. The $sanitizer->validateFile() method was rewritten and improved plus new ‘dryrun’ and ‘getArray’ options were added. The core file and image fields have been updated with the ability to require (rather than just utilize) a FileValidatorModule for certain file upload extensions. This was motivated by SVG files being increasingly problematic (or at least my understanding of them) because they can contain the kind of bad stuff that regular markup can (scripts, loading external assets, etc.). But since SVG files are used by many just like the bitmap image formats (JPG, GIF, PNG, etc.) exploits can spread quite easily and unknowingly. I’m not aware of that ever occurring in a ProcessWire site, but it’s one of those things that strikes me as being an inevitable problem for any website (on any software) that has regular SVG uploads. ProcessWire doesn’t allow SVG file uploads by default, but you can manually add “SVG” to your allowed upload extensions for any file/image field… and from what I understand now, a lot of people are doing this—using SVG files not just during development, but as an image format that their clients upload too. The usual protections of having a trusted user admin environment don’t help much here. That’s because what matters is not whether the uploading user is trusted or not, but whether the source of the SVG is trusted. And that’s something we have no control over. It seems apparent the core could provide some extra help and guidance in this area. So ProcessWire now requires you install the FileValidatorSvgSanitizer module if you want to have SVG uploads, OR you can check a box in the file/image field settings to whitelist the extension (acknowledging you understand the risks). Now you could manually add HTML or JS as allowed upload extensions to a files field as well— and if you are rendering the resulting HTML markup or JS directly in the code of your website, then the same risks would be present as with SVG. But the risks seem obvious for doing this with HTML or JS files. Plus, why would someone do that with HTML or JS files unless for some very specific development purpose? I think it’s unlikely any of us are running websites where clients might be downloading HTML or JS files from other sources and uploading them into the website. And even further unlikely that we’d written the site’s code to include the contents of those files among the site's markup. Whereas, this is essentially what happens with SVG files, from what I understand. As I’ve come to learn this week, it’s even quite common to do this (maybe I’m late to the party). Given the above, this week I rebuilt the existing FileValidatorSvgSanitizer module from the ground up, so that it now uses a better/newer SVG sanitization library found by Adrian. I’d suggest installing the module if you are supporting SVG uploads in your site. It seems to do a nice job of cleaning the harmful stuff out of SVG files. I’m not aware of anything it can miss, but I’d still advise some caution with SVG file uploads even if you have the module installed. If you currently allow SVG file uploads but don’t really need them, just remove SVG as an upload option, as that’s still the safest bet. That’s essentially what the current dev branch version does: if you’ve added SVG as an allowed upload extension, it’ll disable it until you decide if you need it; and if you do, then install the sanitizer module or whitelist the extension. I know not everyone is going to like that, but it seems like it’s the right and safe thing to do; as well as a good strategy going forward for any other file formats with similar concerns. When our next master version arrives, it’ll be in the upgrade instructions as well. The core has been updated so that it can support the same means for any other file formats we come across in the future that might be problematic in similar ways. That’s everything I can remember that’s new to 3.0.167. Thanks for reading and have a great weekend!1 point
-
This is a braindump of experiences and considerations that a matrix-based page builder should balance. I'll give more thoughts on this later: heading hierarchy source order div nesting section patterns general fields general fields "2"..."n" ex: image + image_2 ex: list_items + list_items_2 (for example, a section w/ description list + tabs) fields mapping to components ex: title => heading ex: image => image ex: menu_items => subnav items, nav items ex: list_items => description list items, tab items, accordion items, slider items, slideshow items, grid items what to do if field is empty? hide surrounding div structure? list_items field selectively showing/hiding appropriate fields depending on component type ex: accordion items => title + body only ex: slideshow items => image only (note: currently a bug with this using matrix template overrides; utilizing hack workaround instead atm) item templates for use in repeatable components when to use hannacode vs. component section switching (in an easy/quick way) section options (mystique) css framework components 3rd party components component options css framework flexibility (use setting() to store classes) naming approach ex: Grid vs. List of People user friend matrix-type registration visual selector w/ placeholder content instant preview (ideally prodrafts) direct vs. dynamic content saved / reusable sections multilingual weird things like - wrapping links (messes up predictable div nesting?) - image backgrounds using inline css - single line vs multi-line text ex: sometimes a heading should be multi-line instead of having 2 separate fields ability to programmatically include a section template using $files->include css grid vs. flex grid - flex gap? https://coryrylan.com/blog/css-gap-space-with-flexbox A section is literally an HTML <section> (or it can be a div if specified). Imagine a section that has 2 components in the following source order: image + text. A section can be styled independently and map to UIkit section classes: https://getuikit.com/docs/section You could call this section "Image + Text" in repeatermatrix for example and give it the UIkit markup to put the image on the left and the text on the right using the appropriate css classes, and wrap it in a container. This is very easy to understand for content editors. Very important. Now you what if you want flexibility such as having a small container instead of a regular sized container? The "wrong" way to do this would be to make a new matrix-type called "Image + Text (Small Container)". Instead you think of maybe having additional dropdowns to manage such things in the matrix block, but if you think that through and build many sections, it will be overly complex and feel wrong. The "correct" way to do this would be to have various section patterns that modify the div structure and component options of that matrix-type. So for example, if you wanted a small container + animations + rounded corners on the image, you could have a section pattern (would have to be created by the developer) that the user selects that applies all that collectively. Maybe each section has 2-5 section patterns which seems reasonable. With this section pattern approach, you could theoretically have the same section display the image on its own row and the text below. But what if you want each component to be in its own different sized container? If you think this through this requires a different div nesting structure to keep things super flexible: <div class="<?=setting('div-1-class')?>" <?=setting('div-1-attr')?>><!-- container --> <div class="<?=setting('div-1-div-1-class')?>" <?=setting('div-1-div-1-attr')?>><!-- container inner --> <div class="<?=setting('div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-attr')?>><!-- grid --> <div class="<?=setting('div-1-div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-div-1-attr')?>><!-- col --> <div class="<?=setting('div-1-div-1-div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-div-1-div-1-attr')?>><!-- component wrapper --> <?=files()->include('./fields/blocks/image.php', [ 'page'=>$page, 'block_options'=>setting('image-1-block-options') ])?> </div> </div> <div class="<?=setting('div-1-div-1-div-1-div-2-class')?>" <?=setting('div-1-div-1-div-1-div-2-attr')?>> <div class="<?=setting('div-1-div-1-div-1-div-2-div-1-class')?>" <?=setting('div-1-div-1-div-1-div-2-div-1-attr')?>> <?=files()->include('./fields/blocks/text.php', [ 'page'=>$page, 'block_options'=>setting('text-1-block-options') ])?> </div> </div> </div> </div> </div> vs. <div class="<?=setting('div-1-class')?>" <?=setting('div-1-attr')?>><!-- container 1 --> <div class="<?=setting('div-1-div-1-class')?>" <?=setting('div-1-div-1-attr')?>><!-- container inner 1 --> <div class="<?=setting('div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-attr')?>><!-- grid 1 --> <div class="<?=setting('div-1-div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-div-1-attr')?>><!-- col 1 --> <div class="<?=setting('div-1-div-1-div-1-div-1-div-1-class')?>" <?=setting('div-1-div-1-div-1-div-1-div-1-attr')?>><!-- component wrapper 1 --> <?=files()->include('./fields/blocks/image.php', [ 'page'=>$page, 'block_options'=>setting('image-1-block-options') ])?> </div> </div> </div> </div> </div> <div class="<?=setting('div-2-class')?>" <?=setting('div-2-attr')?>><!-- container 2 --> <div class="<?=setting('div-2-div-1-class')?>" <?=setting('div-2-div-1-attr')?>><!-- container inner 2 --> <div class="<?=setting('div-2-div-1-div-1-class')?>" <?=setting('div-2-div-1-div-1-attr')?>><!-- grid 2 --> <div class="<?=setting('div-2-div-1-div-1-div-1-class')?>" <?=setting('div-2-div-1-div-1-div-1-attr')?>><!-- col 2 --> <div class="<?=setting('div-2-div-1-div-1-div-1-div-1-class')?>" <?=setting('div-2-div-1-div-1-div-1-div-1-attr')?>><!-- component wrapper 2 --> <?=files()->include('./fields/blocks/text.php', [ 'page'=>$page, 'block_options'=>setting('text-1-block-options') ])?> </div> </div> </div> </div> </div> You can represent those 2-component div structures like this: (1 2) (1) (2) If you have 3 components: (1 2 3) (1 2) (3) (1) (2 3) (1) (2) (3) 4 components: (1 2 3 4) (1 2 3) (4) (1 2) (3 4) (1) (2 3 4) (1) (2) (3 4) (1) (2 3) (4) (1 2) (3) (4) (1) (2) (3) (4) 5 components: (1 2 3 4 5) (1) (2 3 4 5) (1 2) (3 4 5) (1 2 3) (4 5) (1 2 3 4) (5) (1 2 3) (4) (5) (1 2) (3) (4) (5) (1 2) (3 4) (5) (1) (2 3 4) (5) (1) (2 3) (4 5) (1) (2) (3 4 5) (1) (2 3) (4) (5) (1) (2) (3) (4 5) (1 2) (3) (4 5) (1) (2) (3) (4) (5) (it can actually get more complex than that, but no need to go there) A CSS Grid approach probably removes the complexity required for div nesting (not 100% sure since I haven't used CSS-grid in a long time). UIkit would ideally have a built in solution in version 4. Maybe I'm overthinking this div nesting part. Perhaps the section pattern (which comes in via a json settings file and stores its values in setting()) directly specifies which div nesting layout should be used. Hmm.1 point
-
I played around with multi-instances and found out that we currently (PW 3.0.163) are not able to use multiple instances when more then one site has set $config->useFunctionsAPI (in site/config.php) to true! Then I saw that, (when $config->useFunctionsAPI was set to false) in all instances $config->version returned the same version, that from the master instance. So, first I was a bit confused, but then I thought that this may have to do with the early step when PW processes/build the $config. And indeed, if I set in all site/config.php files the $config->useFunctionsAPI to false, and then in all site/init.php files to true, everything is working fine. Now we can use our sites with the functions API, and we can load as many multiple instances in parallel we want. ? TL;DR site/init.php /** * FOR BETTER SUPPORT OF MULTIINSTANCES, WE ENABLE $config->useFunctionsAPI HERE, * INSTEAD OF THE site/config.php FILE: */ $wire->config->useFunctionsAPI = true; Bootstrapping three different instances, in first step each of them in a single environment: <?php namespace ProcessWire; if(!defined('PW_MASTER_PATH')) define('PW_MASTER_PATH', 'E:/laragon/www/hwm/'); if(!defined('PW_MASTER_HTTPURL')) define('PW_MASTER_HTTPURL', 'https://hwm.local/'); // bootstrap ProcessWire instance site1 (3.0.163) require_once(PW_MASTER_PATH . 'index.php'); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); When running all three in a multi instance environment, they load fine, (no compile error), all with the use for the functions API enabled: <?php namespace ProcessWire; if(!defined('PW_MASTER_PATH')) define('PW_MASTER_PATH', 'E:/laragon/www/hwm/'); if(!defined('PW_MASTER_HTTPURL')) define('PW_MASTER_HTTPURL', 'https://hwm.local/'); if(!defined('PW_SITE2_PATH')) define('PW_SITE2_PATH', 'E:/laragon/www/hwm2/'); if(!defined('PW_SITE2_HTTPURL')) define('PW_SITE2_HTTPURL', 'https://hwm2.local/'); if(!defined('PW_SITE3_PATH')) define('PW_SITE3_PATH', 'E:/laragon/www/hwm3/'); if(!defined('PW_SITE3_HTTPURL')) define('PW_SITE3_HTTPURL', 'https://hwm3.local/'); // bootstrap ProcessWire master instance (3.0.163) require_once(PW_MASTER_PATH . 'index.php'); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); // create a secondary instance from master (3.0.163) $wire = new \ProcessWire\ProcessWire(PW_MASTER_PATH); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); // create instance of a second site (3.0.162) $site2 = new ProcessWire(PW_SITE2_PATH, PW_SITE2_HTTPURL); mvd([ 'httpurl' => $site2->wire('pages')->get(1)->httpURL, 'instanceNum' => $site2->getInstanceNum(), 'config->version' => $site2->wire('config')->version, 'useFunctionsAPI' => $site2->wire('config')->useFunctionsAPI ]); // create instance of a third site (3.0.152) $site3 = new ProcessWire(PW_SITE3_PATH, PW_SITE3_HTTPURL); mvd([ 'httpurl' => $site3->wire('pages')->get(1)->httpURL, 'instanceNum' => $site3->getInstanceNum(), 'config->version' => $site3->wire('config')->version, 'useFunctionsAPI' => $site3->wire('config')->useFunctionsAPI ]);1 point
-
When a blog post article receives many comments ? your start to consider to apply pagination to comments. Looking around this forum, I found some useful hints, but I couldn't find how to take full advantage of PW built-in pagination. In particular the ukBlogPosts() function in Uikit 3 Site/Blog Profile renders a list of paginated posts. ukBlogPosts() is calling a ukPagination() function which permits to navigate the posts. ? After some unsuccessful attempts to use it with CommentArray … ? I decided to insist. ? And here is the result. The ukPagination() function in _uikit.php expects an argument $items of type PageArray. Of course a CommentArray ... is not a PageArray ... ? so we have to remove this blocking point. Going deeper inside the function you will notice that pagination is achieved through a call to a render() method of MarkupPagerNav. Let's look at its declaration: public function ___render(WirePaginatable $items, $options = array()) Interesting ? here $items are declared of type WirePaginatable. What's that? ? a class? No! It's an interface! ? Looking around further I discovered that WirePaginatable interface is used by PageArray, PaginatedArray, and CommentArray.. bingo! ?? We got what to do first, modify ukPagination() function in _uikit.php changing $items argument type from PageArray to WirePaginatable: //function ukPagination(PageArray $items, $options = array()) { function ukPagination(WirePaginatable $items, $options = array()) { //>>>>> REPLACE WITH THIS LINE Now the function accepts our CommentArray as argument, but to make it work we need to set its Limit, Start, and Total attributes as PW does automatically when dealing with pages. In order to do that we will use the WirePaginatable methods setLimit(), setStart(), setTotal(). In this process our Start attribute will have to be calculated dynamically to consider the page number when navigating the comments. To do that we will largely ? modify the function ukComments() in _uikit.php: function ukComments(CommentArray $comments, $options = array()) { if(!$comments->count) { //>>>>>NEW-start if(input()->pageNum > 1) { // redirect to first pagination if accessed at an out-of-bounds pagination session()->redirect(page()->url); } return ''; } //>>>>>NEW-end $defaults = array( 'id' => 'comments', 'paginate' => false, //>>>>>NEW 'limit' => 3, //>>>>>NEW ); //if(!count($comments)) return ''; $options = _ukMergeOptions($defaults, $options); $language = user()->language->id; //>>>>>NEW $comments = $comments->find("language=$language"); //>>>>>NEW if($options['paginate']) { //>>>>>NEW-start $limit = $options['limit']; $start = (wire()->input->pageNum - 1) * $limit; $total = $comments->count(); $comments = $comments->slice($start, $limit); $comments->setLimit($limit); $comments->setStart($start); $comments->setTotal($total); } //>>>>>NEW-end $out = "<ul id='$options[id]' class='uk-comment-list'>"; foreach($comments as $comment) { $out .= "<li class='uk-margin'>" . ukComment($comment) . "</li>"; } $out .= "</ul>"; if($options['paginate'] && $comments->getTotal() > $comments->count()) { //>>>>>NEW-start $out .= ukPagination($comments); } //>>>>>NEW-end return $out; } As you will notice we have added two options to the function [paginate => false] and [limit => 3]. In such a way the ukComments() function will operate as before. If you wish to use pagination you will have to pass [paginate => true] as argument of the function. Of course you can freely choose the limit value or by pre-setting it in the function or by passing it as an argument to the function. Please note that the modification implements the necessary changes (2 lines) to make comments language-aware as described in the tutorial: We are now just to steps away to see our paginated comments. ? First you will need to enable pagination in the templates where you want comments to be paginated. Login to PW Admin and select your template. In the tab URLs enable the "Allow Page Numbers?" option. Second you will need to modify the associated php template, in our example blog-post.php, to change the call to ukComments() as follows (sorting is not necessary, depends on your preferences): //echo ukComments($comments); echo ukComments($comments->find("sort=-created"), ['paginate' => true]); //>>>>> REPLACE WITH THIS LINE And here we are! Below a snapshot our paginated comments! ??? As you can see on the bottom of the comments you have the standard PW pagination navigator, without being forced to rewrite a new one for the purpose! ? I hope you can find something useful with this tutorial.1 point