Leaderboard
Popular Content
Showing content with the highest reputation on 03/01/2022 in all areas
-
Hello, I would really appreciate your comments on this blog post I am preparing for our site. =========== Creating a dynamic search with very little code in ProcessWire is easy. This search cannot compete with engines like Elasticsearch, Solr, and others. However, it is suitable for most "showcase" sites. Here is how we did it on the Spiria site using the small library htmx and its companion hyperscript. The goal The recipe Inclusion of libraries htmx and hyperscript (the latter is optional). A field of type textarea integrated to the page models that we want to index. A code for indexing the existing content in the file `ready.php` A search controller which we name here `api.php`. This controller will also be a page with the `api` template. A form placed in the pages requiring the search. Content indexing Before we can program, we need to index the content on which we want to apply our search. In my proof of concept, I have developed two strategies. This is probably overrated, because I am not sure of the gain in speed. Index for a single term search Indexing for a multiple term search To do this, we need to introduce two fields in each model where we want an indexation. The `search_text` field which will contain only one occurrence of each word on a page. The `search_text_long` field which will preserve all sentences without HTML tags. We place a hook in the `ready.php` page in this way: <?php namespace ProcessWire; pages()->addHookAfter("saveReady", function (HookEvent $event) { $p = $event->arguments[0]; switch ($p->template->name) { case "blog_article": $french = languages()->get('fr'); $english = languages()->get('default'); $txt_en = $p->page_content->getLanguageValue($english) . ' ' . $p->blog_summary->getLanguageValue($english); $txt_fr = $p->page_content->getLanguageValue($french) . ' ' . $p->blog_summary->getLanguageValue($french); $title_en = $p->title->getLanguageValue($english); $title_fr = $p->title->getLanguageValue($french); $resultEn = stripText($txt_en, $title_en); $resultFr = stripText($txt_fr, $title_fr); $p->setLanguageValue($english, "search_text", $resultEn[0]); $p->setLanguageValue($english, "search_text_long", $resultEn[1]); $p->setLanguageValue($french, "search_text", $resultFr[0]); $p->setLanguageValue($french, "search_text_long", $resultFr[1]); break; } }); And function stripText($t, $s) { $resultText = []; $t = strip_tags($t); $t .= " " . $s; $t = str_replace("\n", " ", $t); $t = str_replace(",", "", $t); $t = str_replace("“", "", $t); $t = str_replace("”", "", $t); $t = str_replace("'", "", $t); $t = str_replace("?", "", $t); $t = str_replace("!", "", $t); $t = str_replace(":", "", $t); $t = str_replace("«", "", $t); $t = str_replace("»", "", $t); $t = str_replace(",", "", $t); $t = str_replace(".", "", $t); $t = str_replace("l’", "", $t); $t = str_replace("d’", "", $t); $t = str_replace(" ", "", $t); $t = preg_replace('/\[\[.*\]\]/', '', $t); //$t = preg_replace('/\?|\[\[.*\]\]|“|”|«|»|\.|!|\ |l’|d’|s’/','',$t); $arrayText = explode(" ", $t); foreach ($arrayText as $item) { if (strlen(trim($item)) > 3 && !in_array($item, $resultText)) { $resultText[] = $item; } } return [implode(" ", $resultText), $t]; } If you have the ListerPro module, it becomes easy to save all the pages to be indexed in batch and any new page will then be indexed as it is created. The `stripText()` function aims at cleaning up the text as we want. Note that, in my example, I distinguish between French and English. This little algorithm is entirely perfectible! I have commented a shorter way to clean up the text, but at the expense of comprehension. As mentioned before, it is probably unnecessary to create two search fields. The most important thing would be to optimize the text as much as possible, as many small words are useless. The current code restricts to words longer than three characters, which is tricky in a computing context like our site where names like `C#`, `C++`, `PHP` compete with `the`, `for`, `not`, etc. That said, perhaps this optimization is superfluous in the context of a simple content and not and limited in number. So now see the process and the research code. The structure structure.svg This scheme is classical and needs few explanations. The `htmx` library allows a simple Ajax call. The form code01.svg The form has a `get` method that returns to a conventional search page when the user presses the `enter` key. A hidden field with the secret key generated on the fly enhances security. The third field is the `input` of the dynamic search. It has a `htmx` syntax. The first command, `hx-post`, indicates the method of sending the data to the API. Here, it is a `post`. `htmx` allows to handle events on any DOM element. So we could have several calls on different elements of a form. The second line indicates where the API response will be sent when received, which is `div#searchResult` below the form. The `hx-trigger` command describes the context of sending to the API. Here, when the user releases a key, with a delay of 200 ms between each reading of the event. The `hx-indicator` command is optional. It tells the user that something is happening. In the example, the `#indexsearch` image is displayed. This is automatically handled by htmx. We have the possibility to pass other parameters to the search with the `hx-vals` command. The given example is simplified. We send the search language. The last command comes from the `hyperscript` syntax. It indicates that if you click anywhere outside the search field, you will make the contents of `#found` disappear, which will be described below. It is clear from this example that no javascript is called, except the [htmx] and [hyperscript] libraries. It is worth visiting the web site of these two libraries to understand their philosophy and possibilities. The Search API The API resides in a normal ProcessWire page. Although it is published, it remains "hidden" from CMS searches. This page allows requests to be answered and the correct functions to be called. Several requests to the CMS can be gathered in this type of page. <?php namespace ProcessWire; $secretsearch = session()->get('secretToken'); $request = input()->post(); $lang = sanitizer()->text($request["lang"]); if (isset($request['CSRFTokenBlog'])) { if (hash_equals($secretsearch, $request['CSRFTokenBlog'])) { if (!empty($request["search"])) { echo page()->querySite(sanitizer()->text($request["search"]),$lang); } } else { echo __("A problem occurred. We are sorry of the inconvenience."); } } exit; In this case : We extract the secret token of the session, token that will have been created in the search form page. We then process everything that is in the `post` query. Note that this is a simplified example. We compare the token with the one received in the query. If all goes well, we call the SQL query. Our example uses a class residing in `site/classes/ApiPage.php`; it can therefore be called directly by `page()`. Any other strategy is valid. The following code represents the core of the process. <?php namespace ProcessWire; public function querySite($q, $l) { $this->search = ""; $this->lang = $l == 'en' ? 'default' : 'fr'; user()->setLanguage($this->lang); $whatQuery = explode(" ", $q); $this->count = count($whatQuery); if ($this->count > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text_long~|*= "' . $q . '",sort=-created'; } elseif (strlen($q) > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text*=' . $q . ',sort=-created'; } if ($this->search !== "") { $this->result = pages()->find($this->search); return $this->formatResult(); } return ""; } protected function formatResult() { $html = '<ul id="found">'; if (count($this->result) > 0) { foreach ($this->result as $result) { $html .= '<li><a href="' . $result->url . '">' . $result->title . '</a></li>'; } } else { $html .= __('Nothing found'); } $html .= '</ul></div>'; return $html; } The `formatResult()` function is simple to understand and it is here that we see the `ul#found` tag appear which, remember, is deleted by the _hyperscript_ line of the form. _="on click from elsewhere remove #found" Actually, I've commented out this line in the production code so far. Instead of clicking anywhere, it would be better to add a background behind the form when doing a search in order to focus the `click` event. But again, all strategies are good! In the current code, it is not necessary to add CSS to display the result. It is placed in an empty `#searchResult` tag, so it is invisible at first. As soon as it is filled by the search result, everything becomes accessible, the CSS being targeted on the `ul#found` list and not on its parent. Conclusion The purpose of this article was to demonstrate htmx and the possibilities that this library allows. There is already an excellent search module for ProcessWire, SearchEngine, which can coexist very well with the code described here.3 points
-
I was about to create a similar topic, but why do it if this one already exists))) Not so long ago I started donating to some ProcessWire modules' authors. And each 1st of the month I am looking at my Patreon receipt in my email with a kind of a pride. I think that this thing is my little contribution to the community I love so much and which gave so much to me for free. I do know that Ryan likes to receive his support with pro modules payment. But many modules really shouldn't be paid/pro as they are so essential to many (and so fun to build). But their authors still would be glad to receive some money to sustain their enthusiasm supporting them and creating new features. I am sure the appreciation itself is equally important, but what is an easier way to show it than ???))) The other thing is that I sometimes notice some great improvements that seem to happen to modules as soon as some support comes in. I am writing this to encourage everyone to support the PW ecosystem by donating to module authors. I will list the modules I know that clearly asked for donations. I am sure that is not an extensive list. But at least something to start with. Tracy Debugger Mystique and other modules by ukyo (please don't miss this one @Jonathan Lahijani))) Rock Migrations and a bunch of rock stuff by bernhard All the great stuff from teppo I really like the Patreon/OpenCollective/Github donations, but only 1 of 3 listed used them. Sooooo..... Start throwing money at these fine gentlemen)) Support yourself by making you favorite modules not go away. I wish we could bring back @tpr to support his AOS and wire shell by @marcus and @justb3a. Would they keep supporting their thing is they had some donations coming in? Who knows. And please feel free to list you other module authors that you know asked for support below. Edit 2022-10-27: added teppo's link2 points
-
Thanks Robin. I created this now: https://github.com/processwire/processwire-issues/issues/15362 points
-
Hi @Laegnur and welcome here. Your issue come from the fact that your translated strings are on the same line (I admit it could be tricky to spot on first instance if you are not used with ProcessWire). To understand better, please read this sentence : So to get it working, write the first line on two lines lol : <?php // i18n - copyright echo _x('All rights reserver', 'copyright'); ?> <!-- html markup - separator --> | <?php // i18n - developer echo _x('Developed by', 'developer'); ?> You can use of course the syntax you want, just keep in mind that the strings need to be on two lines. For more information about: https://processwire.com/docs/multi-language-support/code-i18n/2 points
-
I can reproduce the issue. Please open a GitHub issue so that Ryan can investigate. As you say, there seem to be two things needing attention: The description for the Tags inputfield on the Advanced tab of Edit Field should state the range of characters that are allowed in a tag (umlauts and other accented characters are not allowed). If a disallowed character is added via that Tags input it should be sanitized out to avoid breaking InputfieldTextTags. Disallowed characters are already sanitized out when adding a tag via Manage Tags > Add new tag.2 points
-
Hello. I'm almost a new user in Processwire. Now I'm adventuring myself into make a new website with this CMS, and adding multi-language support to it. I installed the site using the blank profile, and install later the language support modules. Configure 3 languages, and add fields, templates and pages with translated labels, all correct. But now, I have 5 strings across multiple template files that I need to translate. When I go to one of the languages, and build the phrase index it appears to find all the strings but when search for translatable files, it only find 4 string. The problematic string is the "_x('Developed by', 'developer')". What I'm doing wrong with this string?1 point
-
So... that "layouts within layouts" thing almost ended up in v 0.22, but I decided to hold it back. I don't actually think it solves the right issue, or that it makes enough sense as is. In other words I believe I may have been looking at this from the wrong direction. That, or I just need some extra information before going forward ? What I was going to add would've been a new $layouts API variable, which would've made it possible to embed second (named) layout within one layout. Something along these lines: <!-- layouts/default.php --> <head> <title>Hello World</title> </head> <body> <?= $layouts->render('single-column') ?> </body> <!-- layouts/single-column.php --> <main> <h1><?= $page->title ?></h1> <?= $page->body ?> </main> The only thing this really adds to current feature set is the ability to reuse layouts — which, to be honest, doesn't seem particularly useful, especially considering that the downside is that it adds a new API variable, new layer of complexity, etc. To me it doesn't seem too different from this, which is already doable: <!-- layouts/default.php --> <head> <title>Hello World</title> </head> <body> <?= $partials->render('sublayouts/single-column') ?> </body> <!-- partials/sublayouts/single-column.php --> <main> <h1><?= $page->title ?></h1> <?= $page->body ?> </main> With recent API updates, it's also quite straightforward to dynamically change the sublayout: <!-- layouts/default.php --> <body> <?= $partials->render('sublayouts/' . $view->sublayout ?: 'default') ?> </body> <!-- controllers/HomeController.php --> <?php // ... public function render() { // override sublayout for the home template $this->view->sublayout = 'home'; } // ... Unless I'm missing something, this seems like a pretty clean way to achieve simple sublayouts. Only difference is that they live under the "partials" directory, but after thinking this through that actually seems like a logical place: having them under "layouts" would feel a bit weird (these are different from the "main layouts" after all), while adding a whole new "sublayouts" directory at the root level would, at least to me, seem a bit unnecessary. (Although if partials get the ability to point to files in other / absolute directories, it will of course become possible to add such directory on a case by case basis.) Am I on the right tracks here, @Ivan Gretsky? ? Technically you can go as deep as you want with this type of structure, with relatively little code: if you need a third level of sub-sub-layouts, those would just need to be rendered in the sublayout partial(s). I guess it's, in a way, the opposite of what Twig/Blade/etc. do: instead of the sublayout or view defining that they extend some other file (layout or sublayout), you'd output the child in the parent file. Anyway, let me know what you think. I'll be happy to work on this further, but I've realized that since this isn't really something I personally use, it's way too easy to forget what the actual problem I'm solving was... ?1 point
-
Cross-posting here that $partials->get('path/to/partial') is now supported in Wireframe 0.22.0, along with new method $partials->render('path/to/partial'). More details in the docs: https://wireframe-framework.com/docs/view/partials/.1 point
-
pictures in basic pages only shown when logged in In the frontend all I can find is this empty link: <p><a href="/site/assets/files/4121/log-dls_hepbandc_1300px.jpg"></a></p> So... How do you add images to your content field? Maybe there is an issue with it. Or Maybe even errors when uploading another image. Can you provide a screenshot from your "Inhalt" tab, please. hidden/unpublished basic pages shown in navigation The website looks quite custom made so it's probably a query for that menu that has something like include=all, include=hidden or similar in it and therefore shows more pages than necessary. Or at least that's my guess for now. So in case you have access to the source code of that site you could find the part. Maybe it's even a module that manages all those menus. Just looking at the screenshot of your backend I assume the site is already running for quite some time now and is a bit older. Which leads to the question: Did this happen before? Were there any other issues? Was something changed in the last days, weeks, or months? Right now it's all just a wild guess. And btw... welcome the ProcessWire Forums!1 point
-
1 point
-
Thanks, it's running on batch of 5000 records and I will see how much time it take. I guess I still have the benefit of not crashing the server while getting all the candidate pages. $end = strtotime( "2021-02-27 23:59:59"); // loop $database->beginTransaction(); $result = $rockfinder ->find("template=receipt, numChildren=0, created<$end, limit=5000") ->each(function($row, $finder) use ($database) { $finder->pages->delete($finder->pages->get($row->id)); }); $database->commit();1 point
-
RockFinder does not bring you any benefit here as it relies on the native findIDs. Have a look here:1 point
-
Hello Robin, Thanks. The search can be found here: https://www.spiria.com/en/blog.1 point
-
Just stumbled into this package and reminded me of this discussion, thought it might be worth dumping it here, maybe the goals it means to solve resonate with the ideas exchanged here: https://fractal.thephpleague.com/1 point