Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 09/25/2025 in Posts

  1. We've long wanted a way to utilise a CDN to offload images/files/videos from ProcessWire sites without losing all the native greatness of ProcessWire image and file field types. Having read various discussions on here about ways to approach this that never seemed to reach conclusion, I've thrown myself into creating a module that allows offloading of files to Bunny.net CDN as we need a solution for a specific project. I think this would be easily adaptable to any S3 compatible CDN but I've only tested on Bunny. ⚠️ This is still very beta! Use at your own risk! I've been conducting basic testing and so far, so good but there's bound to be holes or things that others may suggest better ways of doing. But I'm now at a stage where the insight/experience of the PW community might add value to the project - so I'm sharing now! Full disclosure: Once past the initial project scaffolding I've been using AI/careful prompting to write some of the code so that I can arrive at a prototype as quickly as possible. This seems to have worked well, although some of the code looks a little verbose and could probably be refactored later on. Also not security/pen-tested yet. https://github.com/warp-design/WireBunnyCdn/ Features: Automatically uploads images to Bunny storage on page save, including all variants and mirrors assets folder structure for simple merging back to local at a later date if needed. Automatically cleanses deleted files (or files from deleted pages) from your CDN. Option to mirror files to CDN or delete local copies (this is the main aim for me, otherwise we could just use ProCache). Handles (basic currently) image sizing - either using standard ProcessWire `$image->size(X,X)` methods or by implementing Bunny Optimizer for sizing using URL params. Rewrites image paths via CDN so that you can use standard `$page->imageField->url` calls with the output being a Bunny path rather than local PW path. Also handles the image previews in admin view. Roadmap: Support for video uploads (with optional separate CDN endpoint for Bunny Stream buckets). Support for front-end video output to templates using Bunny stream players/optimisation etc. Implement chunked/background uploads for large files. Support for other size() method options, like cropping etc and mapping to Bunny Optimizer equivalents. Anyway - look forward to hearing any advice/feedback/bug reports... I'm sure there's many!
    6 points
  2. Thank you - this is really useful to know. If I’m understanding it correctly I think we’re ok in this instance as we’re only pushing content to Bunny via API rather than the CDN hitting the site itself. It’s definitely early days though and I think there will be lots of edge cases to look out for. We’ve thrown this together for a product MVP that will have some ad traffic sent to it, so there should be plenty of data to look at. The main motivation at the moment is to minimise the data storage burden vs local storage on the VPS where the rest of the site runs rather than performance optimisation. But caching/compression etc will be interesting next steps.
    1 point
  3. Thanks! I'll see what our host (OVH) says and keep you posted.
    1 point
  4. Awesome. I use BunnyCDN as well (previously KeyCDN) configured with ProCache, mainly because they publish the list of IPs they use if their edge-servers need to pull assets (and then serve) from your website. While this post isn't directly related to your module, it matters if you are using WireRequestBlocker for the reasons I explained here (post only viewable if you have access). Long story short, if a CDN makes a request to a URL on your site that matches a blocking rule in WireRequestBlocker (rare, but it's bound to happen by accident) then the IP of that particular BunnyCDN edge-server will get blocked. Then if a visitor on your site is being sent assets from that edge server, then it will error because the CDN was never able to obtain it due to the edge-server being blocked. This is why the site might look fine in California, but broken in New York for example, as the user is being sent assets from different edge-servers. To prevent this from happening, I have a cronjob set up (runs every 24 hours) to grab the list of BunnyCDN edge-server IPs and I insert it into WireRequestBlockers "IP addresses to whitelist" field. This is a function that can do what I described above: function bunnycdnWhitelistIps() { if(!wire('modules')->isInstalled('WireRequestBlocker')) return false; // Fetch BunnyCDN edge server list $url = 'https://bunnycdn.com/api/system/edgeserverlist'; // Use cURL to fetch the content $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($httpCode !== 200 || $response === false) { throw new WireException("Error fetching data from BunnyCDN API. HTTP Code: $httpCode\n"); } // Parse the JSON response $data = json_decode($response, true); if(json_last_error() !== JSON_ERROR_NONE) { throw new WireException("Invalid IPs."); } // Extract IP addresses into an array $ipAddresses = []; if(isset($data) && is_array($data)) { foreach($data as $ip) { if(filter_var($ip, FILTER_VALIDATE_IP)) { $ipAddresses[] = $ip; } } } // Remove duplicates and sort $ipAddresses = array_unique($ipAddresses); sort($ipAddresses); $data = wire('modules')->getModuleConfigData('WireRequestBlocker'); $data['goodIps'] = implode("\n", $ipAddresses); wire('modules')->saveModuleConfigData('WireRequestBlocker', $data); }
    1 point
  5. At some point this happened to me as well. However only if I browsed the website while I was logged in as admin in the same browser. So for what it's worth try with a different browser, log out as admin or even use a VPN to test.
    1 point
  6. I'm monitoring the ProcessWire logs to find eventual PHP errors that needed a change by upgrading ProcessWire and PHP. Some old PHP was not recognised anymore, but most of that is done now. Most pages get some content (images) from external websites, usually just one per 'person'/page, and several on queried pages, like ' $features = $pages->find("parent=/events/|/the-eyes/, bfd_day.name=$todayday, bfd_month.name=$todaymonth, sort=bfd_year"); ', giving a list of events for 'today' with an average of 30-40 features. It all went well for over 10 years and without any notable changes the problem occurred and got worse. The problem affects the whole website, same when I call the home page or any other page including admin pages, even the Matomo statistics sometimes give a 500 error. From the hosting error logs I get many '[Wed Sep 24 00:08:51 2025] [X-OVHRequest-Id: 1d8b279cbdb0de4b3c5dad4c408a1f98] [error] [client 34.174.60.244:0] [host www.birthfactdeathcalendar.net] AH10141: FastCGI: comm with server "/homez.863/birthfac/www/index.php" aborted: idle timeout (160 sec)' errors. I filed an error report with them and wait for reply. There are no hooks used anywhere, site modules are up to date and give no errors at all in the logs. With an average of 30 visitors a day it's not extremely busy either. There are however over 33.000 pages.
    1 point
  7. I've built this over and over in several of my modules (RockDevTools for Livereload, RockCalendar for creating events). Always a lot of work. Always a lot of boilerplate code. Always a lot of issues to fix. I think it's time to bring SSE (Server Sent Events) to everybody. Simple example (Empty Trash): Client-Side: // create stream const stream = ProcessWire.Sse.stream( 'ssedemo-empty-trash', (event) => { const textarea = document.querySelector('textarea[name="empty-trash-status"]'); try { let json = JSON.parse(event.data); stream.prepend(textarea, json.message, 100); } catch (error) { stream.prepend(textarea, event.data); } } ); // update progress bar const progressBar = document.querySelector('#empty-trash-progress'); stream.onProgress((progress) => { progressBar.value = progress.percent; }); // click on start button document.querySelector('#empty-trash').addEventListener( 'click', (e) => { e.preventDefault(); stream.start(); }); // click on stop button document.querySelector('#stop-empty-trash').addEventListener( 'click', (e) => { e.preventDefault(); stream.stop(); }); Server-Side: public function __construct() { parent::__construct(); /** @var Sse $sse */ $sse = wire()->modules->get('Sse'); $sse->addStream('ssedemo-empty-trash', $this, 'emptyTrash'); } public function emptyTrash(Sse $sse, Iterator $iterator) { $user = wire()->user; if (!$user->isSuperuser()) die('no access'); $selector = [ 'parent' => wire()->config->trashPageID, 'include' => 'all', ]; // first run if ($iterator->num === 1) { $iterator->max = wire()->pages->count($selector); } // trash one page at a time $p = wire()->pages->get($selector); if ($p->id) $p->delete(true); else { $sse->send('No more pages to delete'); return $sse->stop(); } // send message and progress info $sse->send( $iterator->num . '/' . $iterator->max . ': deleted ' . $p->name, $iterator ); // no sleep to instantly run next iteration $sse->sleep = 0; } Code + Readme: https://github.com/baumrock/SSE/tree/dev What do you think? Would be nice if you could test it in your environments and let me know if you find any issues!
    1 point
  8. I'm working on RockCalendar which will be released soon if everything goes smoothly 😎 I think it should be stable enough next month. If you want to get notified you can subscribe to the Rock Monthly Newsletter. The module uses https://fullcalendar.io/ for the calendar interface and when clicking an event it will open the page editor in a pw modal: Opening the modal was a bit of a challenge, because PW unfortunately does not offer a good JS API to open modals. It only offers markup-based options to control the behaviour of the modal - at least as far as I could find out. This is a problem, because all events in the calendar get loaded by fullcalendar and I only have the eventClick() callback where I need to fire my action. My solution is to create a fake element in the dom and trigger a click on that element: calendar.on("eventClick", (info) => { // create a fake link element in body let link = document.createElement("a"); let $link = $(link); $link.attr( "href", ProcessWire.config.urls.admin + "page/edit/?id=" + info.event.id ); $link.addClass("pw-modal"); $link.attr("data-autoclose", ""); $link.attr("data-buttons", "button.ui-button[type=submit]"); $link.on("click", pwModalOpenEvent); $(document).on("pw-modal-closed", this.refresh.bind(this)); $link.click(); $link.remove(); }); Maybe that helps anyone else looking for a solution to a similar problem. If anybody knows a better way how to do it, please let me know! PS: If you have input (feature requests) for the development of RockCalendar please let me know: https://processwire.com/talk/topic/30355-request-for-input-what-features-should-a-pw-calendar-module-have/
    1 point
  9. Needed this today while working on RockGrid. Realised that the version above attaches an event listener for every created link but doesn't remove it later. So I improved this by listening on the "pw-modal-closed" event on the $link instead of the $(document). This didn't work at first, but I found out, that the culprit was that I instantly removed the $link after it has been clicked, so obviously there is no element any more to listen for the event. After moving the $link.remove() in the modal closed callback it worked as expected. Here is the updated function that I use in RockCalendar: const openInModal = (href, options = {}) => { // merge options with defaults const defaults = { calendar: false, autoclose: true, buttons: "button.ui-button[type=submit]", }; const opts = { ...defaults, ...options }; // create a fake link element in body let link = document.createElement("a"); let $link = $(link); $link.attr("href", href); $link.addClass("pw-modal rc-link-remove"); if (opts.autoclose) $link.attr("data-autoclose", ""); if (opts.buttons) $link.attr("data-buttons", opts.buttons); $link.on("click", pwModalOpenEvent); $link.on("pw-modal-closed", () => { if (opts.calendar) opts.calendar.refresh(); $link.remove(); }); $link.click(); }; A different approach is to add a second button (save + close) to the modal:
    1 point
×
×
  • Create New...