Jump to content

Recently Updated Topics

Showing topics posted in for the last 7 days.

This stream auto-updates

  1. Past hour
  2. Thanks @szabesz My comparison would be based strictly on licensing and 3rd party dependencies, so it wouldn't be fair. I didn't want to inject any processwire modules/jquery, etc. into the frontend. I leave processwire to do what it does best. I found sundeditor while working on a forum project a few years back. The source css/js files are fairly easy to update/customize, and the plain javascript, MIT license, and source were big selling points. So I can't really offer a comparison as I haven't tried modifying tinymce/ckeditor.
  3. Today
  4. Turns out, I started using Cline in PHPStorm instead of Junie. I used up all credits – that came with Junie's trial period – in no time. However, that was not enough time to decide if Junie is worth the money or not. My main issue was that I want to use agent Skills, but Junie is still in "beta" state in that regard and it has no useful documentation on its Skills feature either. Being new to coding with AI agents, I do not want to spend money on something I do not have enough time to play with in advance. So I looked for alternatives, and found the Cline plugin (extension), and sort of fell in love with it at first sight :D I have a z.ai subscription (GLM Coding Lite-Yearly Plan) which was cheap thanks to last year's Black Friday deal (thanks to @wbmnfktr for mentioning z.ai to me) and the GLM-5.x models are pretty good (https://docs.z.ai/guides/llm/glm-5#coding-performance-on-par-with-claude-opus-4-5) so the Cline + GLM-5.x combo works well, cheap and is relatively easy to get started with. My subscription has a "5 Hours Usage Quota" and the maximum I used it so far was only 6% percent in a 5-hour coding session, giving me plenty of room for a lot more usage. So this setup is cheap and powerful, I "just" need to develop my own Skills library, on which I already started to work on. (BTW, JetBrains IDEs do support 3rd party LLM providers, but z.ai has a non-standard BASE URL, and the IDE does not support that which is a shame. It fails just because of that.) Anyway, thanks to the output path being configurable in the settings of Contex, I pointed it to .agents/skills (that's where Cline saves its files, contrary to Cline's official doc page which is erroneously states it is [~/].cline/skills), so now the module generates files into .agents/skills/context. So far so good, but a Skill to be used properly by the agent needs a proper SKILL.md file too. At least that is my understanding. So I vibe coded a BASH script to generate SKILL.md for me, I just need to run it after each Context generation process, and I get the following in .agents/skills/context/SKILL.md : I am not sure this is the best SKILL.md content but the LLM suggested this and it aligns with Cline's recommendation (after I asked the model to make it align...), so I started using/testing it. @maximus I wonder if you could add support for optionally generating SKILL.md, too. The issue with SKILL.md files is that agents seem to have certain preferences regarding their content (ie. the format of the information is presented in them), so maybe a template-based approach could be adopted. I am just brainstorming here as I have no specific idea about how such a template option could be implemented. Thanks in advance for considering.
  5. Yesterday
  6. Christophe

    I'm back

    Welcome back @Soma 👽!
  7. This week I worked with Claude Code on refactoring the core WireHooks class to improve it in various ways. I do this work because I like to code, so am of course handling that part. But Claude has been very helpful in finding issues as well as reviewing all my code before it gets committed. In this case, Claude found several bugs that are now fixed, and also saved me from introducing more when I added new features. Working with Claude Code reminds me of 1989 when I was in high school learning Turbo Pascal and building a BBS, and getting help from a neighbor and good friend that was doing the same. I learned so much then and now it feels like that time again. But man have things come a long way since then. I asked Claude about helping with the ProcessWire issues and requests GitHub repos, and now it's solving issue reports, replying to them and committing fixes. For simple fixes that involve a line or two, I'm going to let it handle the the whole thing. It's posting under my GitHub account, but it identifies itself as Claude in replies and and in commits. That way it's easy to tell what it does under my account. I had it just cover a couple of issue reports today as a test (see 2195 and 2192), but will be having it do a lot more going forward. Back to the WireHooks class, I asked Claude about writing a commit message for the WireHooks updates, and wow, check this out: Bug fixes in WireHooks: - conditionalArgMatch(): Fix Selector mutation bug where the shared Selector object stored in hook options was permanently mutated after the first call, causing argMatch hooks (e.g. addHook('WireLog::save(=errors)')) to stop matching after their first invocation. - getHooks(): Remove key variable from foreach to prevent shadowing $method param. - getHooks(): Replace array_merge() in loops with direct array append. - addHook(): Prevent 'noAddHooks' option from leaking into stored hook options. - isHookedOrParents(): Use $className string consistently in cache key writes, rather than $class which may be a Wire object. - runHooks(): Scope $useHookReturnValue inside foreach so it resets per hook. - runHooks(): Use strict null comparison ($toObject === null). Bug fixes in HookEvent: - arguments(): Allow setting an argument value to null (use func_num_args() > 1 instead of $value !== null check). - arguments(): Use array_key_exists() instead of isset() for null-correct lookup. - getArgumentNames(): Guard ReflectionMethod instantiation with method_exists() to avoid uncaught ReflectionException for dynamically-added hook methods. - __toString(): Fix argument list building (was appending to string instead of array), add null/bool display, avoid PHP fatal for objects without __toString(). New features in WireHooks: - cancelHooks: Now accepts string values 'before' or 'after' (in addition to boolean true) to cancel only hooks of a given type. For example, a before hook can set $event->cancelHooks = 'after' to cancel remaining after hooks without affecting the hooked method call. - Custom event data carry-forward: Custom data set on a HookEvent (via $event->customKey = $value) is now carried forward to subsequent HookEvent instances within the same runHooks() call. This allows before hooks to pass data to after hooks without using external variables. - getHooks('*'): Supports wildcard method to return all hooks for all objects. Available in debug mode only (uses allStaticHooks/allLocalHooks aggregates). - allStaticHooks: Now tracked alongside allLocalHooks when debug mode is on. - addHooks() comment fix: "If there is a parenthesis" corrected to "no parenthesis". - addHook() exception messages improved with additional context. - runHooks() docblock updated to include 'either' type. - hookTimer() PHPDoc: @param String corrected to lowercase string. New features in HookEvent: - $defaults static property: Defines the canonical set of standard HookEvent fields, used to distinguish custom data from built-in event properties. - $eid property: Each HookEvent instance gets a unique sequential event ID. - set() override: Tracks non-default keys set on the event in $customKeys. - getCustomData(): Returns only the custom (non-default) data set on the event, used internally by runHooks() for the carry-forward feature. - cancelHooks property updated to support bool|string type. Anyway, I know a lot of you here are deep into the AI stuff, and I'm just getting started, so none of this is surprising. But it sure is a pleasure collaborating with Claude Code on this stuff and it brings back that coding wonder and excitement from 1989. Some Claude and Claude Code podcasts I've enjoyed listening to or watching this week include one from AI for Humans Claude is Cooking, Wednesday's AI Daily Brief on How to use Claude's new upgrades, and the Get Educated AI video of how to setup Claude to use your computer and web browser, etc. (though I've not tried this just yet!). Lastly, last week I put together a ProcessWire powered JSON feed of bike tours, and the client prompted that they wanted a map and look what Claude Code did, I'm impressed!
      • 7
      • Like
  8. Is Vimeo or a dedicated video host not an option?
  9. @szabesz Sorry to hear that. I've been billed from Singapore for Kimi Code... Not sure if it would be useful to add mandarin in the email subject to create urgency? Though I think english is widely spoken there too.
  10. Hi @Roope, thanks for this great module. I found a markup issue in Email Obfuscation 1.3.1 (ProcessWire 3.x, PHP 8.1+) when an email link contains an aria-label attribute. Reproduction Input HTML: <a href="mailto:mail@domain.de" class="uk-button uk-button-text" aria-label="Send email">mail@domain.de</a> Output with EMO enabled: <a href="mailto:<noscript data-emo="...">Enable JavaScript to view protected content.</noscript>" class="uk-button uk-button-text" aria-label="Send email"><noscript data-emo="...">Enable JavaScript to view protected content.</noscript></a> Expected behavior: the whole <a ...>...</a> should be obfuscated/replaced as one unit, not inject <noscript> into the href attribute. Suspected cause In EmailObfuscation.module (obfuscate()), the skip pattern for label also matches aria-label, which seems to split the anchor tag before the mailto regex can process it as a full link. As a temporary workaround, removing aria-label from mail links avoids the broken markup.
  11. Last week
  12. My way to upload local site to production is using basic Linux tools. I'm on Windows but with a WSL console running Debian, you have access to all tools. First I use Ant to synchronize files from my code to a separate local directory (Ant is a set of tools that you use by creating a simple XML describing tasks), I exclude some files or directories (like PW cache and logs...), automatically increment a version number, replace some variables in some files (like site version/build number, debug variables...)... Then an rsync command send the code to the server. Deploying a site update is then only executing this script and waiting a few seconds. 🙂 I exclude site config.php because some values change from production server (database credentials, debug=false|true, Stripe identifiers...). Example of a deploy script (very simple but powerful stuff ^^): #!/bin/bash # Directory where Ant project synchronize files. build_dir=./target/processwire/ echo "=> Executing ANT project..." cmd.exe /c build-processwire.bat buildOk=$? if [ $buildOk -ne 0 ]; then echo "ANT build failed, stopping script." exit fi read -p "Press enter to update PRODUCTION server, ctrl+c to cancel." echo "=> Copying files to web server..." rsync -avh --delete-delay --include-from=deploy-includes.txt --exclude-from=deploy-excludes.txt --chown=linux_user_name:linux_user_group -e ssh $build_dir linux_login@server_ip:/var/www/path_to_productionsite/ If this is the first time I deploy this project, or if this is a staging server (not production) and I want to reset database at every deploy, I add this line, it copies my local PW database to the server: echo "=> Copying database to production" /mnt/e/xampp_php8.2/mysql/bin/mysqldump.exe --add-drop-database -uDB_local_user --databases database_name | ssh linux_login@server_ip "mysql -uDB_user -pDB_password" Since Ant project uses files synchronization (and not copy) and rsync does the same, it deploys to server only files that have changed since last deploy, so it's fast and console logs are clear. If you're interested with Ant XML file, I can show you an example. Same with directories/files I exclude from build, both in Ant and rsync. EDIT: For the first installation on server, I start with a regular PW install directly on server. EDIT 2: Script also set the site in maintenance while deploying, displaying a "Maintenance, please come back later" message, with a bit of Apache configuration. It uploads a file at the site root on server, and when this files exists Apache redirects users to a basic HTML page (except for my IP).
  13. We send everything through Amazon SES. You can limit it to specific regions, and you won’t find a cheaper way to send bulk emails. It’s $0.10 per 1,000 emails. And the deliverability is top-notch. And no fixed monthly fees — just pay as you go.
  14. Hello, After form validation, is it possible to force the form to display an error on a given field? This is possible with the processwire core InputfieldForm and I often use it. Generally I use this to do additional and more complex checks after the basic form validation. For example the form contains a file upload field, it's a zip and I have to validate zip content, if it's not valid the form should display values entered by user with an error message on this field. I know I can create a custom validation rule, but within this validation I have to access variables that are outside the form. For example the form is to edit an ItemData instance, and within the validation I need to access this object. Can I pass variables to the validator custom rule? I looked at the documentation, what is the $params parameter, can I use it to pass variables to validator? Valitron\Validator::addRule('alwaysFail', function($field, $value, array $params, array $fields) With the core InputfieldForm I created a wrapper class and I use it this way: $onValidate = function (AccountEditionForm $form): void { // $onValidate is called only if form basic validation succeeded. if (someCondition) { // Triggering an error, $onSuccess won't be called, // form field will be red with error message, // form still filled with user values so user has only to fix the field with error. $form->setError('fieldName', 'errorMessage'); } } $onSuccess = function (AccountEditionForm $form)): void { // Form is valid (basic validation + $onValidate), use its data. }; $form->execute($onSuccess, $onValidate); Is there a way to do this kind of process, or can you recommend me another way? Thank you.
  15. Well that was quick! 😅 Wrapping with json_encode() worked for me. Gsap text animation now works: const paragraphs = [ <?=json_encode(__("Here is some text"))?>, <?=json_encode(__("Here is some more text"))?> ];
  16. Hi @adrian When uploading and inserting an image into a field (e.g., a TinyMCE "body" field) with this module active, any UTF-8 characters (like Greek letters) in other Textarea fields on the same page (e.g., a plain text "seo_description" field) are unexpectedly converted into HTML entities (e.g., "α" becomes "&alpha;" ) and saved to the database. I asked Gemini to provide a solution. Steps to Reproduce: Create a page with a TinyMCE field (body) and a standard Textarea field (seo_description). Enter UTF-8 characters (e.g., Greek text) into the "seo_description" field and save. Upload and insert an image into the "body" field. Save the page. Check the "seo_description" field or the database; the UTF-8 text has been converted to HTML entities. Root Cause Analysis: The issue stems from the "replaceRteLinks" method, which is triggered to update image URLs after a rename. Scope Logic: The method fetches all fields of type FieldtypeTextarea and FieldtypeTextareaLanguage and processes them using DOMDocument, regardless of whether the field actually contains the image URL. Encoding Handling: DOMDocument::saveHTML() natively converts extended characters into HTML entities. Because it processes unrelated fields, it permanently alters their raw text in the database. Proposed Solution: Updating the "replaceRteLinks" method to strictly check if the field contains the target URL before processing it, and utilizing ProcessWire's native sanitizer to restore entities converted by DOMDocument. Here is the patched method: private function replaceRteLinks($newFilename, $oldFilename) { $textareaFields = $this->wire('fields')->find("type=FieldtypeTextarea|FieldtypeTextareaLanguage"); $fieldsStr = $textareaFields->implode('|', 'name'); $oldRelativeUrl = str_replace($this->wire('config')->paths->root, '', $oldFilename); $oldRelativeUrlSansExt = str_replace(pathinfo($oldFilename, PATHINFO_EXTENSION), '', $oldRelativeUrl); foreach($this->wire('pages')->find("$fieldsStr%=$oldRelativeUrlSansExt, include=all") as $p) { foreach($textareaFields as $taf) { // FIX 1: Only parse the field if it actually contains the image URL. if($p->$taf != '' && strpos((string)$p->$taf, $oldRelativeUrlSansExt) !== false) { // FIX 2: Explicitly declare UTF-8 for the DOMDocument $pagedom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); // add <cun> as fake root element $pagedom->loadHTML('<?xml encoding="utf-8" ?><cun>' . $p->$taf . '</cun>', LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED | LIBXML_SCHEMA_CREATE); $pagedom = $this->replaceRteLink($pagedom, $newFilename, $oldFilename, 'a', 'href'); $pagedom = $this->replaceRteLink($pagedom, $newFilename, $oldFilename, 'img', 'src'); // remove fake root element $html = str_replace(['<cun>', '</cun>', '<?xml encoding="utf-8" ?>'], '', $pagedom->saveHtml()); // FIX 3: Restore UTF-8 characters converted to entities by DOMDocument $html = $this->wire('sanitizer')->unentities($html); $p->of(false); $p->$taf = $html; libxml_clear_errors(); $p->save($taf); } } } }
  17. The content owner of this video that I randomly chose from the YouTube homepage had marked this particular video content as not-embeddable, so the JSON response from the oembed provider of YouTube responded with "Unauthorized" which the Essence library doesn't really handle (it just returns nothingness, which in this case is kind of appropriate). Once I tracked that down, I tried another YouTube video URL and it worked as expected. 🤦
  18. Hi Folks. I've got a client with an e-commerce jewellery shop. She's been updating the prices a lot in the lasts week or so (price of silver!) but can only go about 30 minutes before the site crashes. I've had a look a the logs and see frequent "SQLSTATE[42000] [1203] User xxxxxx_admin already has more than 'max_user_connections' active connections (/wire/core/WireDatabasePDO.php:556)" notices followed by "SQLSTATE[HY000]: General error: 2006 Server has gone away (/wire/core/WireDatabasePDO.php:1000)" notices (the xxxxx_admin is the database admin account). The site's been in use for about 5 years without issues, she uses the 'live search' feature in the PW backend (upper right corner) to jump from product to product by typing in the product_code (a field I had added via the PageSearch modules' config/settings). She said she often would type a PC like '41-PB-1001' and would often make mistakes, then backup to the second hyphen then that's when things would sometimes freeze. I've checked the cPanel when the site goes down and the CPU uses is always screaming at 99%/100%. They get pretty good traffic on the site, so I've been hesitant to updated anything, but last week I updated php to 8.2, then today I brought PW up to 3.0.255 but it didn't fix it. I can't reproduce it easily, partly because I can't take the time to type searches for 20 or 30 minutes! Anyone ever seen this before? Thanks.
  19. Hey all, MediaHub is ready! Thanks for the kind words and the questions about availability. I've been replying to a few people privately, but here's the latest.. It has been given a thorough kicking on my own site(s) and 2 client sites. I'm opening up a waitlist for those interested in purchasing early access. I want to limit it initially to the first 5-10-ish people. IE small batches so I can make sure the purchase and licensing flow works smoothly hear how the setup goes across different environments provide proper support to each person. If you're interested, DM me here. Early access members will get a discount on the first year of updates as a thank you for feedback. I'll post a walkthrough video here shortly, but the screenshots earlier in the thread should give you a pretty good picture of things. Cheers everyone, Peter
  20. psy

    SeoMaestro

    Still using Seomaestro. Discovered a scenario where it missed pages in the sitemap that used urlsegments. In my case, it was the blog module authors page. It should also handle paginated pages but untested. Hope this helps. $wire->addHookAfter('SeoMaestro::sitemapItems', function(HookEvent $event) { $items = $event->return; $pages = wire('pages'); $users = wire('users'); $roles = wire('roles'); $config = wire('config'); $seen = []; foreach($items as $item) { if(!empty($item->loc)) { $seen[rtrim($item->loc, '/')] = true; } } $getSeoSettings = function(Page $page) { foreach($page->template->fieldgroup as $field) { if(!$field->type instanceof FieldtypeSeoMaestro) { continue; } $seo = $page->get($field->name); if(!$seo || !$seo->sitemap->include) { return null; } return [ 'priority' => $seo->sitemap->priority, 'changefreq' => $seo->sitemap->changeFrequency, ]; } return null; }; $appendItem = function($url, $lastmod, array $settings) use (&$items, &$seen) { $key = rtrim($url, '/'); if(isset($seen[$key])) { return; } $item = new \SeoMaestro\SitemapItem(); $item->set('loc', $url); $item->set('lastmod', date('c', (int) $lastmod)); $item->set('priority', $settings['priority']); $item->set('changefreq', $settings['changefreq']); $items[] = $item; $seen[$key] = true; }; $appendPaginatedItems = function(Page $page, $totalItems, $perPage, $lastmod) use ($config, $getSeoSettings, $appendItem) { $settings = $getSeoSettings($page); if(!$settings) { return; } $totalPages = (int) ceil($totalItems / $perPage); if($totalPages < 2) { return; } for($pageNum = 2; $pageNum <= $totalPages; $pageNum++) { $appendItem( $page->httpUrl . $config->pageNumUrlPrefix . $pageNum . '/', $lastmod ?: $page->modified, $settings ); } }; $blogPostsPage = $pages->get('template=blog-posts'); if($blogPostsPage->id) { $latestPost = $pages->get("template=blog-post, sort=-modified"); $appendPaginatedItems( $blogPostsPage, $pages->count("template=blog-post"), 8, $latestPost->id ? $latestPost->modified : $blogPostsPage->modified ); } foreach($pages->find('template=blog-category, include=hidden') as $categoryPage) { $latestPost = $pages->get("template=blog-post, blog_categories=$categoryPage, sort=-modified"); $appendPaginatedItems( $categoryPage, $pages->count("template=blog-post, blog_categories=$categoryPage"), 10, $latestPost->id ? $latestPost->modified : $categoryPage->modified ); } $authorsPage = $pages->get('template=blog-authors'); $authorSettings = $authorsPage->id ? $getSeoSettings($authorsPage) : null; if($authorsPage->id && $authorSettings) { $authorRole = $roles->get('blog-author'); foreach($users->find("roles=$authorRole, sort=title") as $author) { $authorSlug = wire('sanitizer')->pageName($author->title); if(!$authorSlug) { continue; } $postCount = $pages->count("template=blog-post, created_users_id=$author"); if(!$postCount) { continue; } $authorUrl = $authorsPage->httpUrl . $authorSlug . '/'; $latestPost = $pages->get("template=blog-post, created_users_id=$author, sort=-modified"); $lastmod = $latestPost->id ? $latestPost->modified : ($author->modified ?: $authorsPage->modified); $appendItem($authorUrl, $lastmod, $authorSettings); $totalPages = (int) ceil($postCount / 10); if($totalPages < 2) { continue; } for($pageNum = 2; $pageNum <= $totalPages; $pageNum++) { $appendItem( $authorUrl . $config->pageNumUrlPrefix . $pageNum . '/', $lastmod, $authorSettings ); } } } $event->return = $items; });
  21. @jploch @diogo Awesome work, I love the new website!
  22. Most important to me is that I understand everything that goes into the core, and that means that AI is used for ideas and suggestions, but the code is still written by hand. And even then, the suggestions and ideas only end up in the core if they produce a measurable improvement. After testing and benchmarking, sometimes the ideas/suggestions result in an improvement, and just as often they do not. I do the same with pull requests. Coding and re-coding something is how I feel comfortable that I understand it. Perhaps too old-school but I don't think that will ever change. So long as I'm in charge of the core, I need that level of understanding with it. On another project I'm working on with a client (an add-on to their website), we're letting Claude handle the code entirely, with lots of instructions from us, but zero code from us. It's kind of a test and a learning experience, and the client initiated it. We don't know the details of the code, but we do know that the code works quite well. Though I had a peek at its code and found it to be quite solid. What's funny is that in this case, Claude is having me build web services it can pull data from. So I'm giving it instructions, but it's also giving me instructions. 🙂
  23. [Update] v1.0.25 – Multi-Email Account Merge A small but handy addition for real-world scenarios: customers sometimes purchase with different email addresses and end up with split accounts. The new Merge User Accounts tool in the module config lets you consolidate them in seconds. You enter the source email (the old/unwanted account) and the target email (the one the customer wants to keep). The module transfers all purchases from the source to the target and permanently deletes the source account. Before committing, you can run it in test mode to see exactly what would be transferred – no changes are written. Once you're confident, check "Merge now", save, and the merge report confirms what happened. One thing worth noting: the source account is permanently deleted after the merge – so double-check in test mode first. 🙂 Feedback welcome!
  24. @Peter Knight Thanks for the work on this, Peter. Gemini AI actually helped me realize that it should work seamlessly with my PhpStorm + Cline + z.ai setup as well. Since your module includes the local Node.js MCP script, I should just be able to point my local Cline instance to that file via its MCP settings, and it will handle the sync workflow right inside PhpStorm. Looking forward to the release!
  25. Thank you both for the feedback! Added a dedicated sitemap-edit permission in v1.0.1. It is created automatically on module install — just assign it to the roles you need in Access > Roles. https://github.com/mxmsmnv/Sitemap
  1. Load more activity
×
×
  • Create New...