Leaderboard
Popular Content
Showing content with the highest reputation on 09/05/2024 in all areas
-
Introduction Automated PDF production is a quite common task for ProcessWire developers. PW does not offer core tools for that. There are some external modules that we can utilize. In this showcase I would like to share my considerations for the tool of choice and my experiences working with it. The task at hand PDF production for product pages on an existing ProcessWire installation acniti.com is a Japan based company that specializes in nanotechnology solutions for gases in water. They have developed technologies to create nanobubbles in water, which can change the properties of water and improve dissolved gas levels through super saturation. Their founder and CEO has chosen ProcessWire for his website and has developed the site himself. He tasked me to add functionality for creation of downloadable PDFs from available product data. The client was forwarded to me from Bernhard because of time constraints on his side. I really appreciate that. The core requirements in detail: Produce separate PDF files for multiple languages, including LTR languages Design a template: cover page, images page, content pages, specs pages Cache PDFs Minimal impact on existing template code Tool choice: RockPdf module I had done some PDF generation before, using mPDF library directly. While there are other options available for ProcessWire (Pages2PDF, MakePDF) I decided to use Bernhard's RockPdf module since it seems the most feature rich and well maintained one. It is also based on the quite popular mPDF library. Reasons for choosing RockPdf Auto-reloading of PDFs in concert with RockFrontend This can save a lot of time (and strain on the F5 key), since the PDF is automatically reloaded every time a code change is made. All it requires to achieve that is one line of code: $pdf->save(__DIR__ . "/tmp/{$filename}.pdf", preview: true); Easy font loading I needed to load different fonts depending on the content language. Here is, how I did that RockPdf CSS utility classes They really came in handy, since they are specifically targeted at mPDF, using pt units. I could easily set some base colors with $pdf->setLessVars([ 'col-muted' => '#dddddd', 'col-primary' => '#0b54a2', ]); The utility classes that I mostly used are those for widths, margins and paddings. They are quite handy for setting up the layout of the PDF. Easy file saving to PW field For caching the created PDFs I utilized a RockPdf convenience method to save the files to a field on the product page $pagefile = $pdf->saveToField( page: $page, field: self::PDF_FIELD, filename: $filename, append: true, ); Implementation Modular approach for minimal impact on existing code I created two modules: Main module ProductPdf: non-autoload, holds all logic/templates for generating PDFs and required markup (download button) Module ProductPdfHooks: autoload, hooks: Page(template=product)::render displays PDF for download Pages::saved(template=product|product-type) creates PDFs in all languages and saves them to field for caching Re-usage of existing template logic There was quite a lot of logic (some of it rather complex) already available in the project that I wanted to utilize. Mainly for specs table creation. I had to do some minimal refactoring of original code. Then I was able to include that logic into my module without having to duplicate it. Benefits of this approach: Minimal impact on existing code Easier to maintain Challenges Limitations of mPDF library mPDF is not good at modern CSS features. It is quite limited in terms of CSS support. I had to do some workarounds to make it work with the layout I needed. Different approach to styling Although RockPdf's utility classes helped a lot, I still had to do some inline styling. Display of complex tables Display of tables in particular was a pain point since mPDF does a lot of automatic adjustments to column widths and distribution that I needed to disable in order to get the desired results: // Ensures that tables retain their original proportions $mpdf->keep_table_proportions = true; // And adding autosize="1" attribute to tables. Page headers, footers, margins and page breaks The RockPdf module docs have some great examples for setting up headers and footers, margins and page breaks. I used those to set up the layout of the PDF without having to read too much into the mPDF docs. Minimal impact on exisiting code base This was overcome by the modular approach I described earlier and it worked out really nice. The only addition to the original product template file for rendering the download button, was calling a static method from my module: <?= ProductPdf::renderDownloadbutton($page) ?> That button requests the page URL with a query parameter. The display of the PDF for download is handled through a Page::render hook PHP DOM manipulation of existing markup necessary Since I reused existing logic for constructing specs tables, I needed to add some inline styles and change some URLs on the fly. I used native PHP DOMDocument for that. There is a feature in the RockFrontend module that offers DOM manipulation tools with a nice API. I would have loved to use those but at the the time of working on this project, I just was not aware of their existence. The result Product pages on acniti.com now have a download button that allows the user to download the PDF of the product page in their language. See it live here The PDF is loaded from the cache field on the page, which is updated every time a product is edited and saved. If no cache file exists, the PDF is created on-the-fly and cached for future use. It is presented to the user in a new browser tab for viewing and downloading. The PDFs feature a clean layout / design which corresponds to the acniti branding. Cover page: Content pages: Specs table: Feedback from the client The client has a lot of experience with ProcessWire which one can see from looking at their website at acniti.com. He gave me great feedback on the project: Erik van Berkum, acniti LLC Lessons Learned and conclusion PDF creation in PHP is still not an easy task. The most popular library, mPDF, has some restrictions related to CSS that can make it tedious to work with. Especially when it comes to more complex layouts and tables. Using the RockPdf module for the task was a great choice. Its API is very well designed, offers a lot of conveniences and is well documented. Bernhard responded quickly to my questions and provided great support. In conclusion, the ProcessWire ecosystem offers great tooling for PDF creation that makes life for developers more enjoyable :-) Future considerations Would I use this approach again? Well, that depends mainly on the requirements. For this task the chosen tooling worked very well and I am happy with my choice. But for more complex layouts/designs that require modern CSS features, I would prefer rendering PDFs through Chromium browser using puppeteer or a self-hosted gotenberg service.8 points
-
So far, I've been working on the locked-down part of the site that I am using Unpoly for (so I cannot showcase that), but this will change in the near future. In the meantime, here are some great examples that other developers using Unpoly have started to share with each other: https://github.com/unpoly/unpoly/discussions/6663 points
-
You could also try Lister Selector: https://processwire.com/modules/process-lister-selector/3 points
-
To test selectors, I use TracyDebugger's console panel. It is useful to run a code snippet and see what it's output is. You can use it to test your selector, e.g.: d($pages->find('selector...')); Here is some more info about the console panel: https://adrianbj.github.io/TracyDebugger/#/debug-bar?id=console3 points
-
Hi, $input->get('pageId','int'); is casting the value to int, so null becomes zero. This is the equivalent of $sanitizer->int(null);2 points
-
2 points
-
2 points
-
I recently discovered Pinkary.com which I would describe as a Twitter clone, built by one of the Laravel team members with all the latest and greatest of Laravel and its ecosystem (the project is open-source). Right now it's got about 1000 members after being launched earlier this year and it's almost all web developers, which reminds me of the early Twitter days. I don't get excited about social media or microblogging much, but having a concentrated community of like-minded folks is intriguing and a place to find interesting things going on and nuggets of information, without all the noise, bots and other nonsense you'd see on Twitter/X. I think it's worth a join.1 point
-
This week we introduce a new module named Custom Fields. This module provides a way to rapidly build out ProcessWire fields that contain any number of subfields/properties within them. No matter how simple or complex your needs are, Custom Fields makes your job faster and easier. Not only does this post introduce Custom Fields, but also documents how to use them and includes numerous examplesโ https://processwire.com/blog/posts/custom-fields-module/1 point
-
First milestone reached!!! ๐๐ Yesterday I spent the whole day with refactoring the JS of the rrule gui and I added some more options to cover hopefully 99.9 of all necessary scenarios. The gui now has a toggle to switch between simple and advanced mode to keep things as clean as possible and not overwhelm the users for simple rrules like "every week for 4 times". At the moment 80 hours have gone into this module (excluding the daterange input, which was already done some time ago). ๐ช๐คฏ Here my answers to the open questions: I don't think there is an ideal solution to this problem. I even found a bug in google calendar when comparing and playing around!!! I decided against a fake "never" option, as it does something different than what the UI shows. So in my case if the user does not enter an end date or a max number of events it get's limited to 100. This limit is configurable in the module's settings, though ๐ I've also added the dates of the first and last event beside the resulting rrule to make it clear what is going to happen. Additionally the user will get a table of all events that will be created. Sometimes - but not always! rrules are tricky ๐ - the first occurrence is the main event itself. In that case the table shows a hopefully clear indicator: Yes and no, I'm using a mix to somehow get the best of both worlds. I'm creating real pages for the events, but I'm only populating the date field. All other fields (like shown with the title in the video) will be inherited from the main event at runtime. This makes it possible to have complex event descriptions (like one built with RockPageBuilder) with images etc. and when creating 1000 recurring events it will still only consume the space of one event + 1000 date inputs (but not 1000 copies of images). I hope the video answers that? ๐1 point
-
Thanx for the answer, @BrendonKoz. I guess they want only one of those... I've read about the contradiction you mentioned but do not know the answer myself yet.1 point
-
As @teppo mentioned in the issue this is possible to do this and I think this is the most robust way of writing code for page reference fields that work consistently and are not going to break if someone changes field formatting: foreach($page->get('my_pageref_field[]') as $p) ... if($page->get('my_pageref_field.first')) ...1 point
-
yeeees! me, me, me, me, MEEEEE ๐ please do let us know! as an contao-webmaster i solve this by using custom classes an editor is able to choose from per section - looks a little like your approach1 point
-
So is this a proposed solution as a template for module developers to implement in any JS-enabled modules, is it for PW to implement in its JS-powered backend implementations, or even something else entirely...? I'm just wondering how or where the integration of this would take place. I can see the benefit to this being offered, whether or not I can immediately see a need for myself at this point in time.1 point
-
@Ivan Gretsky I wish I had answers to your questions, but I'm mostly replying here out of curiosity for myself: I'm a little confused here. Is the first item that they want you to remove the Domain attribute, or they want it added? If they want it (Domain attribute) added, according to MDN, the "__Host- prefix [...] must not have a domain specified", though in your linked example, a domain is specified. That definitely confuses me. (Maybe MDN is wrong?) As for how to modify those values in PW, I don't have an answer there. ๐1 point
-
I still follow soma on Twitch. He occasionally has (digital) painting sessions. ๐ Signed up for Pinkary. Hopefully they'll offer an API like the original Twitter days to build out supporting apps. Interestingly, Pinkary runs completely on SQLite.1 point
-
If you don't like to use zero as an "empty" value, you can ask for a default value: $input->get('pageId', 'int', -1); But zero is fine since there's no zero ID or zero lang and we can check value easily. $pageId = $input->get('pageId','int'); if ($pageId) ...1 point
-
Thanks! Now I understand what was happening. Removing the "int" argument let's me check via "isset" if any of those parameters are passed.1 point
-
Thanks to @ryanthe pack is there now https://processwire.com/modules/brazilian-portuguese-pt-br-language-pack/ !1 point
-
1 point
-
I can see how ProcessWire is not only a great platform to use, but also a great learning tool and a springboard to help jump above your head. Many moved to more enterprisy dev and more appy tools. New active members come and bring some fresh thoughts. All is good ๐1 point
-
Good point. I think you could hook BEFORE Pages::clone and do exactly that instead of instructing the users to so manually. Note that you need the $event->replace = true; flag so that the original clone process does not kick in after your new page has been created. Something like this (untested): <?php $wire->addHookBefore('Pages::clone', function($event) { $page = $event->arguments('page'); if($page->template != 'your-template') return; $p = new Page(); $p->template = $page->template; $p->parent = $page->parent; $p->title = $page->title; $p->save(); $event->replace = true; // should not be necessary but just to be sure wire()->session->redirect($p->editUrl()); }); Of course that would mean that any clone would wipe all data. But you could add a checkbox to that page that disables the hook and therefore would work as a normal clone.1 point
-
In your site/config.php file, ensure that the $config->httpHosts is properly updated with only the domain you're currently using, which is plant-directory.something.edu. Since you've switched addresses, you don't need the old domain unless you're maintaining multiple environments (like a dev or staging environment). You can try this configuration: $config->httpHosts = array('plant-directory.something.edu'); If you have a development or staging site (e.g., dev.plants.something.edu), you can include both: $config->httpHosts = array('dev.plants.something.edu', 'plant-directory.something.edu'); ProcessWire sometimes caches configuration files. You can clear the cache by doing the following: Log in to your ProcessWire admin panel and navigate to Modules > Core > Clear compiled files (if you can log in).1 point
-
If the file vendor/autoload.php exists, it will be called automatically by ProcessWire's index.php. No need to call it manually.1 point
-
1 point
-
If you are concerned with file size like that then you can use CDN (https://unpoly.com/install/cdn) conditionally: load local files when CDN is not available. Unpoly also have advanced caching, so by using that properly your site can behave more like an application then a website by providing (almost) instant responses. (As a related side-note, I also agree with this certain forum user on this forum: https://forum.bootstrapstudio.io/t/bootstrap-css-and-cleanup/10578/2?u=szabesz) As for my own experiences, I have not yet implement the conditional usage of CDN but the e-commerce site I developed using Unpoly for its frontend JS magic is loved by customers, partially because of the easy to use user interface I crafted for them, and partly because of the speed of the site, even though it loads full Bootstrap 5, full jQuery and also Unpoly (plus my CSS and JS, of course). Sure, browser and Unpoly caching helps a lot! About half of the users use their mobile phones to place orders and they are equally satisfied. I just cannot showcase the site because currently it is for contracted customers only. There are plans to open it up for the average retail customers as well (on a different domain) so when that is in production, I will probably provide this forum with my very first showcase. (In 1 or 2 years... as I have loads of other projects to finish before I can start working on that.)1 point