Leaderboard
Popular Content
Showing content with the highest reputation on 11/11/2016 in all areas
-
While we don't have a new core version for you this week, 3.0.41 will be here next week, with 3.0.40 becoming the new master at the same time. This week, I'll introduce something new I've been working on for awhile here (ProfilerPro), and think some of you might find it as useful as I have. https://processwire.com/blog/posts/a-look-at-the-new-profilerpro-module/6 points
-
Oh, but there is: It's not just for internal links, though you could probably configure it for that.6 points
-
It's all about checking the $event. Using Tracy, inside your runPatch() method, do this: bd($event); That will return something like: Now that shows that you are looking for: $event->arguments[0] Just to confirm, do: bd($event->arguments[0]); and you'll get: which shows that you can simply do: if($event->arguments[0] === 'MyModuleName') {4 points
-
Hi @Robin S and everyone. I have turned off live dumping by default for the bd() method and added a new bdl() or barDumpLive() method. You can choose which you want to use knowing that bdl() will ignore maxDepth and maxLength settings (thereby dumping the full contents of an object etc) and will be much faster to render the output, but at least for now, it has the side effect of sometimes dumping the wrong object as noted above. bd() will always dump the correct object, but obviously slower and limited by the maxDepth and maxLength settings. As noted in the links referenced in this issue (https://github.com/adrianbj/TracyDebugger/issues/6), there may be a way to make live dumping always reliable in which case I will combine the methods again. Let me know if you have any problems.4 points
-
@Nukro, did you solve the issue of the number of items on the last page of pagination? For example, if you make setTotal(50) and limit=7 and your find() actually finds more than 50 pages, then the second to last page will have results 43-49, meaning you want the last page to have only one result. But it will instead show 7 results giving you 56 results in total instead of the 50 you want. I played around and this seems to solve that: $limit = 7; // could be set from a field in your page $items = $pages->find("has_parent=1, limit=$limit"); // whatever selector, this was just for testing $total = min($items->getTotal(), 50); $items->setTotal($total); $remainder = $total - $items->getStart(); if($remainder < $limit) { $items->removeItems(range($remainder, $limit))->setTotal($total); } $start = $items->getStart() + 1; $end = $items->getStart() + count($items); echo "<p>Showing items $start - $end of $total</p>"; echo $items->each("<p>{title}</p>"); echo $items->renderPager();4 points
-
3 points
-
ConnectPageFields Allows the connecting of two related Page fields so that changing one updates the other. Purpose of module An example: suppose your website is about movies. You have a template "movie" with Page field "actors". For each movie you add the actors that appear in the movie. All good, but what if you want to find results like... the 10 actors who have appeared in the most movies actors who haven't appeared in any movies since 1990 You cannot retrieve these pages with a single efficient $pages->find() query, and must load a large PageArray into memory in order to iterate or filter it. For the sake of making these types of queries more efficient you could structure your templates/fields so that movies are added to actors instead, but this may be a less comfortable workflow and can run into equivalent problems (e.g. "find the 10 movies with the largest cast"). The solution is to have a two-way relationship so that movie pages have an "actors" Page field and actor pages have a "movies" Page field. This module will keep these two Page fields in sync so that adding "Ryan Gosling" to "Drive" automatically adds "Drive" to "Ryan Gosling". Also, you can select the same Page field in both Page field A and Page field B. For example, create a "Related" Page field for related pages. Choose "Related" for both fields in a pair in the module config. Now when you add "Orange buffoon" to Related for "Donald Trump", "Donald Trump" is automatically added to Related for "Orange buffoon". Usage Install the ConnectPageFields module. If you haven't already done so, create the two Page fields you want to connect and add them to templates. In the module config select the two Page fields in a "Connected field pair" row as Page field A and Page field B. You can add rows as needed using the "Add another row" button. Troubleshooting Make sure you have set the "Selectable Pages" settings for each Page field correctly: The settings for Page field A should allow pages using the template(s) that Page field B has been added to. The settings for Page field B should allow pages using the template(s) that Page field A has been added to. http://modules.processwire.com/modules/connect-page-fields/ https://github.com/Toutouwai/ConnectPageFields Module config: Demo showing how changing one Page field updates the other:2 points
-
I think you will need to modify your system / setup for this. One possible solution could be: add a hidden field to all templates / pages that may contain internal links hook into saveReady, check / collect all internal links from the current page save the current page id into the hidden field of each internal linked page Simplest solution to inform your client, is not to hide the "hidden field", but to show it readonly. If there are entries visible (that could be direct clickable links that open that page for editing into modals!), he has to change the content in that pages before. Additionally, you can hook into before page delete / page trash, and check the hidden field for entries programatically, to avoid accidental deletions before correcting the internal links. Another solution could be some sort of centralized maintained list / collection of all link relations. But your client seems to be enough tech savy so that the simple solution may fit already.2 points
-
I made an update 4 month ago to solve this. Unfortunately it hasn't been changed on github, but it is marked as changed in my desktop tool. Don't know why. Anyway I will make an update today. I have the module running under 3.0.39 without problems. Please update to 1.2.12 points
-
2 points
-
As Robin S recognized in your comment you are talking about OR, but your code is doing something else. This one should be able to replace your code (not tested) $all = $pages->find("template=player, team.name=$selectedTeam"); $selector = "(people.count>=3), (places.count>=3),(people.count>=2, places.count>=1),(people.count>=1, places.count>=2)"; $allConcerned = $all->filter($selector); $notConcerned = $all->not($selector); As far as I know there is no diff function like array_diff for WireArrays.2 points
-
I think you're looking for OR groups: https://processwire.com/api/selectors/#or-groups $allConcerned = $pages->find("template=player, team.name=$selectedTeam, (people.count>=3), (places.count>=3)");2 points
-
@Speed, to start learning about hooks have a read of this documentation page. It's not a simple concept to grasp so don't worry if you don't understand all of it at first. You also need to know a bit about Object-oriented programming terms like class and method. Regarding MarkupSimpleNavigation, first understand what it is doing: it takes all the pages in your website (or all the pages you give it) and loops over them, looking at each page (item) one at a time and outputting some markup for that page. It has some markup that it uses by default but we can override that using a hook. getItemString() is a method in the MarkupSimpleNavigation class - you can get some idea what it does from it's name. It gets the string (markup) for the item (the page in the loop that MSN is currently looking at). In the hook example I gave we are saying to MSN "if the item you are currently looking at is a direct child of the Home page, don't output the default markup but output this special markup instead". Methods can have arguments (variables) that are used inside the method. When you hook a method you can use those variables in your hook, which can be handy for what you want to achieve. To know what arguments/variables are available you need to look at the method code. If you look at the code for getItemString() you can see there are two arguments: $tpl and $page. For this hook we want to get $page (the item MSN is looking at) so we can test it to see if the page is a direct child of the Home page. $page->parent->id doesn't mean the home page. What this says is "for the parent of this page, get the ID property". Each page has a unique ID - one way you can see the ID of a page is to hover the "Edit" link for a page in the tree and look at the link URL. You'll see something like ".../page/edit/?id=1234". The Home page always has an ID of 1 (there might be some very rare exceptions but we won't worry about that). So in the hook we say if the page's parent has an ID that equals 1 then the page is a direct child of the Home page so output this special markup.2 points
-
It won't be a repeater exactly but I can do something that achieves a similar result. Bit busy right now but will update the module over the next few days.2 points
-
This is a beta release, so some extra caution is recommended. So far the module has been successfully tested on at least ProcessWire 2.7.2 and 3.0.18, but at least in theory it should work for 2.4/2.5 versions of ProcessWire too. GitHub repo: https://github.com/teppokoivula/ProcessLinkChecker (see README.md for more techy details, settings etc.) What you see is ... This is a module that adds back-end tools for tracking down broken links and unnecessary redirects. That's pretty much all there is to these views right now; I'm still contemplating whether it should also provide a link text section (for SEO purposes etc.) and/or other features. The magic behind the scenes The admin tool (Process module) is about half of Link Checker; the other half is a PHP class called Link Crawler. This is a tool for collecting links from a ProcessWire site, analysing them and storing the outcome to custom database tables. Link Crawler is intended to be triggered via a cron task, but there's also a GUI tool for running the checker. This is a slow process and can result in issues, but for smaller sites and debugging purposes the GUI method works just fine. Just be patient; the data will be there once you wait long enough Now what? For the time being I'd appreciate any comments about the way this is heading and/or whether it's useful to you at all. What would you add to make it more useful for your own use cases? I'm going to continue working on this for sure (it's been a really fun project), but wouldn't mind being pushed to the correct direction early on. This module is already in active use on two relatively big sites I manage. Lately I haven't had any issues with the module, but please consider this a beta release nevertheless; it hasn't been widely tested, and that alone is a reason to avoid calling it "stable" quite yet. Screenshots Dashboard: List of broken links: List of redirects: Check now tool/tab:1 point
-
Tracy Debugger for ProcessWire The ultimate “swiss army knife” debugging and development tool for the ProcessWire CMF/CMS Integrates and extends Nette's Tracy debugging tool and adds 35+ custom tools designed for effective ProcessWire debugging and lightning fast development The most comprehensive set of instructions and examples is available at: https://adrianbj.github.io/TracyDebugger Modules Directory: http://modules.processwire.com/modules/tracy-debugger/ Github: https://github.com/adrianbj/TracyDebugger A big thanks to @tpr for introducing me to Tracy and for the idea for this module and for significant feedback, testing, and feature suggestions.1 point
-
Are you talking about the Tracy debugAll() method? When you say it won't show those fields, do you mean the various properties of the images? The output in the PW Info panel is "manually" generated to include all those things. I haven't come across a built in PW method that outputs everything cleanly like that. Take a look at the code I used to generate that array: https://github.com/adrianbj/TracyDebugger/blob/ac3ff2f6cdfcb151d53f7938d3f4ec270f23b115/ProcesswireInfoPanel.inc#L313-L3671 point
-
Thanks a lot, this helps a lot as I can now see the fields from the Admin. Is there a reason why debugAll won't show those nicely formatted fields when I debug from the frontend, but the ProcessWire popup will?1 point
-
You need to create a function which hooks in LanguageTranslator::getTranslation() to manipulate the result.1 point
-
This is a start for you that comes from the Migrator module: $p = $pages->get() // use this to get the page that you are importing comments to foreach($comments as $comment){ $c = new Comment(); $p->of(false); $c->text = $comment['text']; $c->cite = $comment['cite']; $c->email = $comment['email']; $c->ip = $comment['ip']; $c->website = $comment['website']; $p->fieldname->add($c); $p->save(); // setting the status doesn't current work due to this: https://github.com/ryancramerdesign/ProcessWire/issues/1034 $c->status = $comment->status ? $comment->status : 0; //need to set after saving to allow setting status without being subject to moderation settings $p->save(); } This assumes that you have already parsed the rows of the CSV file into the $comments array. To do that, take a look at: http://php.net/manual/en/function.str-getcsv.php Hope that helps to get you going.1 point
-
Take a look here how to manipulate forms: https://bitbucket.org/pwFoo/frontenduser/wiki/Login extensions and plugins https://bitbucket.org/pwFoo/frontenduser/wiki/Register extensions and plugins https://bitbucket.org/pwFoo/frontenduser/wiki/Code snippets / Examples You could try to hook FrontendUser::save $fu->addHookBefore('save', function($event) { $userObj = wire('fu')->userObj; $form = wire('fu')->form; // Do... }); Or username or email field // hook after field processed (form need to be submitted) $myField->addHookAfter('processInput', function($event) { $form = wire('fu')->form; $currentField = $event->object; // Do ... }1 point
-
@pwFoo Could you give me an example how to do this? I think this could be of help for others too. EDIT: pls note that I do not want to send the username with the form, because it can be manipulated. See my previous post.1 point
-
1 point
-
@modifiedcontent Thank you for your suggestion. But relying on JavaScript is not good. I also think that a rewrite is not needed. I don´t want no username. I just don´t want the username in the form because it could be manipulated. The username should be the same as the email address, but sanitized as pageName.1 point
-
It could also be done with a PW hook Make username hidden and not required. Hook into form process and set the required user object value server side.1 point
-
1 point
-
Hi, I would like to do a register form with email pre-registration validation, but without the username field. Instead the username should be the email sanitzed as a pageName. I have the following code right now: <?php namespace ProcessWire; // prepare register form // Additional email pre-register validation plugin (built-in) $fu->register(array('email', 'emailValidation', 'password')); $fu->form->setMarkup($form_markup); $fu->form->setClasses = ($form_classes); $fu->addHookBefore('FrontendUser::save', function ($event) { $user = wire('fu')->userObj; $form = wire('fu')->form; if (!count($form->getErrors())) { // set the username to sanitized email value // $user->name = $form->fhValue('email', 'pageName'); $user->addRole('editor'); } }); // process register / form submit $fu->process($profile_url); $register_form = $fu->render(); $view->set('registerForm', $register_form); // this is for Twig But when I submit that form I get an error: Call to a member function getErrors() on null in line 94 of FrontendUserRegisterEmailValidation.module I think it is because in the function "hookRegisterFormAfterProcess" there is line 84: $user = $form->get('username'); As I send no username, this can not work. So how can I make it work without touching the FrontendUserRegisterEmailValidation.module? Thanks in advance.1 point
-
1 point
-
If you get this error message: That means that ProcessWire is in production mode (as it is by default) and is not reporting errors as a security best practice. However, if you are just installing ProcessWire or developing a site in it, this error message is hardly helpful. Here are three ways you can find out what error occurred: Look at the last line in the file /site/assets/logs/errors.txt. You have to examine this file directly on your file system as PW makes this file non-web accessible. If you entered your email address for the admin user account when you installed ProcessWire, it will email the fatal error message to you. Though if you are running PW on a machine that doesn't send email, that may not help. Edit the file /site/config.php and change the line that says $config->debug = false; to: $config->debug = true; That will force PW to output it's error messages rather than saying "Unable to complete this request due to an error." Note however that you do not want "debug" turned on with a production site, as it's not good security to reveal technical details like this to users of your site. Any one of the above 3 methods will reveal exactly what error occurred. Regardless of where you found it, the error message will include the following in this order (each separated by a colon ':') Date and time that the error occurred Current user when the error occurred URL of the page that was being viewed Detailed error message with PHP file, line number and sometimes a debug backtrace Most often, examining the information above will make it clear what the problem is. However, if it doesn't, post your error message here (being careful not to reveal any personal server details). Or if you prefer, private-message it to me or email it to me at http://ryancramer.com/contact/. Hope this helps a few people. I noticed repeat instances of "Unable to complete this request due to an error" in our Google Analytics, so it's clear some are experiencing this message and arriving at our site. I wanted to make sure we had a proper answer.1 point
-
To get access to any other database table within ProcessWire you could use my module Fieldtype SelectExtOption http://modules.processwire.com/modules/fieldtype-select-ext-option/1 point
-
I didn't see you code $filter = ""; if($input->urlSegment1 == "femmes") { $filter = ", civilite=mademoiselle|madame"; } if($input->urlSegment1 == "hommes") { $filter = ", civilite=monsieur"; } // you just add $filter to your selector // $filter is empty unless you have an url segment $children = $page->children("limit=12 {$filter}"); $pagination = $children->renderPager(); foreach($children as $child) { $thumb = $child->joindre_au_maximum_4_photos->first()->size(200,200); echo "<div><a href='$child->url'>"; echo "<img src='$thumb->url' width='$thumb->width' height='$thumb->height' /></a>"; echo "<p><strong><em>$child->votre_pseudo</em></strong><br />$child->pays</p></div>"; } I used $results just as an example, in your code you extend your default selector with some extra checks based on the url segment. If you have multiple fields you need to filter by you could use two url segments like this $filter = ""; if($input->urlSegment1 && $input->urlSegment2) { // first segment is the field name // second segment is the value // so the url might be like /fr/annuaire-des-books/mannequins-modeles/civilite/madame // or like /fr/annuaire-des-books/mannequins-modeles/other_field/some_value $filter = ", $input->urlSegment1=$input->urlSegment2"; } // you just add $filter to your selector // $filter is empty unless you have an url segment $children = $page->children("limit=12 {$filter}"); $pagination = $children->renderPager(); foreach($children as $child) { $thumb = $child->joindre_au_maximum_4_photos->first()->size(200,200); echo "<div><a href='$child->url'>"; echo "<img src='$thumb->url' width='$thumb->width' height='$thumb->height' /></a>"; echo "<p><strong><em>$child->votre_pseudo</em></strong><br />$child->pays</p></div>"; } Of course you can check for specific values of the segments and build the selector accordingly.1 point
-
This line helped me: $m->save('gruppenzuordnung'); Tank you, guys!1 point
-
1 point
-
Which one is the repeater? gruppe? You are trying to remove the repeater form its own item, as far as I can see. $m is a Page object, not a (Repeater)PageArray if I'm correct: $m->gruppenzuordnung->remove($delGruppe); So incidentally it is probably the image this line removes because of a matching key of the image: http://kongondo.github.io/ProcessWireAPIGen/devns/source-class-ProcessWire.WireData.html#333-351 You need to work on the RepeaterPageArray: http://kongondo.github.io/ProcessWireAPIGen/devns/class-ProcessWire.PageArray.html#_remove like this: $delGruppe->remove($m); // or $gruppe->remove($m); // depending on what you want to achieve1 point
-
I'm not sure what's causing your issue, but does it make a difference if you save only the repeater field rather than the whole page? $m->save('gruppenzuordnung');1 point
-
Yeah, sorry, that should probably have been my first suggestion When you recreate it, make sure you choose "ProcessModule" as the Process on the Content tab under title. Actually I wonder if the better way might be to run these three commands to make sure page ID is correct - I am not sure if that matters or not. INSERT INTO `field_title` (`pages_id`, `data`) VALUES('21', 'Modules'); INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `sort`) VALUES('21', '2', '2', 'module', '21', NOW(), '41', NOW(), '2', '2'); INSERT INTO `field_process` (`pages_id`, `data`) VALUES('21', '50');1 point
-
Thanks a lot for this amazing tutorial. May I ask you few questions? How do you get the json info for image field type, either single or array (possibly included responsive file sizes)? How would you exclude specific fields, is it better to include each one manually or exclude the one you don't want? Do you usually add query parameters as well, for example if you want to output children of a specific page ID in the same call? Do you know if there's any example showing a tipical nested structure like mysite/api/pages/categoryList/{catID}/articleList/{articleID}/galleryList/{galleryID} etc.. For example, in my single page app I need to output all pages of the website, both root and nested pages. Would you still use the classes used in this tutorial or this library that seems more up-to-date? https://github.com/NinjasCL/pw-rest1 point
-
You can use url segments (need to enable them for each template you need to have them). Then the Femmes link will be /fr/annuaire-des-books/mannequins-modeles/femmes and in the template file for directory_listing_mm you do this $filter = ""; if($input->urlSegment1 == "femmes") { $filter = ", civilite=value_representing_femmes"; } if($input->urlSegment1 == "hommes") { $filter = ", civilite=value_representing_hommes"; } $results = $pages->find("template=directory_listing_mm {$filter}"); /fr/annuaire-des-books/mannequins-modeles/femmes /fr/annuaire-des-books/mannequins-modeles/hommes the red part will be $input->urlSegment1, so if $input->urlSegment1 is femmes you change the selector to search for pages where your civilite is femmes. You have quite a few filters so this might not be the best solution. I say you change the civilite to page fields the the code becomes: $filter = ""; if($input->urlSegment1) { $filter = ", civilite={$input->urlSegment1}"; } $results = $pages->find("template=directory_listing_mm {$filter}"); If changing fields to page field is an option i can explain further.1 point
-
Hi @Peter Knight If they are served up in the PW site, then the 404 event won't take place, so Jumplinks won't even start the process at all. mod_rewrite/mod_alias would be the most efficient in this simple scenario.1 point
-
They really are the same thing: https://processwire.com/api/ref/wire-array/append/ https://github.com/processwire/processwire/blob/36984e4a057268b7a45b848e1b3b6ee757583459/wire/core/WireArray.php#L956-L9591 point
-
Got it, thanks. And your interpretation of those movies clearly goes much deeper than mine1 point
-
What I decided to do was leave the RTE fields alone. By default that allows subdomains to link to any page in the tree. In our case, this is doesn't present any permissions issues since we don't have editors who need to be "jailed" to their subdomain. I decided to use a textformatter to rewrite the links on output. It's pasted below in the event anyone else needs something similar. <?php class TextformatterMultisiteLinks extends Textformatter { public static function getModuleInfo() { return array( 'title' => 'Multisite Link Formatter', 'version' => 100, 'summary' => "Converts links in RTE fields for multisite setups.", 'author' => 'Tom Reno (Renobird)', 'requires' => "Multisite" ); } public function format(&$str) { if (wire("config")->MultisiteDomains && wire("config")->httpHosts){ $rootSite = wire("config")->httpHosts[0]; // make sure primary domain is first in config.php $document = new DOMDocument(); $document->loadHTML($str); $xpath = new DOMXpath($document); foreach ($xpath->query('//a[@href]') as $a) { $href = $a->getAttribute('href'); // Check $href against our domain roots. Rewrite accordingly. foreach (wire("config")->MultisiteDomains as $key => $domain) { $domainPageName = $domain['root']; if (strpos($href, $domainPageName) !== false) { $url = str_replace($domainPageName, "http://$key", $href); $a->setAttribute('href', $url); break 2; // this link was rewritten, so move on to the next. } } // append full URL if not viewing the $rootSite if ($_SERVER['HTTP_HOST'] != $rootSite){ if ((substr($href, 0, 1) == '/')) { $a->setAttribute('href', "http://$rootSite" . $href); } } } $str = $document->saveHTML(); } } }1 point
-
Hi everyone, One thing that I am guilty of (and I am not the only one ) when sharing links to a line (or lines) of code in a Github repo, is sharing a link to the latest version of the file, rather than a link to a specific blob that won't ever change. In fact there are lots of places in the PW documentation/blog that now have outdated links because of this. I have known about this shortcut for a while, but keep forgetting it, so hopefully this will engrain it a little better for me. After you click a line to share (or multiple lines by clicking the first line and then shift+clicking the last line), simply hit the "y" key and the link in the browser address bar will change to one for the current blob, which will always remain correct. Here's some more info if you are interested: https://blog.mariusschulz.com/2015/07/25/sharing-line-highlights-in-github-files If you see me (or anyone else) sharing non-permanent links, please remind them of this1 point
-
This sounds like an analytics / marketing type of problem, so maybe researching these types of solutions might help. You will need some way of storing custom user actions - like “a user watches a video” or a “a user downloads a file” probably via javascript. These user actions could be stored in an analytic solution which are well designed to handle this type of data. You could then access the user analytics data via their api, and setup rules in your own code to show your survey based on certain criteria. I’ve not researched this space for while, but from my previous experience Keen.io is a nice hosted solution for doing any type of custom event tracking, and also a good way to learn about these type of analytics systems. The documentation and api is easy to get started with and they have a very reasonable free plan (15 million events per month), which will probably cover most situations. https://keen.io/guides/data-modeling-guide/ https://keen.io/docs/data-collection/ Piwik is a stable open source hosted solution for analytics, and they also have a user events tracking feature. I don’t know how well it fares against some of the commercial solutions for tracking user events (like Mixpanel etc.) but is definitely worth a look http://piwik.org/docs/event-tracking/ I was recently looking at an open source marketing application called Mautic and they classify this type of thing as a "Campaign". Maybe you can integrate Mautic into your own app or learn from their implementation how they solved a similar problem… https://mautic.org/docs/en/campaigns/campaign_builder.html1 point
-
Essentially we're doing this in the array_reduce() [Page, Page, Page] => [ 'key' => [Page, Page], 'key2' => [Page] ] So just count how many items the resulting array has. If 1 show only the key, otherwise do the array_map(). I think the argument about more structure (compared to loops) is far more valid than the "shorter is better" one. I'm using Laravel's collection package via https://github.com/tightenco/collect with some additional hooked in functions and especially such transformation tasks are so much clearer to create as well as to read.1 point
-
I can really recommend http://adamwathan.me/refactoring-to-collections/ if you're curious (has a free sample as well). Brought a lot of light into all those functions and their usefulness.1 point
-
If there's nothing in the errors.txt the error does happen before pw's error handling kicks in. So probably some error either in config.php (simply checked by adding die() in various places and see if the error persists) or with any autoloading modules or in init.php. Also check your php/mysql/apache versions for any obvious mismatches.1 point
-
@ryan: You should add this to the Troubleshooting guide1 point
-
Did you try another index.php ? Did you check on any database commands in your local database that should not be there anymore in the server database ? E.g. some CPanels don't like database commands created in a local environment.1 point
-
str_replace can take an array, so just add any expected opening tags to the 'tags' array, rather than just the <p>, thus $words = 50; $tags = array('<p>','<h2>','<ol>'); $excerpt = str_replace($tags,' ',$page->get("body")); $excerpt = trim(strip_tags($excerpt)); $excerpt = implode(' ', array_splice(explode(' ', $excerpt), 0, $words - 1)) . '…'; echo '<p>'.$excerpt.'</p>'; The PW page I copied my original from was just working with text I had input, so I knew to only expect <p> tags1 point