Leaderboard
Popular Content
Showing content with the highest reputation on 11/13/2012 in all areas
-
Thanks for your updates. This module has been updated with your changes, so it now supports drag-n-drop positioning. While I was in there, I added reverse geocoding (generating an address from the marker position), live geocoding of the address changes (previously it required a submit) and a toggle checkbox to enable/disable geocoding.3 points
-
Clients usually love PW, exactly because of the reasons we stated. The worst type of clients, the almost tech-savvy ones, usually like the videos/idea of CMS, where you push 8 buttons and have everything set… then you show them the admin screens and they flee in panic. And when it comes to clients who are willing to compromise on the quality, just so they can use some technology… Well, I try to catch this early on, and just end the relationship, then and there.2 points
-
WireArray types may be associative and are designed to support direct access via indexes. So you could have a collision with a WireArray that has a index of "count" (or one of the other suggested direct access properties). While it's certainly feasible to support that for numerically indexed WireArrays, I'm reluctant to introduce conventions that can't stay consistent across WireArrays.2 points
-
Since this thread is related , I'm also posting this here. I wrote a little helper module to find related pages with score and sorting by score AND modified. Module code can be found here: https://gist.github.com/3558974 $pages->findRelated( Page $page, PageArray $tags, string $field [, string $tmpl, int $limit] ); It add a $pages->findRelated() method. You can do something like this: $found = $pages->findRelated($page, $page->tags, 'tags', 'product|product2', 100); if($found) { foreach( $found->slice(0,5) as $rel ) { echo "<p><a href='$rel->url'>$rel->title</a> ($rel->score)</p>"; } } else { echo "<p>No related Products found</p>"; }2 points
-
MarkupCache is a simple module that enables you to cache any individual parts in a template. I'm working on a site now that has 500+ cities in a select pulldown generated from ProcessWire pages. Loading 500+ pages and creating the select options on every pageview isn't terribly efficient, but I didn't want to cache the whole template because it needed to support dynamic parameters in the URL. The solution was to cache just the code that generated the select options. Here's an example: $cache = $modules->get("MarkupCache"); if(!$data = $cache->get("something")) { // ... generate your markup in $data ... $cache->save($data); } echo $data; I left the markup generation code (the part that gets cached) out of the example above to keep it simple. Below is the same example as above, but filled out with code that finds the pages and generates the markup (the part that gets cached): $cache = $modules->get("MarkupCache"); if(!$data = $cache->get("city_options")) { foreach($pages->find("template=city, sort=name") as $city) { $data .= "<option value='{$city->id}'>{$city->title}</option>"; } $cache->save($data); } echo $data; That was an example of a place where this module might be useful, but of course this module can used to cache any snippets of code. By default, it caches the markup for an hour. If you wanted to cache it for a longer or shorter amount of time, you would just specify the number of seconds as a second parameter in the get() call. For example, this would keep a 60-second cache of the data: $cache->get("city_options", 60) I hope to have it posted to GitHub soon and it will be included in the ProcessWire distribution. If anyone wants it now, just let me know and I'll post it here or email it to you.1 point
-
This module creates a page in the ProcessWire admin where you can test selectors and browse page data and properties without editing a template file or a bootstrapped script. Given selector string is used as is to find pages - only limit is added, if given. Errors are catched and displayed so anything can be tested. Pages found with a valid selector are listed (id, title) with links to the page. I was thinking this would be useful for someone new to ProcessWire, but it turns out I'm using it myself all the time. Maybe someone else finds it useful as well. Module can be downloaded here: https://github.com/n...essSelectorTest Modules directory: http://modules.processwire.com/modules/process-selector-test/ Features Edit selector string and display results (and possible errors as reported by ProcessWire) Explore properties and data of matching pages in a tree viewLanguage aware: multi-language and language-alternate fields supported Repeater fields and values Images and their variations on disk More data is loaded on-demand as the tree is traversed deeper Quick links to edit/view pages, edit templates and run new selectors (select pages with the same template or children of a page) Page statuses visualized like in default admin theme Add pagination Screenshots1 point
-
EDIT: This project has been put on ice - I don't work with ProcessWire in my day job anymore, so this project is looking for a new maintainer. Knowing that, you can decide whether it's worthwhile reading through 7 pages of posts EDIT: The source code has been dumped on GitHub - feel free to fork and have at it. There's one thing about ProcessWire that pains me, and I've brought this up before - it's the same problem I have with e.g. Drupal... Because the meta-data (Configuration, Fields and Templates) is stored inside the database, once you have a live site and a development site, moving changes from the development site to the live site is, well, not really possible. Repeating all the changes by hand on the live site is simply not an option I'm willing to consider. Telling the client to back off the site and give me a day or two to make the changes, and then moving the whole database after making and testing the changes on a development site, is really a pretty poor solution, too. I had heard some talk about a module in development, which would make it possible to import/export Fields and Templates? It sounds like that would mostly solve the problem. Ideally though, I'd really like a solution that records changes to Fields and Templates, and allows me to continuously integrate changes from one server to another. So I started hacking out a module, but I'm not sure if it's going to work at all, if it's even a good idea, or if it's worth the effort. I'm looking for feedback on the idea as such, more than the code I wrote, which isn't real pretty right now. Anyway, have a look: https://gist.github.com/b7269bb7bd814ecf54fb If you install this, create a "data" folder under the module's folder - migration files will be written there. Basic overview of the idea and code: The module hooks into Fields::load() and takes a "snapshot" of the current Field properties and settings on start-up. It also hooks into ProcessField::fieldSave() and when a field is saved, it compares it's properties and settings to the snapshot it took at startup - if changes were made, it writes the previous name and updated properties into a file. The same thing is not implemented for Templates yet, but would be. The migration-files are named like "YYYY-mm-dd-HH-mm-ss.json", so that they can be sorted and applied in order. Each file contains a JSON representation of a method-call - currently only updateField() which would repeat a previous set of changes and apply them to another installation of a site. (not implemented) So basically, the module would record changes to Fields and Templates, and would be able to repeat them. How those files get from one system to another is less of a concern - would be cool if servers could exchange migrations semi-automatically, using some kind of private key system, but initially, simply copying the files by hand would suffice. I'm more concerned about the whole idea of capturing changes and repeating them this way. Any thoughts? Is this approach wrong for some reason? Would this even work?1 point
-
I’ve been working on a simple admin theme. I originally just wanted to add a simple dashboard area on the home page to display some quick links to key actions and documentation for clients, but I ended up doing a whole theme. The main focus of the theme is for the client / editor role, so it’s not been optimised for the developer usage yet. There are a few enhancements which are aimed at clients (opening previews in a new window, showing tree actions on hover). I have also tried to optimise it for mobile layout. You can see a preview on this video It’s using the Bootstrap framework and Open Sans font. The main issues I currently have are a conflict with the Bootstrap framework scripts and the older version of Jquery that ships with the PW admin. If I upgrade to Jquery 1.8.2 a lot of PW admin functionality breaks (sorting, ask select, modals). If I stick with the currently shipped version of jQuery 1.6, the bootstrap scripts do not work (drop downs, message alerts, mobile navigation). The other big issue, is I made a few simple hacks to some core js files (/wire/modules/Process/ProcessPageList/ProcessPageList.js, and /wire/modules/Jquery/JqueryWireTabs/JqueryWireTabs.js) - this was mainly to insert extra css classes here and there or to show if the tree has children. Is there a better way to do this? Other issues I am thinking about Is there a way to modify the “add new page” workflow? So when the user adds a new page, I’d like to change the default “You are adding a page using the …” message. Maybe this could be an additionally template field called “instructions” or “”details” ? It could be a used as a kind of “templates documentation”, which could be used to document the project for other devs and designers and for the clients / editors. How can you modify the login screen without overriding this file (/wire/modules/Process/ProcessLogin/ProcessLogin.module)? Also not to sure if having two save buttons is good for usability - maybe I will just have one in the header and make it fixed as you scroll.1 point
-
You are thinking well here. Repeaters were created for a small number of repetitions. For a less predictable amount that justifies paginating would be better to use normal pages. In your case, you would have a page "testimonials" as a child of "our customers" and each children of "testimonials" would be one testimonial. The template for those pages would have the fields that you put on the repeater. foreach($pages->find('template=testimonial') as $testimonial) { echo "<blockquote><p>{$testimonial->testimonial_body}</p>"; echo "<small>{$testimonial->testimonial_customer}</small></blockquote> "; echo "<hr>"; } edit: It's also good to add that you are not forced to create a template file for all templates. In the case of the testimonials pages, if you are not planning to display them individualy, just create their template without a corresponding file, and their URLs won't be accessible in the browser.1 point
-
you. have form bilder in /wire/modules + /site/modules? donut do.it can .youclear module cache? rm site/assets/cache/Modules.* /site/assets/ all. write able? chmod -R uog+rw /site/assets1 point
-
1 point
-
1 point
-
Fix for that is coming at some point: http://processwire.com/about/roadmap/1 point
-
[/quotamos]why.you want to do.it ? .. donut do.it may be.fun sometime homo .generos nodes u. love, ? notthing.wrong withe that I .under stand ? i. like to add a suffox .sometime too. good flexibile we.have .will try it out1 point
-
I wanted to use Soma's Sliders to generate hsl colours.( 3 sliders ) But Older IE needs rgb. here's just a bunch of code I used/created to manage it. maybe helpful. <?php header("Content-type: text/css"); // rgbToHsl function rgbToHsl ( array $rgb ) { list( $r, $g, $b ) = $rgb; $r/= 255; $g/= 255; $b/= 255; $max = max( $r, $g, $b ); $min = min( $r, $g, $b ); $h; $s; $l = ( $max + $min )/ 2; if ( $max == $min ) { $h = $s = 0; } else { $d = $max - $min; $s = $l > 0.5 ? $d/ ( 2 - $max - $min ) : $d/ ( $max + $min ); switch( $max ) { case $r: $h = ( $g - $b )/ $d + ( $g < $b ? 6 : 0 ); break; case $g: $h = ( $b - $r )/ $d + 2; break; case $b: $h = ( $r - $g )/ $d + 4; break; } $h/= 6; } return array( $h, $s, $l ); } // hslToRgb function hslToRgb ( array $hsl ) { list( $h, $s, $l ) = $hsl; $r; $g; $b; if ( $s == 0 ) { $r = $g = $b = $l; } else { $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s; $p = 2 * $l - $q; $r = hueToRgb( $p, $q, $h + 1/ 3 ); $g = hueToRgb( $p, $q, $h ); $b = hueToRgb( $p, $q, $h - 1/ 3 ); } return array( round( $r * 255 ), round( $g * 255 ), round( $b * 255 ) ); } // Convert percentages to points (0-255) function normalizeCssRgb ( array $rgb ) { foreach ( $rgb as &$val ) { if ( strpos( $val, '%' ) !== false ) { $val = str_replace( '%', '', $val ); $val = round( $val * 2.55 ); } } return $rgb; } // cssHslToRgb function cssHslToRgb ( array $hsl ) { // Normalize the hue degree value then convert to float $h = array_shift( $hsl ); $h = $h % 360; if ( $h < 0 ) { $h = 360 + $h; } $h = $h/ 360; // Convert s and l to floats foreach ( $hsl as &$val ) { $val = str_replace( '%', '', $val ); $val/= 100; } list( $s, $l ) = $hsl; $hsl = array( $h, $s, $l ); $rgb = hslToRgb( $hsl ); return $rgb; } // hueToRgb function hueToRgb ( $p, $q, $t ) { if ( $t < 0 ) $t += 1; if ( $t > 1 ) $t -= 1; if ( $t < 1/6 ) return $p + ( $q - $p ) * 6 * $t; if ( $t < 1/2 ) return $q; if ( $t < 2/3 ) return $p + ( $q - $p ) * ( 2/ 3 - $t ) * 6; return $p; } // rgbToHex function rgbToHex ( array $rgb ) { $hex_out = '#'; foreach ( $rgb as $val ) { $hex_out .= str_pad( dechex( $val ), 2, '0', STR_PAD_LEFT ); } return $hex_out; } // hexToRgb function hexToRgb ( $hex ) { $hex = substr( $hex, 1 ); // Handle shortened format if ( strlen( $hex ) === 3 ) { $long_hex = array(); foreach ( str_split( $hex ) as $val ) { $long_hex[] = $val . $val; } $hex = $long_hex; } else { $hex = str_split( $hex, 2 ); } return array_map( 'hexdec', $hex ); } /** * * Output to a "r, g, b" string * */ function colorStringRGB($h, $s, $l ) { $output = ''; $hsl = array($h, $s, $l); $color = cssHslToRgb($hsl); foreach( $color as $rgb ) $output .= $rgb . ', '; $output = rtrim( $output, ', ' ); return $output; } // --------------------------------------------------------------------------------------- $out = ''; $sections = array( 'aa' => 'value-a', 'bb' => 'value-b', 'cc' => 'value-c', 'dd' => 'value-d', 'ee' => 'value-e', 'ff' => 'value-f', 'gg' => 'value-g', ); /** * * Converts HSL to RGB. * * */ $minH = $page->css_hue->min; $maxH = $page->css_hue->max; $count = count($sections); $steps = round(( $maxH - $minH ) / $count); $minL = $page->css_lightness->min; $maxL = $page->css_lightness->max; // l is defined in the foreach $h = $page->css_check == 1 ? $minH : $maxH; $s = $page->css_saturation; foreach($sections as $key => $value) { $out .= "\n\nli." . $key . ' {'; $out .= "\n\t" . 'background-color: rgb(' . colorStringRGB($h, ($s+20), $minL) . ');'; $out .= "\n" . '}'; $out .= "\n"; $out .= "\n\ndiv." . $key . ' {'; $out .= "\n\t" . 'background-color: rgb(' . colorStringRGB($h, ($s+20), $minL) . ');'; $out .= "\n" . '}'; $out .= "\n"; $out .= "\n." . $key . ' .column { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(2n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(3n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(5n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(7n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(11n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(13n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(17n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column:nth-child(23n) { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column.r1.c8 { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $out .= "\n." . $key . ' .column.r3.c1 { background-color: rgb(' . colorStringRGB($h, $s, rand($minL,$maxL)) . '); }'; $h = $page->css_check == 1 ? $h + $steps : $h - $steps; } echo $out; ?>1 point
-
You could also output it as a js var and use it in scripts. <script> var mycolor = "#<?php echo $page->color?>"; </script>1 point
-
You could create a styles.php template. And add your colorpicker there. then on top of that PHP file: <?php header("Content-type: text/css"); ?>1 point
-
just an example: <style> body{ background-color: <?=$page->backColor?>; } </style>1 point
-
That’s what I did in my current project. I have a “more topics in this blog“ box on every tag page. The box features a list of links to all tags – except the current tag page. So every tag page has a different "more topics" list, wich is cached for 24 hours. Probably a far longer cache period would be reasonable, since I’m not going to add new tags every day. <?php $tags = $pages->find("template=tag, sort=title"); $cache_topic_name = 'more_tags_list_for_' . $page->name; $cache = $modules->get("MarkupCache"); if(!$more_tags_list = $cache->get($cache_topic_name, 86400)) { $more_tags_list = '<ul>'; foreach ($tags as $tag) { if ($pages->find("template=blog-article, tags=$tag")->count() > 0) { $more_tags_list .= '<li><a href="' . $tag->url . '">' . $tag->title . '</a></li>'; } } $more_tags_list .= '</ul>'; $cache->save($more_tags_list); } echo $more_tags_list; ?>1 point
-
/** * Save the data to the cache * * Must be preceded by a call to get() so that you have set the cache unique name * * @param string $data Data to cache * @return int Number of bytes written to cache, or FALSE on failure. * */ public function save($data) { if(!$this->cache) throw new WireException("You must attempt to retrieve a cache first, before you can save it."); $result = $this->cache->save($data); $this->cache = null; return $result; } Does this answer you question?1 point
-
Here's a bit of code that you might find useful. It's something that I referred to earlier in this thread. Say you have a group of pages that represent businesses - like entries in a business directory. Put something like this at the top of your template for each business. <?php $siblings = $page->siblings(); foreach($siblings as &$sibling){ $pagetext = strtolower($page->title.' '.$page->city.' '.$page->postcode); $siblingtext = strtolower($sibling->title.' '.$sibling->city.' '.$sibling->postcode); $stopwords = array('limited','shop','emporium'); $pagetext = str_replace($stopwords,'',$pagetext); $siblingtext = str_replace($stopwords,'',$siblingtext); similar_text($pagetext, $siblingtext, $sim); $sibling->similarity = $sim; } $siblings->sort('-similarity'); ?> $stopwords are words you want to be ignored in the comparison (in my real-world use they are business-specific terms that a great many businesses have in their names). Then to display a list of similar entries... <?php $score = 40; if($siblings->eq(1)->similarity >= $score){ echo '<h3>Similar Entries</h3>'; echo '<ul>'; for($i = 1;$i <= 10;$i++){ if($siblings->eq($i)->similarity < $score) continue; echo "<li><a href='{$siblings->eq($i)->url}' title='{$siblings->eq($i)->title}, {$siblings->eq($i)->address}, {$siblings->eq($i)->city} {$siblings->eq($i)->postcode}'>{$siblings->eq($i)->title}</a>"; if($user->isSuperuser()) echo ' '.round($siblings->eq($i)->similarity, 2); echo "</li>"; } echo '</ul>'; } ?> Adjust $score to get satisfactory results. (And the bonus prize is that logged-in super users get to see the score in the page output.)1 point
-
@Michael: AFAIK PW takes that query and creates one SQL query for MySQL, so it isn't that CPU intensive - hundreds of rows are nothing for MySQL. But then again, there are ways to improve this, if it's too much: caching the results will give you first boost. Then, if it grows over time (and is killing the server), you always can go the 'half-static' way -having the numbers saved somewhere, and regenerate results based on hook to Pages::save(), etc. I think you get the point.1 point
-
I recommend taking a look at the API http://www.processwire.com/api/ and running through all the great information there. For a complete rundown on everything you can do in a template, try Soma's cheatsheet http://www.processwi...api/cheatsheet/ For everything you said you had to do, I think PW would be a great fit, it doesn't even sound that custom to me. Tags and categories would be just their own pages and link to articles using a page reference field. Short/long versions is easy, in your template you'll just check if the user is logged in eg: if ($user->isLoggedin()) { // show long version echo $page->body; } else { // show teaser echo $page->summary; } For PDFs, are you uploading them yourself? If so, it's just a case of adding a file field to your template and then linking to it/them. PW sounds like a perfect fit to me. If you need any help with code, just ask.1 point
-
You can make a custom module for this. Just copy the image inputField and change the label. Or even better, leave the description for the alt, and add the url field to it.1 point
-
In Google Webmaster Tools you can actually get an average position of processwire.com for a given keyword.1 point
-
Sure, I'll be glad to share any numbers I have access to. Here's a graph showing traffic growth since the project started. Currently we get 500-1000 visits a day, depending on the day. We may be quite small in terms of traffic still, but our trend is upward. Other than WordPress, it appears that most other well-known CMSs are trending downward in terms of search volume.1 point
-
1 point
-
We should probably setup a separate Module development forum, perhaps as a subforum of the Modules/Plugins forum. I will plan to do this as soon as there's time to go through and organize all the threads into the right place.1 point
-
You might be able to avoid a core modification here, because the markup already has identification for children (which is how the PageList js knows it has children). The presence of a span.PageListNumChildren as a child of any .PageListItem indicates that a page has children. Lets say you wanted to add that "hasChildren" class to all items that had children: $(".PageListNumChildren").each(function() { $(this).parent(".PageListItem").addClass("hasChildren"); }); You can also target the .PageListNumChildren span directly with CSS, as this is what contains the numbered quantity of children in its text. Let me know if this doesn't solve it, I don't mind adding another class here if it helps.1 point
-
You can currently do this by modifying or extending the CommentForm class. Copy /wire/modules/Fieldtype/FieldtypeComments/CommentForm.php to your own location, like in /site/templates/includes/ or something like that. Then rename the class to be something else, like "CommentFormMichael" or whatever you'd like it to be named. Modify the class to suit your needs. Then when you output your comments, use your class rather than the one that comes with PW: echo $page->comments->render(); require("./includes/CommentFormMichael.php"); $form = new CommentFormMichael($page, $page->comments); echo $form->render(); This same approach can be taken with the comments list rendering too, except that you would copy or extend the CommentList class rather than the CommentForm class.1 point
-
1.) He's using Joomla. He can't understand this. 2.) It's not about the number of features. Actually, the less, the better- are you really using all of them? 75% of them? 50% of them? 3.) Years of testing… Right. (come on. If anything has 100% of code years-tested, it's probably outdated) 4.) Want to do it different way than your precious module? Yea, fuck you. [This is 95% applicable for WP/drupal as well) --- With building on huge CMS + modules, you end up doing a lot of compromises, which I'm not willing to do. And those are also the bad compromises, the ones users see (unlike e.g. Rails, which makes do stuff his way, but output and experience are in your own hands) --- Ultimately, I'm not selling my clients my ability to stick modules together. I sell the best possible website (in terms of Bussiness Goals and UX) I can create.1 point
-
Just made version 1.1 of this module available (GitHub). Changes (additions actually) in latest version: Explore properties and data of matching pages in a tree view Language aware: multi-language and language-alternate fields supported Repeater fields and values Images and their variations on disk More data is loaded on-demand as the tree is traversed deeper [*]Quick links to edit/view pages, edit templates and run new selectors (select pages with the same template or children of a page) [*]Page statuses visualized like in default admin theme I'll update the first post in this thread and include some screenshots there as well.1 point
-
Have you tried adding this to the "TinyMCE Advanced Configuration Options" - go to the field which has TinyMCE and look under the Input tab. If that does not work you can also add the paste text button quite easily - pastetext Also the paste word button - pasteword1 point
-
We absolutely need something for db migrations / versioning db with git or such, if anyone wants to throw money at Ryan1 point
-
Would it be possible to add drag and drop for pinning a location? Like this: http://jsfiddle.net/salman/ZW9jP/4/ Got it working. Just the updating of the address filed when manually moving the marker is missing. /** * Display a Google Map and pinpoint a location for InputfieldMapMarker * */ var InputfieldMapMarker = { options: { zoom: 5, draggable: true, center: null, mapTypeId: google.maps.MapTypeId.HYBRID, scrollwheel: false, mapTypeControlOptions: { style: google.maps.MapTypeControlStyle.DROPDOWN_MENU }, scaleControl: false, }, init: function(mapId, lat, lng) { var options = InputfieldMapMarker.options; options.center = new google.maps.LatLng(lat, lng); options.zoom = 5; var map = new google.maps.Map(document.getElementById(mapId), options); var marker = new google.maps.Marker({ position: options.center, map: map, draggable: options.draggable }); document.getElementById("_Inputfield_karta_lat").value = marker.getPosition().lat(); document.getElementById("_Inputfield_karta_lng").value = marker.getPosition().lng(); google.maps.event.addListener(marker, 'dragend', function (event) { document.getElementById("_Inputfield_karta_lat").value = this.getPosition().lat(); document.getElementById("_Inputfield_karta_lng").value = this.getPosition().lng(); }); } }; $(document).ready(function() { $(".InputfieldMapMarkerMap").each(function() { var $t = $(this); InputfieldMapMarker.init($t.attr('id'), $t.attr('data-lat'), $t.attr('data-lng')); }); });1 point
-
Thomas, I just today implemented something similar and remembered the logic from your post. Simple and great solution. I made few modifications - most notably I save the related pages into page field also. I also run this code only once per page, since this is part of the import script. Here is how I modified your code: foreach($page->tags as $tag) { $related_pages = $pages->find("template=article, tags=$tag"); foreach($related_pages as $related) { if($related->id != $page->id) $related_id[$related->id]++; } } arsort($related_id); // We limit it to three related pages at max $related_id = array_slice($related_id, 0, 3, true); foreach($related_id as $key => $val){ $rp = $pages->get($key); $page->related_articles->add($rp); }1 point