Jump to content


  • Posts

  • Joined

  • Last visited

  • Days Won


Everything posted by ryan

  1. This week my kids are out of school on fall break, meaning I've also been off work this week (in part at least), so it's been a quiet week for development. Nevertheless, an issue report has been resolved (thanks to matjazpotocnik), two feature requests have been added (here and here), and a bunch of minor core code improvements have been added. While there's not much more to report this week, more is on the way next week. Have a great weekend!
  2. This week we have ProcessWire 3.0.205 on the dev branch. Relative to 3.0.204, this includes 23 new commits including several refactored classes, issue resolutions, several pull requests and various new features. While there's no single major new feature to write a blog post around, there's still a lot here so for full details see the dev branch commit log. This week there's also a new module released in the ListerPro board called PageActionCrawl. This is a ListerPro PageAction module that crawls all pages sent to it. You can use this to crawl your entire site, some portion of pages within it, or even crawl external URLs referenced in URL fields. This is useful for any number of things such as priming caches, finding errors, quality assurance, doing security testing and more. Features include: Supports crawling with GET, POST or HEAD requests. Supports optional query strings and/or URL segments. Reports the HTTP response code and render time for each URL. Highlights error URLs (http code >= 400) in red. Optionally supports crawling of multiple URL variations per page. Supports inclusion of custom POST variables in POST requests. Supports page URLs or URLs stored in FieldtypeURL fields. Supports success and error hooks for custom behaviors on crawled URLs. Speaking of ListerPro actions, I've moved all of the 9 ListerPro action modules into their own new subforum of the ListerPro support board, so if you subscribe to ListerPro be sure to look for the new ListerPro Page Actions board in there. That's also where this new PageActionCrawl module is posted. Thanks for reading and have a great weekend!
  3. @benbyf In your PW admin, when in debug mode, there is a "Debug" link at the bottom right of the screen. If you click that it opens a panel and one of the options in the panel is "Hooks". Click Hooks to open and it'll show you all the hooks that are in use and in what order. The "priority" column indicates order... The smaller the priority number, the earlier it executes, so a priority of 99 executes before 100. But in the Hooks list on the debug screen you'll see priority numbers like 100.0, 100.1, 100.2, etc. This indicates that the hooks were added with the same priority (100, which is the default) and the number after the decimal indicates the order they were added in. They execute in the same order, so 100.0 executes before 100.1, 100.1 executes before 100.2, etc. If you are looking for a way to get this info for some other purpose you can find the code that generates this debug info in /wire/templates-admin/debug.inc, but basically it amounts to a displaying the return values from wire()->getHooks('*');
  4. This last week my wife and daughter took a trip to NYC and it was my daughter's first time there. I was browsing around online looking at things they could do and so I visited the Guggenheim museum website to look into that option... I've always been a fan of the building, a Frank Lloyd Wright masterpiece. In addition to New York, I learned from the website that Guggenheim also has museums in Abu Dhabi, Bilbao and Venice, so I clicked through to view them as well. I really liked the Venice Guggenheim website, which had a much nicer website than the other locations. It was such a nice site that I was curious what they were running, so I viewed the source and... not WordPress (like the others), but ProcessWire. What a nice surprise. Then I was curious about who made such a nice site and there was a credits link in the bottom right corner that says the site was made by basili.co, nice work! It's always fun to come across a ProcessWire powered website randomly like that, and I thought you all would enjoy this one too. This week there are fairly minor updates on the core dev branch. Though the updates include one I've been meaning to do for a long time: improve the API for processing Inputfield forms. Previously there's been no way to check if a form is submitted, short of checking an $input variable yourself. Today I committed an update that adds a $form->isSubmitted() method that solves that, and more. It can identify which form was submitted, which submit button was used, and it also performs additional checks to make sure it's a valid submission before deciding that it's a form worth processing. It improves reliability, accuracy and security. Next week I'll be updating several of the admin forms to use it, among other updates. A few other useful helper methods were added to the Inputfield forms API as well. I realize that these updates may only be of interest to module authors, but I like keep you up-to-date with the week's updates either way. Thanks for reading and have a great weekend!
  5. @adrian @bernhard Thanks, I think I've fixed these issues and they are now on the dev branch. @kongondo I wasn't able to duplicate the language issues you mentioned, but am guessing they must be related to the things Bernhard and Adrian mentioned, but please let me know if you continue to see them.
  6. This week on the core dev branch we’ve got some major refactoring in the Page class. Unless I broke anything in the process, it should be more efficient and use less memory than before. But a few of useful new methods for getting page fields were added in the process. (A couple of these were also to answer feature request #453). Dot syntax You may have heard of dot-syntax for getting page fields, such as $pages->get('parent.title') where “parent” can be any field and “title” can be any field that has subfields. This is something that ProcessWire has supported for a long time, but it doesn’t get used much because it was disabled when output formatting was on. So it wasn’t something you could really count on always being there. Now you can — it is enabled all of the time. But it’s also been rewritten to be more powerful. When using dot syntax with a multi-value field (i.e. any kind of WireArray value) you can also specify field_name.first to get just the first value or field_name.last to get just the last value. i.e. $page->get('categories.first'); will take a value that was going to be a PageArray (‘categories’) and return just the first Page from it. Bracket syntax With bracket syntax you can call $page->get('field_name[]') with the (‘[]’ brackets at the end) and it will always return the appropriate array value for the type, whether a PageArray, WireArray, Pagefiles/Pageimages, or regular PHP array, etc. This is useful in cases where you know you’ll want a value you can foreach/iterate. Maybe you’ve got a Page field that set set to contain just 1 page, or maybe you’ve got a File/Image field set to contain just 1 file. But you want some way to treat all of your page or file/image fields the same, just append “[]” to the field name in your $page->get() call and you’ll always get an array-type value, regardless of the field settings. This bracket syntax can also be used for getting 1 value by index number. Let’s say you’ve got a page field named “categories” that contains multiple pages. If you want to get just the first, you can just call $page->get('categories[0]'); If you want to get the second, you can do $page->get('categories[1]'); This works whether the field is set to contain just one value or many values. Using the first index [0] is a good way to ensure you get 1 item when you may not know whether the field is a single-value or multi-value field. Another thing you can do with the bracket syntax is put a selector in it to filter a multi-value field right in the $page->get() call. Let’s say you want all categories that have the word “design” in the name. You can call $page->get('categories[title%=design]'); If you want just the first, then $page->get('categories[title%=design][0]'); What’s useful about using selectors in brackets is that this does a filter at the database-level rather than loading the entire ‘categories’ field in memory and then filtering it. Meaning it's a lot more memory efficient than doing a $page->get('categories')->find('title%=design'); In this way, it’s similar to the already-supported option to use a field name as a method call, for instance ProcessWire supports $page->field_name('selector'); to achieve a similar result. Dot syntax and bracket syntax together You can use all of these features together. Here’s a few examples from the updated $page->get() phpdocs: // get value guaranteed to be iterable (array, WireArray, or derived) $images = $page->get('image[]'); // Pageimages $categories = $page->get('category[]'); // PageArray // get item by position/index, returns 1 item whether field is single or multi value $file = $page->get('files[0]'); // get first file (or null if files is empty) $file = $page->get('files.first'); // same as above $file = $page->get('files.last'); // get last file $file = $page->get('files[1]'); // get 2nd file (or null if there isn't one) // get titles from Page reference field categories in an array $titles = $page->get('categories.title'); // array of titles $title = $page->get('categories[0].title'); // string of just first title // you can also use a selector in [brackets] for a filtered value // example: get categories with titles matching text 'design' $categories = $page->get('categories[title%=design]'); // PageArray $category = $page->get('categories[title%=design][0]'); // Page or null $titles = $page->get('categories[title%=design].title'); // array of strings $title = $page->get('categories[title%=design].title[0]'); // string or null // remember curly brackets? You can use dot syntax in there too… echo $page->get('Page “{title}” has {categories.count} total categories'); I’m not going to bump the version number this week because a lot of code was updated or added and I’d like to test it for another week before bumping the version number (since it triggers the upgrades module to notify people). But if you decide to upgrade now, please let me know how it works for you or if you run into any issues. Thanks for reading, have a great weekend!
  7. @adrian UserActivity doesn't use ajax on the front-end, so there's nothing to ping to continuously check and verify the activity is still current. Though you still could use it in a non-ajax context, or you could add your own ajax to do it. The reason UserActivity doesn't use ajax on the front-end is just a matter of security... avoiding a place for nuisance garbage to be submitted. It hides its ajax endpoint behind the admin and requires a logged-in user for that endpoint to save an activity. You can use UserActivity's API to save activities on your own though, here's how: $ua = $modules->get('UserActivity'); $activity = $page->url; // i.e. /members/profile-edit/ $ua->saveUserActivity($user, $activity); Then when you want to check if that activity is active, perhaps with a hook into something in ProcessUser or ProcessPageEdit, or even somewhere on your front-end... $ua = $modules->get('UserActivity'); $u = $users->get('adrian'); // user object, name or id $items = $ua->findUserActivity($u, [ 'equals' => '/members/profile-edit/', 'seconds' => 60, // in last n seconds (default=60) ]); if(count($items)) { $this->warning("User may be editing their profile right now"); }
  8. This week we’ve got a new core version on the dev branch (v3.0.204) and a new version of FormBuilder (v53) posted in the FormBuilder board. Relative to ProcessWire 3.0.203 version 3.0.204 has 23 commits containing a mixture of feature improvements and issue resolutions. While there aren't major feature additions here there are a lot of worthwhile improvements and fixes, and I'd recommend the upgrade if using the dev branch. See the dev branch commit log for more details. The new FormBuilder version includes various improvements but the biggest is a new version of the included InputfieldFormBuilderFile module that supports file/image uploads in forms. It has been improved with a preview option (for images), the ability to collect a description for each file, improved multi-file inputs, and more. The new options appear on the “Details” tab when editing a file field in FormBuilder. This new version (v53) is available for download now in the FormBuilder board. If you upgrade an existing installation with file fields, I’d suggest testing out those forms after upgrade, and also review the new options to see if they make sense for your form. Thanks for reading and have a great weekend!
  9. @wbmnfktr Consider the benefits of using a spreadsheet. The benefits of using a Table field are similar, and the data is all accessible, editable and searchable in PW. Table also has many of the benefits of repeaters, but without the overhead. It's much faster in terms of performance and much more efficient in terms of storage (1-1 mapping of input column to table column and input row to table row). On the admin/input side, it's more like using a spreadsheet than a typical Inputfield, which can sometimes be preferable for editors. Table is also much more scalable than other repeatable types (like repeaters). You can have just a few rows or tens of thousands of them, something that you couldn't do with repeaters. Table can also paginate its rows, both input side and front-end. Personally, I use Table on almost every site I develop. One obvious use case I also come across regularly is rate tables like here (click on pricing tab, this is developed with Table). While table is much more scalable than Repeater, it is not as flexible or powerful as Repeater. But when your needs crossover with what Table does, it is certainly preferable.
  10. I hope you are having a great week. Today I've released new versions of two ProcessWire ProFields modules: Table and Combo. In addition to various minor improvements and fixes, the biggest update in both is the addition of file and image fields. Since this is a fairly major feature addition for both modules, please consider these versions in a beta test stage for the moment. Both are available for download in the ProFields board now. Table v23 In ProFields Table both "file" and "image" column types were added. These are fairly basic file upload fields, but a major improvement over not having them at all. It can hold one file per column per row in the table. You can have as many file columns as you need per row. The front-end value of your file or image columns is one you are likely used to using already: Pagefile or Pageimage objects, just like they would be in a regular single-file or image field. This means your file/image values in your Table rows can benefit from many of the built-in methods you already use, such as the ability to present resized images, WebP variations, etc. When you configure a file column you can specify what extensions you want to allow and the maximum number of bytes you are willing for it to accept in an upload. When configuring an image column, you also gain settings to specify the thumbnail image preview size in both regular and HiDPI resolution. Combo v9 The update for Combo fields was similar to the Table field except that file and image fields go quite a bit further in Combo than they could in Table. Specifically, you get almost the full set of InputfieldFile/InputfieldImage configuration settings and features, including the ability to support both single and multi-file uploads, image actions, variations, focus control, description input, tags input and more. The only things missing relative to a regular File/Image field are the manual Crop action (Image field) and the ability to manage separate custom fields for each file or image. The front-end value for your Combo file fields is identical to what it would be on a regular ProcessWire File or Image field. Specifically a Pagefiles or Pageimages (plural) object if supporting multiple files, or a Pagefile or Pageimage (singular) object if supporting 1 file. To say it another way, you can use this exactly as you use other dedicated file/image fields in ProcessWire. Not yet included is the ability to query the properties of a file/image field from selectors, like when using $pages->find(). I'm still working on that part, but since I know most probably don't need that I decided to get this version out first and then continue working on that part. Core updates The core updates this week were mostly minor, even if there were a lot of them. One of the ways that I stay up-to-speed on all the core code is to regularly read through all of it and make small adjustments along the way... anything that makes the code easier to read, or easier for PhpStorm to inspect it. You've seen these kinds of updates pretty much every week for years, but I hadn't thought to mention it before. This week there were more of these kinds of updates than usual so I just wanted to mention that what it's for. What sometimes looks like micro-optimization or minor code changes is usually just me staying fresh and up-to-date with the core. 🙂 That's all for this week. I hope you have a great weekend!
  11. I hope that you all are having a great week! This week I've been working on some updates to the User Activity module and have released version 5 in the ProDevTools board. This version focuses on adding several new requested features, including the following: New options to also detect when you (yourself) are editing the same page in 2+ windows and when you have modified a page in a different window. These are the “collide-self” and “modify-self” options in module config. The module now keeps track of what fields have changed in the page editor and stores them with the activity so that they can be shown in the activity viewer or in page edit alerts. When a page has been modified that is also open to another user it now presents them with a dialog giving the option to reload the page or keep editing. A new “lock” option has been added that blocks a user from editing a page when it is already being edited by another. This is an alternative to just warning them with a pop-up, and it literally prevents them from being able to open the page in the page editor. This can be enabled in the module config and can also be optionally disabled for superuser. A new configuration setting has been added that lets you configure the refresh time in the page list (previously it was not editable). Added feature to limit the “you've been idle for awhile…” to the page editor only. When enabled, idle is still tracked for other admin processes, but idle alerts don't appear. Improvements to ProcessUserActivity (the included activity viewer module), including: 1) visible vs. hidden states are now more obvious. 2) Changes made in page editor are now included in the activity information. 3) The ajax drop-down navigation summary has been improved to include more information. Large portions of the module have been refactored into separate classes for better maintainability and other related improvements. This week there have also been a few commits to the core, but mostly just small fixes and phpdoc improvements, so not enough to write about here, but there likely will be next week. Thanks for reading and have a great weekend!
  12. In this post we cover the details of a new module (ProcessLanguageFieldExportImport) that enables export and import capabilities for multi-language fields in ProcessWire— https://processwire.com/blog/posts/language-field-export-import/
  13. ProcessLanguageFieldExportImport is a multi-language field export/import tool for ProcessWire This is a general support thread for the multi-language field export/import module in the modules directory here and as described in this blog post. This module is also designed to work with several of the ProFields multi-language types. VIP support for this module is available in the ProFields and ProDevTools support boards.
  14. @MrSnoozles Yes you can do this, but not directly. There aren't any file/image functions in comments, but you can use $comment->setMeta('name', 'value'); to store whatever you'd like. So in my case, when someone submits a review with photo attachments, I create a directory /site/assets/comments/[page-id]/[comment-id]/ and then copy the files into it. To do this, take the saveForm() hook code I posted above and append this into it. Note that 'photos' refers to the name if my files field in FormBuilder, but it's also the name I'm using to store an array of photo files (basenames) in my comment meta data: $entry = $processor->getEntry(); if($comment->id && !empty($entry['photos']) && $entry['id']) { $meta = $comment->getMeta(); $path = $processor->entries()->getFilesPath($entry['id']); $meta['photos'] = addCommentPhotos($comment, $entry, $path); $field->updateComment($page, $comment, [ 'meta' => $meta ]); } The snippet above got the meta['photos'] from an addCommentPhotos() function. Here's an example of that function below. It copies the photo files out of the entry, validates them, and places them into the /site/assets/ directory I mentioned above. Note that it requires the FileValidatorImage module. function addCommentPhotos(Comment $comment, array $entry, $sourcePath) { $files = wire()->files; $sanitizer = wire()->sanitizer; $validator = wire()->modules->get('FileValidatorImage'); /** @var FileValidatorImage $validator */ $page = $comment->getPage(); $targetPath = wire()->config->paths->assets . "comments/$page->id/$comment->id/"; $photos = []; // create target directory for files if(!is_dir($targetPath)) $files->mkdir($targetPath, true); foreach($entry['photos'] as $filename) { $sourceName = basename($filename); $sourceFile = $sourcePath . $sourceName; if(!is_file($sourceFile)) continue; // file not found if(!$validator->isValid($sourceFile)) { // photo not valid $processor->addWarning("Error adding photo $sourceName - " . $validator->getReason()); continue; } $sourceExt = pathinfo($sourceFile, PATHINFO_EXTENSION); $sourceName = pathinfo($sourceFile, PATHINFO_FILENAME); $n = 0; // ensure filename is unique do { $targetName = $sanitizer->fieldName($sourceName); if(empty($targetName)) $targetName = 'photo'; $targetName .= ($n ? "-$n" : "") . ".$sourceExt"; $targetFile = $targetPath . $targetName; $n++; } while(is_file($targetFile)); // copy file to target location if($files->copy($sourceFile, $targetFile)) { // populate file basename in $photos $photos[] = $targetName; } } return $photos; } That takes care of getting the photos to the right place. When you want to output then with your comments, here's roughly how you might do it: foreach($page->comments as $comment) { $cite = $comment->getFormatted('cite'); $text = $comment->getFormatted('text'); $when = wireRelativeTimeStr($comment->created); echo "<div class='comment'>"; echo "<p>Posted by: $cite ($when)</p>"; echo "<p>$text</p>"; $photos = $comment->getMeta('photos'); if(!empty($photos)) { $dir = "comments/$page->id/$comment->id/"; $url = $config->urls->assets . $dir; $path = $config->paths->assets . $dir; echo "<ul class='comment-photos'>"; foreach($photos as $basename) { $fileName = $path . $basename; $fileUrl = $sanitizer->entities($url . $basename); if(file_exists($fileName)) { echo "<li><img src='$fileUrl' alt='' /></li>"; } } echo "</ul>"; } echo "</div>"; } But wait, what about when you do things like delete a page, or an individual comment, and want to make sure the photos are deleted too? You can do this by adding hooks into your /site/ready.php file: /** * After page deleted, delete comment files * */ $pages->addHookAfter('deleted', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $path = $event->wire()->config->paths->assets . "comments/$page->id/"; if(is_dir($path)) { $event->wire()->files->rmdir($path, true); $event->message("Deleted page comments files path: $path"); } }); /** * After individual comment deleted, delete comment files * */ $wire->addHookAfter('FieldtypeComments::commentDeleted', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $comment = $event->arguments(2); /** @var Comment $comment */ $path = $event->wire()->config->paths->assets . "comments/$page->id/$comment->id/"; if(is_dir($path)) { $event->wire()->files->rmdir($path, true); $event->message("Deleted comment files path: $path"); } });
  15. @Kiwi Chris Let's say you've got a FormBuilder form named "review" and it has these fields, all of which are required: reviewed_page (Page) where user selects what page they are reviewing reviewer_name (Text) reviewer_email (Email) review_text (Textarea) star_rating (Integer) recommend (Toggle Yes/No) Your comments field is named "comments". We have a template named "form-reviews" where we output this form using FormBuilder embed method C. Above the embed C output code, in that file we have a hook which takes the submitted review and converts it to a pending comment: $forms->addHookAfter('FormBuilderProcessor::saveForm', function(HookEvent $event) { $processor = $event->object; /** @var FormBuilderProcessor $processor */ $form = $event->arguments(0); /** @var InputfieldForm $form */ $field = $event->wire()->fields->get('comments'); /** @var CommentField $field */ $page = $form->getValueByName('reviewed_page'); /** @var Page $page */ $errors = $processor->getErrors(); if(count($errors)) return; // if there were errors, do not create a comment yet $comment = new Comment(); $comment->status = Comment::statusPending; $comment->cite = $form->getValueByName('reviewer_name'); $comment->email = $form->getValueByName('reviewer_email'); $comment->text = $form->getValueByName('review_text'); $comment->stars = $form->getValueByName('star_rating'); // example of using meta data $comment->setMeta('recommended', $form->getValueByName('recommended')); if($field->addComment($page, $comment, true)) { // pending comment added successfully } else { // failed to add comment $processor->addError("Error adding review/comment"); } }); That's basically it. In my case, I have a lot more "meta" fields, so my hook is longer than this. This hook could also be to FormBuilderProcessor::formSubmitSuccess but I choose the saveForm instead because at this point I can still add my own error messages that will be displayed to the user. This gives you the opportunity to do additional validation, should you want to. By the time the formSubmitSuccess method is reached, it's already been determined there were no errors, so at that point you couldn't refuse the form submission if you can't convert it to a Comment for one reason or another. Let's say your Comments field is named "comments", then $page->comments->stars() will return the average of all star ratings on $page (floating point number between 0 and 5.0). For more details, arguments, options see: https://processwire.com/api/ref/comment-array/stars/ https://processwire.com/api/ref/comment-array/render-stars/ https://processwire.com/api/ref/comment-stars/ For rendering stars it'll use ★ but if you want it to use a different icon you can use CommentStars::setDefault() i.e. CommentStars::setDefault('star', '<i class="fa fa-star"></i>')); There are lots of other things you can specify to setDefault() so it's worth looking directly in the file if you decide to use it.
  16. Following up on my July 15th post, I've been working on converting ~1600 reviews from a 3rd party service (BazaarVoice) into ProcessWire for a client's website. That conversion is pretty much done now, here's an example page that has a few reviews (click on the Reviews tab). As you can see, we're using the ProcessWire comments field to store not just the review, but also 15 other details and up to 6 photos, per review. This was all done using the new "meta" features of ProcessWire's comments, mentioned in that post linked above. Though I didn't have an example to show you at the time, so wanted to link it here. Here's another example of how someone can submit a review here. This form is built in FormBuilder and uses an updated FormBuilderFile Inputfield module which will be bundled in the next version of FormBuilder. This new version of InputfieldFormBuilderFile adds a few useful features such as: The ability to show a preview image (when uploading photos). The ability to show just one file input at a time, and reveal additional ones as needed. The ability to include a text or textarea description with each file upload. These are all optional and can be enabled or disabled in the field settings. If none of the options are enabled when the output is exactly the same as before, meaning the upgrade should be seamless for existing forms already using the files input. The way that a submitted review goes from FormBuilder into a comment is automated with a hook to FormBuilder's "saveForm" method. When the form is submitted, it creates a new pending comment, translating most of the fields into 'meta' data in the comment, there's really not much to it. If you ever have a similar need, be sure to let me know and I'm happy to share the hook code that worked for me. In addition to those updates for FormBuilder, this week I've also worked on some related updates to ProcessWire's comments field, especially with improvements to the ProcessCommentsManager module. The module now has a separate dedicated comment editor where you can edit existing comments, and add new comments or replies. Previously you could not add new comments/replies in the comments manager. In the case of the site linked above, they use this particular feature for adding staff replies to reviews. I think this comments stuff is largely done now so next week I'll likely be back to working on other parts of the core and modules. Thanks for reading and have a great weekend!
  17. @Kiwi Chris @MarkE I think that probably could be achieved with a Fieldtype+Inputfield module pair. It sounds kind of like what FieldtypeCombo does except that you define the table structure in Combo, rather than Combo reading an existing table structure. No doubt it would be simpler than Combo since it would just read the table for its settings. Something like translating MySQL VARCHAR column to an InputfieldText, and a TEXT/MEDIUMTEXT column to a Textarea or CKE, and perhaps ENUM/SET columns to InputfieldRadios/InputfieldCheckboxes... that sort of thing. Could be fun to build!
  18. In addition to site development work and ProcessWire core development, this week I upgraded my OS X version (from Mojave to Monterey), my PhpStorm version (from 2018 to 2022), and my Apache/PHP/MySQL environment versions. I tend to wait a bit too long between upgrades on my computer and so I ended up with a lot of things to resolve, and a still few remaining. While waiting for upgrades to install, I randomly came across one of the first sites I ever designed/developed out of college, working for Grafik in the year 2000: https://americanhistory.si.edu/subs/ ... I can't believe it's still there. It looks really dated now (it's 22 years old), but reminded me of how much things have changed in web design/development. While I'd been developing sites for a few years before this, it was just a hobby, and it wasn't until this time that it was my job. Back in 2000 there weren't a lot of people that knew how to create websites and it always felt like you were breaking new ground. Internet Explorer was king (and nobody liked it then either), Google was just a small startup, but AltaVista, InfoSeek and Yahoo were big. Sites were developed in a way that would make you cringe now. I don't think we used CSS hardly at all, but we did use tables for everything layout related. There was no such thing as "mobile" (the iPhone didn't come till 7 years later). There was no such thing as "responsive" layout and accessibility was pretty much an unknown. Most of the time we used images for a lot more than was appropriate (headlines and much more) because HTML and HTML fonts were so limited. It all seems so primitive now but what's the same is that it was fun then and it's fun now, actually it's more fun now. I don't have any point here, just that it's funny to look back at how much as changed. Last week I mentioned that we're likely to upgrade CKEditor 4 to CKEditor 5 sometime in the next year. There were several comments about that and so just wanted to talk a little more about it. First off, I really like CKEditor 4, it's been a great fit for ProcessWire. If the company behind it was going to continue building and supporting version 4 after 2023 then we'd likely stick with it. But the CKEditor 4 end-of-life is sometime in 2023 and I don't think it's an option to stick with it (in the core) after the developer of it is no longer updating or supporting it... at least not long term. While CKEditor 5 is a different animal than CKEditor 4, it's also still the closest upgrade path from CKEditor 4, and I'm hopeful it will be able to serve as a near drop-in replacement from the perspective of users (only). My hope is that by the time we've completed the upgrade to CKE 5, our clients won't necessarily know the difference or have to re-learn anything, unless they want to take advantage of something new exclusive to CKE 5. From my perspective as a developer integrating CKEditor 5 into ProcessWire, the development side is not a drop in replacement for CKE 4 (not even close), as all supporting code will have to be redeveloped. By supporting code, I mean things like the code in the InputfieldCKEditor.module file, the code for our custom CKE plugins (pwimage and pwlink), as well as anything else development related that is referring to CKEditor. There's no doubt it'll be a lot of work. But the rich text editor is one of the most important input types in the ProcessWire admin, so it's fine, it's worth putting a lot of time into. As for CKEditor 5 being bloated relative to CKEditor 4, I very much doubt that's the case. It was a complete rewrite, the folks at CKEditor know what they are doing, and it's safe to assume it's even more optimized and streamlined than CKE 4. In terms of size, the download for CKE 4 and CKE 5 are both 1.7 megabytes. As I understand it, they started with a new codebase for CKEditor 5 in part to start fresh and avoid legacy bloat. So I see this upgrade as being the opposite of bloat. So what happens with CKEditor 4 in ProcessWire when it likely is replaced with CKEditor 5? So long as CKE 5 can be a near drop in replacement for our users, and for the markup it generates, then the CKE 4 module will move out of the core and into an optional module you can install from the modules directory, when/if someone wants it. On the other hand, if the transition is not completely clean between versions then we may end up supporting both in the core for a short period of time. Though I'm hopeful this won't be necessary. There are some other interesting looking editors out there that have been mentioned, and it'd be nice to have more input options available. I see something like Imperavi's Article as a good option for some but not a replacement for our current rich text editor. At least I know my clients would not be happy to have that big of a change occur from a PW version upgrade. Likewise, something like the Easy Markdown Editor is a great option for some, and I'd like to be able to install a module for that and use it in some cases. But folks used to using CKEditor 4 in their work would not be happy to have that big of a change either. CKEditor 4 works really well for what it does, and I think the goal has to be that clients using CKEditor 4 now should be able to continue doing what they are doing with as few changes to their workflow as possible. I'm hopeful we'll be able to get there with CKEditor 5, while also gaining some benefits in the process. Where other input options make a lot of sense is when building a new site where there aren't already users depending on one input method or another. And it may be that at some point (sooner or later) it will make sense for ProcessWire to have another textarea input option that's different enough from CKE to make it worthwhile. But in my opinion, that would be a potential additional option, not a replacement, as CKE is pretty well established and expected in PW.
  19. This week on the dev branch are several updates to the comments system including support for custom fields on comments (which we're calling comment meta data). I'm currently working on a site that uses a reviews system powered by BazaarVoice. It's pretty nice but it's also very expensive (I think at least $500/month in this case). The system powers their reviews which include not just a rating and review text, but also a bunch of other criteria that the user can provide. See an example here — click the "Reviews" tab and what you see there now is currently coming from BazaarVoice. But in a couple of weeks you should see the same thing powered by ProcessWire. Think of this like a comments system with custom fields. That's not something that ProcessWire has supported before, but now this week it does. Though I know most don't need this, so have kept it pretty simple, focusing just on adding API methods to make it possible to get and set custom field values for any comment. These include: $comment->getMeta('name'); $comment->setMeta('name', $value); $comment->removeMeta('name'); The name and $value can be whatever you decide. There's also a bonus $comment->meta() method which combines the methods, letting you get or set, kind of like the meta() method on Page objects. If you want to use comment meta data like this, it's exclusively an API feature. So if you are looking to collect custom fields from users, you'll want to implement your own comment form rather than the default. In our case, we'll be using FormBuilder to implement the comment form that includes the review and custom fields. But you could just as easily use a homegrown form. When it comes to outputting that custom data, you'll want to handle it more like you would outputting a repeater, iterating through the $page->comments and then using $comment->getMeta('name'); for each of the custom properties you want to output. Worth noting is that output formatting doesn't come into play here, so if some text in the meta data needs to be entity encoded for output (for example) then that's your responsibility. How about later editing of the meta data? Should you need it, the ProcessCommentsManager module (Setup > Comments) has been upgraded to support editing of comment meta data. Next to each comment is now a "Meta" link, and if you click it, you'll get a modal window on top of the comment enabling you to edit the meta data as JSON. That might seem a little unconventional, but I'm trying to keep it simple and flexible. Most will probably use this to view meta data rather than edit it, but the ability is there when/if needed. I didn't think it would be worth spending the significant time building a full-blown page-editor like environment for editing comment meta data, but also felt like I needed something like this for occasional editing needs. The InputfieldCommentsAdmin module was also updated to have meta data links for each comment. But the reality is if you have ProcessCommentsManager installed, chances are you aren't editing comments in the page editor anyway. So a new option has been added in the comments field configuration (Input tab) enabling you to disable comments in the page editor and instead link to the editor in the comments manager. When enabled, your Comments field in the page editor would instead look like this: This is worthwhile because the comments manager is just a better place to view/edit comments in the admin since it is paginated, supports editing of all properties, and lets you sort/filter as you see fit. Whereas a big list of comments in the page editor just slows it down. This week the CKEditor version has been upgraded to 4.19.0 (see release notes). I'm also emailing with the people at CKEditor about getting us a license to use CKEditor 5 with ProcessWire. As you may or may not know, the CKEditor 5 license (LGPL) isn't compatible with ProcessWire's license (MPL2 and MIT), so we are working on a license agreement to make it possible. Since CKEditor 4 will reach EOL in 2023 it's a good time to start planning for CKEditor 5 and I'm excited to work with it. Thanks for reading and have a great weekend!
  20. Just a quick update this week. The dev branch version is now at 3.0.202 as of today. Relative to 3.0.201 this includes 22 commits of various fixes, issue resolutions, improvements and additions. While there's nothing major that's new to report in this version, there is nonetheless quite a bit there, and if you are already running on the dev branch it's a worthwhile upgrade. For full details be sure to see the dev branch commit log. Thanks for reading and have a great weekend!
  21. ProcessWire's core comes with a lot of helpful debugging tools and capabilities built-in. In this post we'll cover some of these useful tools and how to use them— https://processwire.com/blog/posts/debugging-tools-built-in/
  22. @joe_g That's right, it doesn't look like there's any reason to provide #maincontent for your situation. When populating markup regions, specify just the region you want to replace, append, prepend, or delete; not a structure of markup regions. Though you did have that case above where you were trying to a direct child to it: <div class="p-8"> outside of your _main.php, so in that case you would, or you could do a <div class="p-8" pw-append="maincontent">...</div> or <div id="maincontent"><div class="p-8">...</div></div>. I recommend adding <!--PW-REGION-DEBUG--> somewhere in your _main.php markup (like at the bottom), and that'll be automatically replaced with some markup that tells you everything your Markup Regions are doing and when it can't find where to place something, etc. Most of my sites have something like this below, and it's handy to look at when you aren't sure why something isn't working, etc. <?php if($config->debug && $user->name === 'ryan'): <div class"uk-container uk-margin"> <!--PW-REGION-DEBUG--> </div> <?php endif; ?> Another tip is that if you ever find Markup Regions are having trouble finding the closing tag for some specific element, you can tell it specially by marking it with a hint comment <!--#maincontent-->. Though I also just find it visually helpful, so I tend to do it whether for markup regions or not. <div id="maincontent"> ... </div><!--#maincontent-->
  23. This week core updates are focused on resolving issue reports. Nearly all of the 10 commits this week resolve one issue or another. Though all are minor, so I'm not bumping the version number just yet, as I'd like to get a little more in the core before bumping the version to 3.0.202. This week I've also continued development on this WP-to-PW site conversion. On this site hundreds of pages are used to represent certain types of vacations, each with a lot of details and fields. Several pages in the site let you list, search and filter these things. When rendering a list of these (which a lot of pages do), it might list 10, 20, 100 or more of them at once on a page (which is to say, there can be a few, or there can be a lot). Each of the items has a lot of markup, compiled from about a dozen fields in each list item. They are kind of expensive to render in terms of time, so caching comes to mind. These pages aren't good candidates for full-page caches (like ProCache, etc.) since they will typically be unique according to a user's query and filters. So using the $cache API var seems like an obvious choice (or MarkupCache). But I didn't really want to spawn a new DB query for each item (as there might be hundreds), plus I had a specific need for when the cache should be reset — I needed it to re-create the cache for each rendered item whenever the cache for it was older than the last modified date of the page it represents. There's a really simple way to do this and it makes a huge difference in performance (for this case at least). Here's a quick recipe for how to make this sort of rendering very fast. But first, let's take a look at the uncached version: // find items matching query $items = $pages->find('...'); // iterate and render each item foreach($items as $item) { echo " <!-- expensive to render markup here ---> <div class='item'> <a href='$item->url'>$item->title</a> ...and so on... </div> "; } That looks simple, but what you don't see is everything that goes in the <div class="item">...</div> which is a lot more than what you see here. (If we were to take the above code literally, just outputting url and title, then there would be little point in caching.) But within each .item element more than a dozen fields are being accessed and output, including images, repeatable items, etc. It takes some time to render. When there's 100 or more of them to render at once, it literally takes 5 seconds. But after adding caching to it, now the same thing takes under 100 milliseconds. Here's the same code as above, but hundreds of times faster, thanks to a little caching: // determine where we want to store cache files for each list item $cachePath = $config->paths->cache . 'my-list-items/'; if(!is_dir($cachePath)) wireMkdir($cachePath); // find items matching query $items = $pages->find('...'); // iterate and render each item foreach($items as $item) { $file = $cachePath . "$item->id.html"; // item's cache file if(file_exists($file) && filemtime($file) > $page->modified) { // cache is newer than page's last mod time, so use the cache echo file_get_contents($file); continue; } $out = " <!-- expensive to render markup here ---> <div class='item'> <a href='$item->url'>$item->title</a> ...and so on... </div> "; echo $out; // save item to cache file so we can use it next time file_put_contents($file, $out, LOCK_EX); } This is a kind of cache that rarely needs to be completely cleared because each item in the cache stays consistent with the modification time of the page it represents. But at least during development, we'll need to occasionally clear all of the items in the cache when we make changes to the markup used for each item. So it's good to have a simple option to clear the cache. In this case, I just have it display a "clear cache" link before or after the list, and it only appears to the superuser: if($user->isSuperuser()) { if($input->get('clear')) wireRmdir($cachePath, true); echo "<a href='./?clear=1'>Clear cache</a>"; } I found this simple cache solution to be very effective and efficient on this site. I'll probably add a file() method to the $cache API var that does the same thing. But as you can see, it's hardly necessary since this sort of cache can be implemented so easily on its own, just using plain PHP file functions. Give it a try sometime if you haven't already. Thanks for reading and have a great weekend!
  24. @joe_g No, you can specify this for each template if you want to. Edit a template and see the "files" tab. Or you could just use your _main.php as a controller that loads different layout files depending on some field value or condition that you determine. Sure you can. Use PHP include(), $files->include() or $files->render() with whatever files or order you want. Regions can be replaced, appended, prepended, removed, etc. as many times as they are output. You can do it individually for templates, see my response for #1. Though I usually structure my templates in a way that the _main.php is the "base" that's generic enough to not have ever needed to define alternate files. I think markup regions are the best way myself. Once you start using them a lot, I suspect you'll find you don't want to use anything else. That's been my experience at least. I don't know exactly what you mean by nested regions. You'd have to provide more details on what you are doing before we could say if you are using something in an intended way or not.
  25. This week I've been busy developing a site. It's the same one as the last few weeks, which is an established site moving out of WordPress and into ProcessWire. Next week the whole thing is getting uploaded to its final server for collaboration and for client preview. I've been pretty focused on getting that ready and don't have any core updates to report this week, though should next week. One thing about the prior version of this site (and perhaps many WordPress sites) is that there wasn't much structure to the pages used in the site, and hundreds of unrelated pages in the site confusingly live off the root, such as /some-product/ and /some-press-release/ and /some-other-thing/. There's no apparent structure or order to it. And those pages that do have some loose structure in the wp-admin have URLs that don't represent that structure on the front-end. There's very little relation between the structure one sees in the wp-admin and the structure that one sees in the URLs, or in the front-end navigation. They all seem to be completely unrelated. That's one thing that I've tried to fix, so that there is some logic and structure rather than having a bunch of unrelated pages all in the same bucket (is this common in WordPress?) But there's one big caveat. We didn't want to change anything about the actual URLs that are used on the site. This is a site with a long history, a lot of incoming links, and a lot of search traffic. The current URLs have been in place a long time and we didn't want to introduce more redirects into the site (there are already a ton of 301 redirects accumulated over time). So we wanted to make sure the existing URLs in the new ProcessWire-powered site are identical to what they were in the WordPress site. That might seem difficult to do, going from an unstructured WordPress site into a highly structured ProcessWire site... but actually it's very simple. Here's how: Making ProcessWire render pages from WordPress URLs We created a new URL field named "href" and added it to most of the site's templates. For established pages that came in from the old WP site, this "href" field contains the WordPress URL for the page. Depending on the page, it contains values like /some-product/ or /some-press-release/, /some-country/some-town/, etc. In most cases this is different from the actual ProcessWire URL, since the page structure is now much more orderly in the back-end. And for newly added pages, we'll be using the more logical ProcessWire URL. But for all the old WordPress pages, we'll make them render from their original URL. This is certainly preferable from an SEO standpoint but also helps to limit the redirects in the site. In order to make $page->url return the old/original WordPress URL (value in $page->href), a hook was added to the /site/init.php file: /** * Update page paths and URLs to use the 'href' field value on pages that have it * */ $wire->addHookBefore('Page::path', function(HookEvent $event) { $page = $event->object; /** @var Page $page */ $href = $page->get('href'); if(!$href) return; // skip if page has no 'href' value $event->return = $href; $event->replace = true; }); Now we've got $page->url calls returning the URLs we want, but how do we get ProcessWire to accept those URLs for rendering pages? The first thing we'll need to do is enable URL segments for the homepage template. We do this by going to: Setup > Templates > home > URLs > and check the box to enable URL segments. Save. Then we need to edit our /site/templates/home.php to identify when URL segments are present and render the appropriate page, rather than the homepage: $href = $input->urlSegmentStr; if($href) { $href = '/' . trim($href, '/') . '/'; $page = $pages->get("href=$href"); if(!$page->id || !$page->viewable()) wire404(); $wire->wire('page', $page); // set new $page API var include($page->template->filename); // include page's template file } else { // render homepage output } As you can see from the above, when URL segments are present, we find a page that has an "href" field value matching those URL segments ($input->urlSegmentStr). If we don't find one, we stop with a 404. But if we do find one, then we set the $page API variable to it and then include its template file, making that page render rather than the homepage. If there is no $input->urlSegmentStr present then of course we just render the homepage. That's it! By using these little bits of code snippets to replace ProcessWire's URL routing, now all the URLs of the old WordPress site are handled by ProcessWire. Like most things in ProcessWire, there's more than one way to accomplish this… We could have used URL/path hooks, or we could have hooked before Page::render to take over homepage requests with URL segments, before the homepage even got involved. Or perhaps we could have hooked in even earlier, to something in the ProcessPageView module or PagesRequest class. Or we could have used an existing module. Any of these might be equally good (or even better) solutions, but I just went with what seemed like the simplest route, one that I could easily see and adjust. Plus, it'll work in any version of ProcessWire. The actual solution I used is a little more than what's presented here, as it has a few fallbacks for finding pages and scanning redirect lists, plus passes along remaining pagination/URL segments to rendered pages. I'm guessing most don't need that stuff, and it adds a decent chunk of code, so I left that out. But there are a couple of optional additions that I would recommend adding in a lot of cases: Forcing pages to only render from their "href" URL and not their ProcessWire URL (optional) Our existing hooks ensure that any URLs output for pages having "href" values are always based on that "href" value. But what if someone accesses a given page at its ProcessWire path/url? The page would render normally. We might want to prevent that from happening, ensuring that it only ever renders at its defined "href" URL instead. If the page is requested at its ProcessWire URL, then we redirect to its "href" URL. We can do that by adding the following code to our /site/templates/_init.php file: if($page->id > 1) { // ensure that we are rendering from the 'href' URL // rather than native PW url, when href is populated $href = $page->get('href'); $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; if($href && $requestUrl && strpos($requestUrl, $href) === false) { // href value is not present in request URL, so redirect to it $session->redirect($config->urls->root . ltrim($href, '/')); } } Enforcing uniqueness and slashes in "href" values (optional) It's worthwhile to enforce our "href" values being consistent in having both a leading and trailing slash, as well as making sure they are always unique, so no two pages can have the same "href" value. To do that, I added this hook to my /site/ready.php file (/site/init.php would also work): /** * Ensure that any saved 'href' values are unique and have leading/trailing slashes * */ $wire->addHookAfter('Pages::savePageOrFieldReady', function(HookEvent $event) { $page = $event->arguments(0); /** @var Page $page */ $fieldName = $event->arguments(1); /** @var string $fieldName */ if($fieldName === 'href' || in_array('href', $page->getChanges())) { // the href field is being saved $href = $page->get('href'); if(!strlen($href)) return; // make sure value has leading and trailing slashes if(strpos($href, '/') === 0 && substr($href, -1) === '/') { // already has slashes } else { $href = '/' . trim($href, '/') . '/'; $page->set('href', $href); } // make sure that the 'href' value is unique $pages = $event->object; /** @var Pages $pages */ $p = $pages->get("id!=$page->id, href=$href"); if($p->id && !$p->isTrash()) { $page->set('href', ''); $page->error( "Path of '$href' is already in use by page $p->id “{$p->title}” - " . "Please enter a different “href” path and save again" ); } } }); That's all for this week. Thanks for reading, have a great weekend!
  • Create New...