Leaderboard
Popular Content
Showing content with the highest reputation on 05/01/2013 in all areas
-
Hi fellow forum friends. I've released (on github at least) my new module - Page Edit Fold Status. May not yet be live in modules section of the site. https://github.com/robphilp/PageEditFoldStatus What this does is use a combo of JQuery, Ajax and the session to remember the state of each field when you edit and then save a page, so that when you return to the page after the save (or later) it will recall that state and fold/unfold all the fields so the edit page is how you last left it. We have pages with a LOT of fields on, and we found it irritating to have them all default to open. That said, if they default to closed and you have to drill down to edit some specific bit of data, it's annoying to have to drill-down again each time when you are making many incremental edits, so I made this module to store the state on a per-page and per-user basis. e.g. some users will need to focus on certain fields, so if they have these open and all others closed, it will retain this state each time they edit a page. No need to open/close these fields each time so you can focus on what's important to you. Because of the use of the session, this behaviour SHOULD last for the lifetime of your login. I hope it is helpful! Just realised I've put this in the wrong forum, can someone move it for me?! Thanks!6 points
-
A little bit of new fun stuff on the 2.3 dev branch: Anonymous functions and hooks You can now use PHP 5.3+ anonymous functions when attaching hooks. All the syntax below will work either from your template files, modules, or any Wire derived class. If using outside of template files or Wire-derived classes, then replace $this with wire(). Though anywhere that you can use $this you can also use wire(), so syntax is also a matter of preference. Example: Add a "hello()" function to all pages that returns "Hello World!": $this->addHook('Page::hello', function($event) { $event->return = "Hello World!"; }); Result: echo $anyPage->hello(); // outputs: Hello World! Example: Add a 'hi' property to all Users that contains the value "Hi [their name]!". $this->addHookProperty('User::hi', function($event) { $user = $event->object; $event->return = "Hi $user->name!"; }); Result: echo $user->hi; // outputs: Hi Ryan! Example: Add a grandparent() function to all pages. $this->addHook('Page::grandparent', function($event) { $page = $event->object; $event->return = $page->parent->parent; }); Result: $myPage = $pages->get('/about/news/categories/sports/'); echo $myPage->grandparent()->url; // outputs: /about/news/ Example: Save a message to a log file for each new page that is added. $pages->addHookAfter('added', function($event) { $page = $event->arguments(0); $this->message("Added new page: $page->path", true); }); Result: The message is added to log file /site/assets/logs/messages.txt when a new page is added. Example: Add a 'createdString' property to all Page objects that is the same as $page->created except that it outputs in a human readable format rather than a unix timestamp: $this->addHookProperty('Page::createdString', function($event) { $page = $event->object; $event->return = date('Y-m-d H:i:s', $page->created); }); Result: echo $page->createdString; // outputs 2013-04-30 03:05:00 This just touches on the possibilities with some overly simple examples. But you can use these anywhere you'd use any other hooks: from within modules, templates, or anywhere in ProcessWire. I think that the anonymous function syntax is particularly convenient within templates, when you can define your own helper functions in your head.inc or init.php or whatever file you have starting your templates. Don't forget you can add stuff in /site/templates/admin.php too in order to hook into things that happen during administration. Error and message logs ProcessWire has always maintained logs, but I'm guessing that most don't know how to write to them. So I setup a simpler syntax for you to save things to PW's system log files: $this->message("This text will be saved to /site/assets/logs/messages.txt", true); $this->error("This text will be saved to /site/assets/logs/errors.txt", true); This is the same as these functions worked before, except for the second "true" argument, which tells it to write to the log file. From the admin side, it'll still display these messages interactively. If you only want it to log and not display the messages in the admin, then replace the "true" with "Notice::logOnly". Though if you are doing this from templates, then you don't need to consider that since templates (other than the admin template) don't typically output ProcessWire's notices. --- Edit: I changed the wire() syntax in the examples to $this. Though either works from template files and modules, but I think $this syntax is more familiar to most.5 points
-
This might be obvious since MariaDB should be totally compatible with mySQL, but just wanted to say that everything seems to be working fine with running pw with it after doing some selector tests.3 points
-
Brilliant answers. My only other comment would be to suggest copying the information you posted above on to the official docs so that it's easier to find for all! Thanks again.3 points
-
No. MySQL can execute hundreds of well optimized queries faster than one unoptimized one. And MySQL can often execute lots of simple queries faster than 1 complex one. ProcessWire takes advantage of this where it can. Query counting isn't useful anymore (if it ever was).3 points
-
Just want to notice how a question about layout became a discussion about frameworks. I rest my case2 points
-
And while on the CSS Framework discussion it is quite easy to use a preprocessor to get what you need and customize the framework exactly as you need it. Twitter Boostrap uses LESS and Foundation uses SASS. This lets you take what you need from them.2 points
-
2 points
-
"Recommended by Mr. Log Himself", it seems. We're not getting off-topic, are we?2 points
-
"Hook*r, directly from the creator of After Shave Actions".. yeah, that sounds just about right2 points
-
Not that it matters much, but you might want to reconsider that statement: http://nikic.github.io/2012/01/09/Disproving-the-Single-Quotes-Performance-Myth.html. Quotes don't matter, but concat method ("foo" . $var . "bar") on the other hand does tend to be slightly easier on eyes because many editors won't actually recognize a variable inside quotes (ie. string interpolation) as something that should / could be highlighted. So, essentially, use whichever you prefer. Just wanted to point out that micro-optimizations like these are a) futile and b) often misunderstood -- and thus generally not worth even thinking about2 points
-
..and now I can let go of trying to come up with a less questionable name for a planned module listing active hooks! Working title was Hook*r.2 points
-
2 points
-
You have to consider XSS anywhere that the [untrusted] user can enter content that will be saved and echoed back to other users. In a default ProcessWire installation with no 3rd party modules, there aren't many places to consider this. The admin is designed for trusted users only. The comments module is the only thing that accepts and stores untrusted user input. It won't store markup in comments. It filters by stripping markup before storing a comment, and by entity encoding output when displaying a comment. Also consider that ProcessWire is designed to let you do what you want to do. If you want to create a text/textarea field that lets you enter script tags, then ProcessWire won't stop you. There are plenty of legitimate uses for such things. There simply isn't anywhere that an untrusted user can enter markup. But HTMLPurifier might have a place in your individual installation. If you are dealing with untrusted user input and allowing markup (HTML) then HTMLPurifier is essential. We have an HTMLPurifier module, ready to use. Content is validated and sanitized server-side, not client side. Though on text-based fields entered in the admin, we make no assumptions about what you are using them for. Even scripts are a legitimate use case (consider video embedding, etc.). Some Inputfields may have client-side validation built-in to do one thing or another (TinyMCE is an example). Though ProcessWire doesn't care about client side validation. What sanitization/validation takes place depends entirely upon the Fieldtype. From the admin side, the text-based fieldtypes are designed to contain any text, including any kind of markup. This is one reason why ProcessWire's admin is focused on trusted users only. While I don't recommend using PW's admin with untrusted users, there may be situations where you have "mostly" trusted users. In such cases, you may want to avoid things like rich text fields and focus on LML-based text formatters (safe ones, like Textile Restricted or BBCode) or even HTML Purifier. Our new CKEditor module actually does use the HTML Purifier for inline mode, since inline mode markup is not entity-encoded when presented in the admin. This is a consideration for any text fields that you don't want to support markup. Such fields might include your 'title' and 'summary' fields, but would exclude something like your 'body' field, which is designed specifically to contain markup. ProcessWire uses a type of modules called Textformatters. For any field that you want to have runtime output formatting, you can add an HTML Entities (or other types) Textformatter from Setup > Fields > [your field] > Details. This is only applicable to text-based fields. For entity encoding output yourself, use $sanitizer->entities($str). Generally you wouldn't ever use this on your managed content stored in PW. Instead, you would use it when echoing back something that the user entered, like search query: $q = $input->get->q; if($q) { $results = $pages->find("body%=" . $sanitizer->selectorValue($q)); echo "<h2>You searched for: " . $sanitizer->entities($q) . "</h2>"; if(count($results)) echo $results->render(); else echo "<p>Sorry, no results were found.</p>"; } ProcessWire's API does not use SQL queries, unless you choose to use the $db variable. So if you want to execute SQL queries, then you should follow best practices towards avoiding SQL injection. However, there are very few scenarios where any of us use SQL in developing websites or apps in ProcessWire, so it's generally a non-issue. What you do have to consider instead is selector injection, even if it's not nearly as potentially dangerous as SQL injection. So that's why we have the $sanitizer->selectorValue(); function. Note the use of it in the code segment above. Yes, all ProcessWire forms are CSRF protected.2 points
-
Regarding moving assets (as in static site files, JS and images etc.) outside /site/: Keeping everything "site-specific" within /site/ can make upgrading ProcessWire slightly easier and enables you to move all site-specific files to another server / PW installation etc. without needing to wonder which files were / are part of PW and which belong to this specific site. This is also how (and why) modules such as Site Profile Exporter work; they simply copy (most) content of /site/ folder and leave everything else intact. So, short answer is "yes, you can do it." But that doesn't necessarily mean that you should do it2 points
-
I found (after 2-3 Projects using PW) that it's a good technique to use templates in a way I think hasn't been thought of yet really by some. (Although the CMS we use at work for year, works this way.) I'm sure I'm maybe wrong and someone else is already doing something similar. But I wanted to share this for everybody, just to show alternative way of using the brillant system that PW is. Delegate Template approach I tend to do a setup like this: - I create a main.php with the main html markup, no includes. So the whole html structure is there. - I then create page templates in PW without a file associated. I just name them let's say: basic-page, blog-entry, news-entry... but there's no basic-page.php actually. - Then after creating the template I make it use the "main" as alternative under "Advanced" settings tab. So it's using the main.php as the template file. - This allows to use all templates having the same php master template "main.php" - Then I create a folder and call it something like "/site/templates/view/", in which I create the inc files for the different template types. So there would be a basic-page.inc, blog-entry.inc ... - Then in the main.php template file I use following code to delegate what .inc should be included depending on the name of the template the page requested has. Using the TemplateFile functions you can use the render method, and assign variables to give to the inc explicitly, or you could also use just regular php include() technic. <?php /* * template views depending on template name * using TemplateFile method of PW */ // delegate render view template file // all page templates use "main.php" as alternative template file if( $page->template ) { $t = new TemplateFile($config->paths->templates . "view/{$page->template}.inc"); //$t->set("arr1", $somevar); echo $t->render(); } <?php /* * template views depending on template name * using regular php include */ if( $page->template ) { include($config->paths->templates . "view/{$page->template}.inc"); } I chosen this approach mainly because I hate splitting up the "main" template with head.inc and foot.inc etc. although I was also using this quite a lot, I like the delegate approach better. Having only one main.php which contains the complete html structure makes it easier for me to see/control whats going on. Hope this will be useful to someone. Cheers1 point
-
So, I've been seeing some email-related topics around here and actually had quite a few struggles of my own with this very subject myself lately. Thing is that sending email should be easy, but it's not always that; especially for those who have to work on multiple, low-price (and regrettably often low-quality) platforms that may or may not provide proper mail servers.. or prefer to host their services themselves and still want to avoid setting up and maintaining a mail server. Hosting a mail server can be real pain in the ass when things don't work like they should, not to mention that most people have very little knowledge about DNS entries etc. this requires. Anyway, long story short: yesterday I started thinking that wouldn't it be sweet to have a layer of abstraction within ProcessWire for sending email? Of course one could still use PHP mail() -- there's no way and no sense in even trying to stop that -- but using a common gateway would definitely bring in some extra value. This layer I'm talking about could by default use built-in PHP mail() but also make it possible to override it, thus allowing multitude of options that PHP mail(), being bound to Sendmail / it's alternatives, can't offer without additional server-side software (such as Nullmailer.) By making sending emails hookable it could also enable all kinds of interesting tricks to be done when mail is sent -- such as writing a custom log file, sending another email to someone else, updating local content (I'd imagine that this could be useful for building newsletter platform, for an example) and so on. Since words tend to fail me at times like these, I put together a quick proof of concept of what I'm talking about here, accompanied by one example of what could be achieved by doing this: A very simple yet functional Mailer class Two commits on top here list all the changes I've made in my PW fork to make this work -- including the fact that I've altered some default modules to use $mailer->send() instead of mail() SwiftMailer module, again very simple but fully functional (though only tested with Gmail SMTP) drop-in replacement for PHP mail() So, what do you folks think of this? Please keep in mind that this is just a suggestion and I'm not saying that this is the right path to take especially considering that it would add another API variable -- it just felt like best option here and I couldn't think of cleaner way to achieve it.1 point
-
IMPORTANT This is now well and truly out of date! I strongly suggest ignoring it. The Bootwire System This is a series of proof-of-concept profiles based on ProcessWire 2.3 (forthcoming) and the Twitter Bootstrap Framework They are currently using Bootstrap 2.3 and JQuery 1.9.1 Bootwire Starter Profile Version 0.5 This version if for those who just want to leap in and get their hands dirty, It includes almost no content at all, very few fields and templates and very little markup. The normal Bootstrap navigation is in place for reference. More details are in the readme. Download Here: http://stonywebsites.co.uk/Bootwire-Starter-Profile.zip Github: https://github.com/jsanglier/Bootwire-Starter-Profile ============================================================ All versions come with a Site Settings page which is a child of a Content Management page in the page directory. This is used for common elements such as site title. The Starter Version has almost nothing in it! Some additional tweaks have been added to TinyMCE and these profiles are pre-loaded with the Modules Manager, the Profile Exporter and the Page Delete module. Please use the Module Manager to ensure all modules are up to date. NEW: Now with some demo functionality in a functions.inc file. Bootwire Demo Profile Built on the starter profile, this version demonstrates some Twitter Bootstrap functionality in the context of a ProcessWire installation. The Demo can be viewed here: http://bootwiredemo.stonywebsites.co.uk/ Download here: http://stonywebsites.co.uk/Bootwire-Demo-Profile.zip Github: https://github.com/jsanglier/bootwire/ Bootwire Admin Theme I have created an admin theme using Bootstrap 2.2.2. It is functioning but should be treated as a work in progress. However, you may like it. It has its own thread here. http://processwire.com/talk/topic/2640-bootwire-admin-proof-of-concept-admin-theme/ ============================================================== Future Profiles Bootwire Blog Profile This is intended to be a basic blog system using the Bootstrap framework. I am planning to start from scratch with the blog, basically because I need the exercise. It will not be as clever as Ryan's system, but it will have the following features (er ... possibly): Category Tree Tag System In-Article gallery Media pages Alternate themes for posts Lots of global settings Manual related posts Automated related posts (okay, probably beyond me) er ,,,, some other great feature that I have not thought of yet.1 point
-
Example: https://github.com/stojg/slycrop/blob/master/slycrop/code/SlyCropEntropy.php Seems like this might be a nice way to auto-crop.1 point
-
You can move your /wire/ dir wherever you want and use symlinks. However, I don't think this would do anything for security. The benefit of doing it would be just if you want to share the same /wire/ dir amont multiple installs. I think that some CMS products are prone to vulnerable files and so you want to move them out of web root. ProcessWire is not one of those products and your .htaccess file protects several directories from web access. You can try modifying PW's /index.php file to choose a different $config->assets dir. But I don't recommend it, as I've not tested it. Yes, you can store these assets wherever you want. ProcessWire doesn't actually link to them, only you do, in your template files. As a result, it doesn't matter where you put them.1 point
-
Hi horst, The exception says that there is already a page under this parent with the same name. Just save the page without acutally setting the name. ProcessWire does this for you: The system builds the name from the title and if a page with the name already exists, Pw appends -1,-2.....-n $p->name = wire('sanitizer')->name($title); //uncomment or remove this line Why getting the NullPage? Maybe your title is not stored exactly the same way (spaces).1 point
-
If the /faculty/ template does have access rights set, yes then it wouldn't work as you get the page explicitly with get("/faculty/"). But for children when I remove view access for a role they won't show up in navigation unless you explicitly check the option to show them in lists and searches.1 point
-
Uncomment the line 20: https://github.com/apeisa/CustomPageRoles/blob/master/CustomPageRoles.module#L20 There were some side-effects to that, so test well and good luck1 point
-
This looks nice! http://learnlayout.com/ And it does end with listing some frameworks, although they state:1 point
-
The CustomPageRoles module is alpha, proof of concept and it says that pages still will be rendered in navigation. You'd have to exclude them when cycling the pages in the loop with $page->viewable(). But implementing this check in the modules manager would get kinda complicated and result in some potential problems with how it all works together. The MarkupSimpleNavigation is access aware, but CustomPageRoles doesn't exclude pages when using with find() or children() calls, that's why it doesn't work also with MSN (heh). You can do use page_roles (page field) to do what you want with a simple check for user roles simply with this: echo $treeMenu->render(array("selector" => "page_roles={$user->roles}")); Because of the use of a selector you have to add and define page roles for all pages you have in navigation. It's not using the viewable access method, but simply checking the page field for if any of the user's roles is added.1 point
-
I think I found the mistake. The class name is ProcessPageEdit, so: public function init() { $this->addHookAfter('ProcessPageEdit::execute', $this, 'example'); } Very helpful: http://processwire.com/api/hooks/1 point
-
Great idea, will definitely try it out! One minor thing I noticed after taking a quick look at your code: you should make sure that $_SERVER['REQUEST_URI'] actually exists before doing that strpos() at init. When bootstrapping ProcessWire for local use (shell scripts etc.) it won't be there and you'll get a PHP notice for that. Not fatal, but annoying in the long run1 point
-
Can't recall where I saw it, but earlier this week someone suggested that finding a way to remove 1 jpeg from a site design is the quickest and easiest optimisation possible.1 point
-
Hi, I am sanitizing some variables coming in via GET through Ajax as follows before passing them on to a selector. Is this the correct way of doing it? Secondly, can I instead type cast where I am expecting integers? Thanks. $sort = explode(" ", $sanitizer->selectorValue($input->get->SortX)); $sortOrder = $sort[1] == "DESC" ? "-" : "" ; $sortValue = $sort[0]; $start = $sanitizer->selectorValue($input->get->StartX); $limit = $sanitizer->selectorValue($input->get->SizeX); //Would this suffice as well since I am expecting integers here? //$start = (int) $input->get->StartX; //$limit = (int) $input->get->SizeX; //What about this (access as array index)? //$start = (int) $input->get["StartX"]; //$limit = (int) $input->get["SizeX"]; $results = $pages->find("has_parent!=2, id!=2|7, include=all, start=$start, limit=$limit, sort=$sortOrder$sortValue");1 point
-
1 point
-
I've got this feeling that hooks debug section is going to be my new best friend. That information could have prevented couple of very nasty situations already. Glad to have it here now, awesome stuff Ryan!1 point
-
1 point
-
1 point
-
So, - that thing with the Titles (sanitizing but leave spaces, etc) seams to be solved. Also basic stream support is added. The output to the frontend is only a simple menu and information about current page. It's made for coders You may have a look to output (it's a bit like var_dump) and the code in the 2 Templatefiles. That's intended as starting point for your own Frontend creation. There is an own caching mechanism with the LocalAudioFiles-module. It caches the menus and the DB-infos. It refreshes them when DB gets modified or you may permanently disable caching in the modules page. Here are the latest screencast: https://youtu.be/qeT5s013GUE and here without audio for the german audience https://youtu.be/MefyBCDDXrs I want upload the first site profile, but have got an error when importing (Can't save page 0: /genre//: It has empty 'name' field). So I first want to fix that and upload siteprofile after that. (Maybe tomorrow evening)1 point
-
Hi BrendonKoz, For an active project I'm using Pw templates and fields to model/store my data coming from another database. But I don't let users see the Pages tree. For some data, I'm not using the ProcessWire 'ProcessPageEdit' Process to edit the pages. I created my own Process module which displays the data from a page in a form. Then I save everything via API. This way you can quickly use standard form modules from Pw but you're also free to generate your own markup beside that. For example the repeaters could (maybe) be displayed in a table which doesn't eat up that much vertical space. With the 'InputfieldMarkup' module you can display whatever kind of stuff you like - would be a solution to display calculations / metrics for your second project mentioned.1 point
-
Wow.. Ryan, you're awesome and so is your CMS... I'm really loving how friendly and easy and at the same time crazy powerful it is. The attention you give to each person or thread here is also admirable. Thanks a lot, and let me know if you need help with anything "design related", although you seem to have it covered (great deisgn and UI everywhere), I'd be more than happy to help! Thanks again I'll try this approach and loop you in on how it went.1 point
-
Also, you don't need anything more than this: foreach($page->side_bar_widgets as $widget) echo $widget->render();1 point
-
Yes helper functions like this can be included using a separate php like in the head.inc and used throughout your templates. I extracted this function from a module I have for a project. This module has various helper function and then I load it in the templates. It's much the same as if I would include a php with functions and just personal preference. For example: $helpers = $modules->get("TemplateHelpers"); then use it like this where I need it. echo $helpers->wordLimiter($page->body); I'm not sure what you mean by applying the function to the body. I use this function to create teaser texts that are limited, and show the complete body only on the detail page. Of course you could modify the body output, that every time you do an echo $page->body, it will run it through a function, but I'm not sure this is a good practice. This using a hook on the formatValue of textfields would do it: (directly in template like a include, or by making it a module) function wordLimiter(HookEvent $event){ $field = $event->argumentsByName('field'); if($field->name != 'body') return; $str = $event->return; $limit = 150; $endstr = ' …'; $str = strip_tags($str); if(strlen($str) <= $limit) return; $out = substr($str, 0, $limit); $pos = strrpos($out, " "); if ($pos>0) { $out = substr($out, 0, $pos); } return $event->return = $out .= $endstr; } wire()->addHookAfter("FieldtypeTextarea::formatValue", null, "wordLimiter"); // now this will trigger the above hook echo $page->body; But it's a little cumbersome, as you can't set the limit. Also this strips tags and on HTML text you'll lose formatting. But just to show adn example what is possible. From your post I guess you like to do something like: echo $page->body->limit(150); // not possible It's not possible to do this, because the $page->body, body is just a string and not an object you could add methods to it. But something like the following would be possible using hooks. echo $page->wordLimiter("body", 120); You can use addHook to add a method wordLimiter to page: function wordLimiter(HookEvent $event){ $field = $event->arguments[0]; // first argument $limit = $event->arguments[1]; $endstr = isset($event->arguments[2]) ? $event->arguments[2] : ' …'; $page = $event->object; // the page $str = $page->get($field); $str = strip_tags($str); if(strlen($str) <= $limit) return; $out = substr($str, 0, $limit); $pos = strrpos($out, " "); if ($pos>0) { $out = substr($out, 0, $pos); } return $event->return = $out .= $endstr; } // this will add a custom method to Page object wire()->addHook("Page::wordLimiter", null, "wordLimiter"); // now you can do this echo $page->wordLimiter("body", 100); // or this echo $page->wordLimiter("summary", 100);1 point
-
Sorry it's been a while since I last updated you guys on this theme. I was moving house when I started this theme and then I didn't have the internet for a few weeks and then I was ill. lol. But I'm all better now and I've continued developing this theme. Before you see the new screenshots of it's progress I just want to share what I found while developing this theme. I found the markup for the admin theme a bit messy. Styling all the fieldsets for example was quite a challenge because some fieldsets are within fieldsets and so you end up with this doubling up effect. As I started to discover more of the admin theme I realised there are a lot of user experience issues and some fieldsets can be quite heavy. Once I've finished this theme I would very much like to offer my help in suggestions for improving the overal experience of the admin. So I took all my learning into consideration and tried to think of the best way to develop a theme that I could build and suited ProcessWire's current way of presenting information; which so far has led me to this approach. I haven't included the header or footers in these screenshots because I'm still playing around with it. Example of editing Guest role: Example of editing Admin user: Example of editing Field module: As you can see it's changed quite a bit, but I'm feeling a lot more confident about where I want to go with this theme. I'll keep you guys updated in the following weeks as to when I'll release an alpha version of it so you guys can help me find any quirky bugs with it. Thanks for listening.1 point
-
'1' is string that has text, which contains one number. 1 is integer 1 === 1 (true, because they have same value and type) 1 === "1" (false, because you compare integer to a string) 1 == "1" (true, because == doesn't care about the type)1 point
-
I can only applaud PW is a great little CMS, and your logical and reasoning seem to be just right in every part of it.1 point
-
This example assumes that you've already setup your fields and added the pages just as they are in the link you posted. It further assumes that each staff member is a page living under /about/staff/. An example would be: /about/staff/jemma-weston/ I'm going to assume these are the field names: title (person's first/last name) image (person's photo) team (per your link) job_title (per your link) email (per your link) To output the staff listing, you would do something like this: <?php echo "<ul class='staff_listing'>"; foreach($page->children as $person) { if($person->image) { // make 150px wide thumbnail $image = $person->image->width(150); $img = "<img src='{$image->url}' alt='{$person->title}' />"; } else { // make some placeholder (optional) $img = "<span class='image_placeholder'>Image not available</span>"; } echo " <li> $img <ul class='staff_detail'> <li><strong>Name:</strong> {$person->title}</li> <li><strong>Team:</strong> {$person->team}</li> <li><strong>Job Title:</strong> {$person->job_title}</li> <li><strong>Email: {$person->email}</li> </ul> </li> "; } echo "</ul>"; Optional Another scenario might be where you want the "team" or "job_title" fields to be from a pre-selected list, and to themselves be pages that the user could click on to view everyone in the "Audit and Accounts" team (for example). The way you would set that up is to create a new structure of pages that has all of the Team types (and/or Job Titles), like this: /about/staff/teams/audit-and-accounts/ /about/staff/teams/business-services/ /about/staff/teams/practice-management/ ...and so on... I would use a new template for your team types, something like team_type.php. The only field it needs to have is a title field. At the same time, I would move your staff members into their own parent like /about/staff/people/. In your staff template, you would make the "team" field be of the field type called "Page". When you create it, it will ask you from what set of pages you want it to use, and you can tell us to use /about/staff/teams/. It'll ask you if you want it to hold one page or multiple pages (PageArray), and you'll want to choose Page (one page). Likewise, where it asks you to select what type of input field you want to use, choose a "Select" or "Radio Buttons" field (and not one of the multiple select types). Add that new "team" field to the staff template, and when you add/edit a staff member, you'll select a team rather than type one in. Your markup to output that field in the staff listing would instead be like this: <li><strong>Team:</strong> <a href='{$person->team->url}'>{$person->team->title}</a></li> Your team_type.php template would list the staff members just like your main /about/staff/ page did (it might even be good to include your staff_list template to do it for you, or convert it to a reusable function), except that it would load the staff members like this: <?php $staff = $pages->get("/about/staff/people/")->children("team=$page"); foreach($staff as $person) { // ... } The main thing to note in the example above is the "team=$page" portion, which is essentially telling ProcessWire to pull all staff members that have a "team" field that points to the current page. So if you are viewing the /about/staff/teams/audit-and-accounts/ page, then it's going to load all staff members that are in that team.1 point