<?xml version="1.0"?>
<rss version="2.0"><channel><title>Tutorials Latest Topics</title><link>https://processwire.com/talk/forum/13-tutorials/</link><description>Tutorials Latest Topics</description><language>en</language><item><title>ProcessWire development and production in the AI era: how not to overwrite a live site</title><link>https://processwire.com/talk/topic/32020-processwire-development-and-production-in-the-ai-era-how-not-to-overwrite-a-live-site/</link><description><![CDATA[<p>
	<a data-fileext="jpg" data-fileid="29526" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2026_08/processwire-ai-deployment-safety-doodle.jpg.f9b72ae6c79c33bd007864836cb0aaa3.jpg" rel=""><img alt="A developer and AI assistant safely deploy code while the production database remains protected" data-fileid="29526" data-ratio="56.24" style="height:auto;width:777px;" width="1000" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2026_08/processwire-ai-deployment-safety-doodle.thumb.jpg.300da1d4783c980d5f7f03bbfbfbb9c0.jpg" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" /></a>
</p>

<p>
	Once a ProcessWire site is live, the production database is not a deployable artifact. It is the source of truth for content, users, roles, module settings, and module-owned data.
</p>

<p>
	Deploy code from Git. Apply reviewed migrations to the production database. Preserve production data, uploads, configuration, and secrets. If an AI coding agent is involved, write these boundaries down in <code>AGENTS.md</code> and enforce them with scripts rather than relying on the agent to infer what “deploy” means.
</p>

<p>
	I arrived at this rule through a real incident.
</p>

<h2>
	The incident
</h2>

<p>
	I was rebuilding a ProcessWire site locally with an AI coding agent. The data model had changed substantially, the development site contained useful demo content, and the production site had little activity. I asked the agent to deploy the new version and replace the old data.
</p>

<p>
	Technically, the operation succeeded:
</p>

<ul>
	<li>
		the code was deployed;
	</li>
	<li>
		the new schema appeared;
	</li>
	<li>
		the development database was imported;
	</li>
	<li>
		development uploads replaced production uploads;
	</li>
	<li>
		the public pages worked.
	</li>
</ul>

<p>
	But a database is not just “content”. The import also replaced production-only module configuration, users, roles, support tickets, AI conversations, and records stored in custom module tables. SMTP happened to survive because its configuration matched in both environments. Other settings did not.
</p>

<p>
	Backups existed, so the incident was recoverable. The site also had almost no real activity. On a busy site, the same operation could have destroyed orders, form submissions, member accounts, unpublished editorial work, audit history, or messages created during the deployment window.
</p>

<p>
	The important lesson was not “the AI made a mistake”. The deployment process allowed an ambiguous instruction to become a destructive database operation. That is a systems problem.
</p>

<h2>
	Why ProcessWire needs an explicit deployment model
</h2>

<p>
	ProcessWire's file layout is deliberately clear: <code>/wire/</code> contains the core, while <code>/site/</code> contains site-specific templates, modules, configuration, and assets. The official documentation also identifies <code>site/config.php</code> as the site configuration file and <code>site/assets/</code> as writable runtime storage.
</p>

<p>
	That filesystem separation is useful, but a mature ProcessWire project has another boundary that is easier to miss:
</p>

<h3>
	Deployment ownership map
</h3>

<ul>
	<li>
		<strong>Git release</strong> — templates, hooks, module code, and frontend assets. <strong>Source of truth:</strong> the repository.
	</li>
	<li>
		<strong>Database structure</strong> — fields, templates, fieldgroups, and module schemas. <strong>Source of truth:</strong> reviewed migrations.
	</li>
	<li>
		<strong>Production database</strong> — pages, users, roles, settings, submissions, and module-owned tables. <strong>Source of truth:</strong> production.
	</li>
	<li>
		<strong>Runtime configuration</strong> — database credentials, hostnames, and environment flags. <strong>Source of truth:</strong> the production runtime.
	</li>
	<li>
		<strong>Secrets</strong> — SMTP, API, and OIDC credentials. <strong>Source of truth:</strong> a secret store or protected runtime files.
	</li>
	<li>
		<strong>Writable assets</strong> — uploads, generated files, logs, sessions, and backups. <strong>Source of truth:</strong> production storage.
	</li>
</ul>

<p>
	The initial launch is special. If production is empty, cloning a reviewed development database may be reasonable. After the first real user, form submission, editor change, or module configuration, that direction must stop.
</p>

<p>
	Production may flow to development through a protected and preferably sanitized snapshot. Development must not flow back as a whole database.
</p>

<p>
	This is the same problem described in ProcessWire's own article introducing migrations: once the live server receives content while features continue to be developed locally, the databases diverge and changes must be merged rather than replaced.
</p>

<h2>
	Use releases for code and migrations for change
</h2>

<p>
	A production release should be an immutable Git commit, preferably accompanied by a tag. Build the deployable file list from that commit—not from the current working directory.
</p>

<p>
	A release manifest should include only public runtime code and static assets. It should explicitly exclude:
</p>

<ul>
	<li>
		<code>.git/</code> and CI configuration;
	</li>
	<li>
		<code>AGENTS.md</code> and development documentation;
	</li>
	<li>
		<code>site/config.php</code> and environment-specific config;
	</li>
	<li>
		database dumps, credentials, and private keys;
	</li>
	<li>
		<code>site/assets/files</code>, caches, logs, sessions, and backups;
	</li>
	<li>
		tests, fixtures, local tooling, and editor configuration.
	</li>
</ul>

<p>
	ProcessWire fields and templates live in the database, so code deployment alone is not always sufficient. Represent structural changes as idempotent, versioned migrations. A migration may use ProcessWire's API, a migration module, or a project-specific CLI script. The important properties are the same:
</p>

<ol>
	<li>
		it has a preview or audit mode;
	</li>
	<li>
		it is safe to run more than once;
	</li>
	<li>
		it changes only named structures or records;
	</li>
	<li>
		it preserves unknown production configuration;
	</li>
	<li>
		large transformations are bounded and resumable;
	</li>
	<li>
		destructive steps require a fresh verified backup and rollback data;
	</li>
	<li>
		successful application is recorded.
	</li>
</ol>

<p>
	ProcessWire has community options such as ProcessDbMigrate and migration modules, but no tool removes the need to classify production data correctly.
</p>

<h2>
	A backup is not verified until it can be restored
</h2>

<p>
	“The backup command returned zero” is not enough.
</p>

<p>
	For every production release:
</p>

<ol>
	<li>
		create a timestamped database backup outside the document root;
	</li>
	<li>
		verify its size and checksum;
	</li>
	<li>
		verify compressed-stream integrity;
	</li>
	<li>
		periodically restore it into an isolated database;
	</li>
	<li>
		keep a backup of the exact files being replaced;
	</li>
	<li>
		record the rollback path before changing production.
	</li>
</ol>

<p>
	This matters because backup tools can produce syntactically valid-looking files that fail only during restoration. Test the recovery path, not merely the backup button.
</p>

<p>
	The official ProcessWire upgrade guide likewise recommends backing up both files and database and testing upgrades on a development or staging site first.
</p>

<h2>
	Add a fail-closed deployment guard
</h2>

<p>
	Documentation is necessary, but an executable invariant is better.
</p>

<p>
	Before deployment, create a private snapshot containing no secret values:
</p>

<ul>
	<li>
		row counts for protected tables;
	</li>
	<li>
		page counts by template;
	</li>
	<li>
		stable page IDs by template;
	</li>
	<li>
		installed module names;
	</li>
	<li>
		names and configured/empty state of module configuration keys;
	</li>
	<li>
		intended Git commit;
	</li>
	<li>
		production hostname;
	</li>
	<li>
		verified backup metadata.
	</li>
</ul>

<p>
	After code deployment and migrations, take the same measurements and compare them. Block completion if:
</p>

<ul>
	<li>
		a protected table disappeared or lost rows;
	</li>
	<li>
		a pre-existing page ID disappeared;
	</li>
	<li>
		a template or installed module disappeared;
	</li>
	<li>
		a module configuration key disappeared;
	</li>
	<li>
		a configured value became empty;
	</li>
	<li>
		the host or release commit does not match.
	</li>
</ul>

<p>
	Stable IDs matter. Row counts alone can miss a replacement database containing the same number of different records.
</p>

<p>
	Some legitimate migrations intentionally remove obsolete data. That should be an exceptional workflow requiring a change ID, a table-by-table impact report, a tested restore, and a second explicit approval. A generic “deploy everything” instruction must never authorize it.
</p>

<h2>
	What to tell an AI coding agent
</h2>

<p>
	An AI agent is fast enough to turn an ambiguous sentence into a complete deployment before a human notices the ambiguity. Give it a narrower contract.
</p>

<p>
	The repository instructions should state:
</p>

<ul>
	<li>
		development and production paths and hostnames;
	</li>
	<li>
		production is read-only unless a production change is explicitly requested;
	</li>
	<li>
		commit, push, deploy, migrate, publish, and replace data are separate actions;
	</li>
	<li>
		“deploy” means code-only unless named migrations are included;
	</li>
	<li>
		a development database must never overwrite a launched production database;
	</li>
	<li>
		production users, content, module configuration, custom tables, and uploads must be preserved;
	</li>
	<li>
		secrets must not be read, printed, hashed, copied locally, or committed;
	</li>
	<li>
		every mutation needs a fresh verified backup and exact rollback path;
	</li>
	<li>
		migrations run in preview first and must be bounded/idempotent;
	</li>
	<li>
		a data decrease is a blocker, not a warning;
	</li>
	<li>
		if scope is ambiguous, stop after read-only investigation;
	</li>
	<li>
		destructive approval is two-phase: impact report first, explicit confirmation of that report second;
	</li>
	<li>
		maintenance is not removed and success is not reported while checks fail.
	</li>
</ul>

<p>
	The open <code>AGENTS.md</code> format is useful here because it gives multiple coding agents a predictable repository-level instruction file. Its documentation explicitly recommends including build commands, testing, security concerns, and deployment details, and supports more specific nested files where needed.
</p>

<p>
	I prepared a reusable template and attached it to this post as ProcessWire-AGENTS-production-safety-template.zip. The archive contains a single AGENTS.md file. Download it, replace the placeholders, and commit it to the root of your project. Do not deploy it into the public document root.
</p>

<h2>
	A practical release checklist
</h2>

<h3>
	Before maintenance
</h3>

<ul>
	<li>
		Select an immutable Git commit.
	</li>
	<li>
		Review the diff and release manifest.
	</li>
	<li>
		List every migration and classify its risk.
	</li>
	<li>
		Create and verify database and file backups.
	</li>
	<li>
		Create the protected data snapshot.
	</li>
	<li>
		Write the rollback command/path.
	</li>
</ul>

<h3>
	During maintenance
</h3>

<ul>
	<li>
		Deploy only manifest files.
	</li>
	<li>
		Preserve production config, secrets, uploads, logs, sessions, and backups.
	</li>
	<li>
		Run migrations in preview, then apply.
	</li>
	<li>
		Run the fail-closed comparison.
	</li>
	<li>
		Stop on any unexplained decrease.
	</li>
</ul>

<h3>
	Before reopening
</h3>

<ul>
	<li>
		Test origin and CDN routes.
	</li>
	<li>
		Test authentication and representative member workflows.
	</li>
	<li>
		Verify email delivery readiness and background jobs.
	</li>
	<li>
		Inspect new logs without exposing secrets.
	</li>
	<li>
		Record commit, manifest checksum, migration results, backups, and rollback.
	</li>
	<li>
		Remove maintenance only when every blocker is resolved.
	</li>
</ul>

<h2>
	Final principle
</h2>

<p>
	The database is not “the backend version of the code”. On a live ProcessWire site it is accumulated production state.
</p>

<p>
	AI does not change this rule. AI makes it more important to encode the rule in files, scripts, permissions, and fail-closed checks—because the agent can execute a bad assumption much faster than a human can recognize it.
</p>

<h2>
	References
</h2>

<ul>
	<li>
		<a href="https://processwire.com/docs/start/structure/files/" rel="external">ProcessWire file and directory structure</a>
	</li>
	<li>
		<a href="https://processwire.com/docs/security/migration/" rel="external">ProcessWire: migrating to production</a>
	</li>
	<li>
		<a href="https://processwire.com/docs/start/install/upgrade/" rel="external">ProcessWire upgrade best practices</a>
	</li>
	<li>
		<a href="https://processwire.com/docs/security/file-permissions/" rel="external">ProcessWire file permissions and protecting site/config.php</a>
	</li>
	<li>
		<a href="https://processwire.com/blog/posts/introduction-migrations-module/" rel="external">ProcessWire: Introduction to the Migrations module</a>
	</li>
	<li>
		<a href="https://processwire.com/modules/process-db-migrate/" rel="external">ProcessDbMigrate module directory entry</a>
	</li>
	<li>
		<a href="https://agents.md/" rel="external nofollow">AGENTS.md open format</a>
	</li>
</ul>

<p>
	<a data-fileext="zip" data-fileid="29525" href="https://processwire.com/talk/applications/core/interface/file/attachment.php?id=29525&amp;key=ae9fec20df6683ad584a46f9adfbe84b" rel="">ProcessWire-AGENTS-production-safety-template.zip</a>
</p>

<p>
	 
</p>

<p>
	 
</p>

<p>
	 
</p>

<p>
	 
</p>
]]></description><guid isPermaLink="false">32020</guid><pubDate>Tue, 11 Aug 2026 04:01:31 +0000</pubDate></item><item><title>Running a ProcessWire site with Docker</title><link>https://processwire.com/talk/topic/10736-running-a-processwire-site-with-docker/</link><description><![CDATA[<div><img src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt="logo.png" data-src="https://www.docker.com/sites/all/themes/docker/assets/images/logo.png"></div>
<div> </div>
<div>Docker (<a href="http://www.docker.com" rel="external nofollow">http://www.docker.com</a>) is <em>an open platform for building, shipping and running distributed applications.</em>
</div>
<div> </div>
<div>Docker <em>containers</em> are a great way to package a complete application with its specific dependencies in a portable way so that it can easily be deployed on any compatible network or cloud infrastructure.</div>
<div> </div>
<div>Recently I spent a few days making my ProcessWire site run in a Docker container, and - as I could not find any good tutorial for this - it sounded like a good idea to write one.  <img data-emoticon="true" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt=":)" srcset="https://processwire-forums.s3.us-west-2.amazonaws.com/emoticons/smile@2x.png 2x" width="20" height="20" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/emoticons/default_smile.png"></div>
<div> </div>
<div>You will find on the web plenty of presentations and tutorials about Docker, so I won't start with the basic concepts, and this tuto assumes that you have a first understanding of Docker's fundamentals.</div>
<div> </div>
<div>What we want to do here is to migrate an existing site to a set of docker containers.</div>
<div> </div>
<div>Therefore, to start with, you should have:</div>
<div>- docker installed on your computer;</div>
<div>- the <span style="font-family:'courier new', courier, monospace;">site</span> directory of your ProcessWIre site</div>
<div>- a backup of your site's MySQL database</div>
<div> </div>
<div>Let's start.</div>
<div> </div>
<div><strong>Create a docker container for the site database</strong></div>
<div> </div>
<div>For several reasons (insulation, security, scalability), it is preferable to host the site database in a separate docker container. </div>
<div> </div>
<div>1. Set-up a SQL database with MariaDb or MySQL</div>
<div><pre class="ipsCode prettyprint">
$ docker run --name database -e MYSQL_ROOT_PASSWORD=rootdbpassword -d mariadb</pre></div>
<div>Here I choose to use the MariaDB official container in its latest version, but MySQLwould be just fine as well.</div>
<div> </div>
<div>2. Run a PhpMyAdmin container and create the ProcessWire database  </div>
<div>  </div>
<div>We first select an simple image with PhpMyAdmin on the Docker Hub: <span style="font-family:'courier new', courier, monospace;">nazarpc/phpmyadmin</span> and we create a docker container based on this image. This container will  access the port exposed by the <em>database</em> container via a private networking interface. We specify this with the `<span style="font-family:'courier new', courier, monospace;">--link</span>` option.</div>
<div> </div>
<div>It can be run temporarily (and exited by ctrl-C):</div>
<div><pre class="ipsCode prettyprint">
docker run --rm --link database:mysql -p 8881:80 nazarpc/phpmyadmin</pre></div>
<div>Or it can be run as a daemon in the background:</div>
<div><pre class="ipsCode prettyprint">
docker run -d --name phpmyadmin --link database:mysql -p 8881:80 nazarpc/phpmyadmin
</pre></div>
<div>From phpmyadmin (accessed from your browser at <a href="http://hostaddress:8881" rel="external nofollow">http://hostaddress:8881</a>) you can now create your ProcessWire database, create a dedicated user for it, and import the database content from a previously saved SQL file.</div>
<div> </div>
<div><em>Note: alternatively, you can do all database operations from the command line in the database docker container created during step 1, or use another mysql user interface container if you prefer…</em></div>
<div> </div>
<div>3. Update the database parameters in your site configuration</div>
<div> </div>
<div>In your site's `config.php` file, the sql server name shall be set to `mysql`:</div>
<div><pre class="ipsCode prettyprint">
$config-&gt;dbHost = 'mysql';</pre></div>
<div>Other `$config-&gt;dbXxx` settings shall match the database name, user and password of the just-created database.</div>
<div> </div>
<div> </div>
<div><strong>Create a Docker Image for Apache, PHP and the Processwire site</strong></div>
<div> </div>
<div>1. Create an image-specific directory with the following contents and `cd` to it</div>
<div><pre class="ipsCode prettyprint">
bash-3.2$ ls -l . config
.:
total 16
-rw-rw-rw-   1 jean-luc  staff  1163 21 aoû 12:09 Dockerfile
drwxr-xr-x  17 jean-luc  staff   578 17 aoû 12:48 ProcessWire
drwxr-xr-x   7 jean-luc  staff   238 21 aoû 12:07 config
drwxr-xr-x   7 jean-luc  staff   238 20 aoû 18:46 site

config:
total 160
-rw-rw-rw-  1 jean-luc  staff    160 20 aoû 18:28 msmtprc
-rw-rw-rw-  1 jean-luc  staff  72518 20 aoû 18:56 php.ini</pre></div>
<div>where:</div>
<ul><li>
<span style="font-family:'courier new', courier, monospace;">`ProcessWire</span>` contains the version of ProcessWire that we want to use for this site;  <br><em>It can be retrieved from github with a link like <a href="https://github.com/ryancramerdesign/ProcessWire/archive/" rel="external nofollow">https://github.com/ryancramerdesign/ProcessWire/archive/</a>{version}.zip`  <br>For example, the 2.6.13 dev version can be obtained by the link <a href="https://github.com/ryancramerdesign/ProcessWire/archive/7d37db8d6b4ca6a132e50aff496a70e48fcd2284.zip" rel="external nofollow">https://github.com/ryancramerdesign/ProcessWire/archive/7d37db8d6b4ca6a132e50aff496a70e48fcd2284.zip</a></em>
</li>
<li>`<span style="font-family:'courier new', courier, monospace;">site</span>`: our site-specific files</li>
<li>`<span style="font-family:'courier new', courier, monospace;">Dockerfile</span>`: the dockerfile for building the image (see below)</li>
<li>`<span style="font-family:'courier new', courier, monospace;">config</span>`: a directory containing specific configuration files copied to the docker image (see below)</li>
</ul><div>2.  Set the `Dockerfile` content</div>
<div><pre class="ipsCode prettyprint">
FROM php:5.6-apache

RUN    apt-get update \
&amp;&amp; apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng12-dev zziplib-bin msmtp\
&amp;&amp; a2enmod rewrite \
&amp;&amp; a2enmod ssl \
&amp;&amp; docker-php-ext-install mysqli pdo_mysql iconv mcrypt zip \
&amp;&amp; docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&amp;&amp; docker-php-ext-install gd 

EXPOSE 80
EXPOSE 443

# Add a specific php.ini file
COPY config/php.ini /usr/local/etc/php/

# Configure the mail sent utility msmtp (<a href="http://msmtp.sourceforge.net" rel="external nofollow">http://msmtp.sourceforge.net</a>) and make it readable only by www-data
COPY config/msmtprc /usr/local/etc/php/
RUN chmod 600 /usr/local/etc/php/msmtprc \
&amp;&amp; chown www-data:www-data /usr/local/etc/php/msmtprc

# Remove all default site files in /var/www/html
RUN rm -fR /var/www/html/*

# Copy ProcessWire core files
COPY ProcessWire/wire /var/www/html/wire
COPY ProcessWire/index.php /var/www/html/index.php
COPY ProcessWire/htaccess.txt /var/www/html/.htaccess

# Copy site-specific files
COPY site /var/www/html/site

# Make www-data the owner of site-specific files
RUN chown -R www-data:www-data /var/www/html/site

VOLUME /var/www/html/site
</pre></div>
<div>Based on the official image `php:5.6-apache`, it installs missing packages to the system, adds  mod-rewrite and mod-ssl to Apache, plus a number of PHP modules needed by Processwire (core or modules): mysqli, pdo_mysql, iconv, mcrypt, zip, and gd.</div>
<div> </div>
<div>Then it copies the site files to the location expected by the Apache server.  Finally it declares a Docker volume `/var/www/html/site` (i.e. the site files and assets), so that it can be shared with other containers.</div>
<div> </div>
<div>3. Set the msmtp configuration</div>
<div> </div>
<div>We need to configure a sendmail utility, so that we can send emails from php, for example when a user registers on the website. The simplest way to do it is to rely on an external smtp server to do the actual sending. That's why we use msmtp.</div>
<div> </div>
<div>- define the desired smtp account in `config/msmtprc`</div>
<div><pre class="ipsCode prettyprint">
account celedev-webmaster
tls on
tls_certcheck off
auth on
host smtp.celedev.com
port 587
user webmaster@celedev.com
from webmaster@celedev.com
password thepasswordofwebmasteratceledevdotcom</pre></div>
<div> </div>
<div>- in `config/php.ini`, configure the sendmail command so it uses msmtp:</div>
<div><pre class="ipsCode prettyprint">
sendmail_path = /usr/bin/msmtp -C /usr/local/etc/php/msmtprc --logfile /var/log/msmtp.log -a celedev-webmaster -t</pre></div>
<div> </div>
<div>4. Build the Docker image</div>
<div><pre class="ipsCode prettyprint">
docker build -t php-5.6-pw-celedev .
</pre></div>
<div>5. Create a Data-only container for the site files</div>
<div><pre class="ipsCode prettyprint">
docker run --name celedev-data php-5.6-pw-celedev echo "Celedev site data-only container"
</pre></div>
<div>6. Run the web server container</div>
<div><pre class="ipsCode prettyprint">
docker run --name celedev-site -p 8088:80 --link database:mysql --volumes-from celedev-data -d php-5.6-pw-celedev</pre></div>
<div>Note that this container is linked to our database and shares the 'celedev-data' volume created previously</div>
<div> </div>
<div>During development, it can be convenient to keep an access to the host file system from the container. For this, we can add a shared volume to the previous command:</div>
<div><pre class="ipsCode prettyprint">
docker run --name celedev-site -p 8088:80 --link database:mysql -v /Users/jean-luc/web/test-docker:/hostdir --volumes-from celedev-data -d php-5.6-pw-celedev</pre></div>
<div> </div>
<div>Our ProcessWire website is now up and running and we can test it in our browser at <a href="http://hostaddress:8088" rel="external nofollow">http://hostaddress:8088</a>. Great!</div>
<div> </div>
<div><strong>What we now have in Docker</strong></div>
<div><pre class="ipsCode prettyprint">
bash-3.2$ docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
php-5.6-pw-celedev    latest              2aaeb241c2e2        3 hours ago         1.149 GB
nazarpc/phpmyadmin    latest              e25cd4fd48b3        8 days ago          521 MB
mariadb               latest              dd208bafcc33        2 weeks ago         302.2 MB
debian                latest              9a61b6b1315e        5 weeks ago         125.2 MB

bash-3.2$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                    PORTS                                         NAMES
68cc5d976f0d        php-5.6-pw-celedev    "apache2-foreground"     20 hours ago        Up 20 hours               443/tcp, 0.0.0.0:8088-&gt;80/tcp                 celedev-site
0729fe6d6752        php-5.6-pw-celedev    "echo 'Celedev site d"   20 hours ago        Exited (0) 20 hours ago                                                 celedev-data
e3e9e3a4715c        mariadb               "/docker-entrypoint.s"   3 days ago          Up 3 days                 3306/tcp                                      database
</pre></div>
<div><strong>Saving the site data</strong></div>
<div> </div>
<div>We can create an archive of the site files by running a tar command in a dedicated container:</div>
<div><pre class="ipsCode prettyprint">
bash-3.2$ docker run --rm -it --volumes-from celedev-data -v /Users/jean-luc/web/test-docker:/hostdir debian /bin/bash

root@2973c5af3eaf:/# cd /var/www/html/
root@2973c5af3eaf:/var/www/html# tar cvf /hostdir/backup.tar site
root@2973c5af3eaf:exit

bash-3.2$
</pre></div>
<div><strong>Tagging and archiving the Docker image</strong></div>
<div> </div>
<div>We can also add a tag to the docker image that we have created in step 4 (recommended):</div>
<div><pre class="ipsCode prettyprint">
bash-3.2$ docker tag 2aaeb241c2e2 php-5.6-pw-celedev:0.11

bash-3.2$ docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
php-5.6-pw-celedev    latest              2aaeb241c2e2        3 hours ago         1.149 GB
php-5.6-pw-celedev    0.11                2aaeb241c2e2        3 hours ago         1.149 GB
nazarpc/phpmyadmin    latest              e25cd4fd48b3        8 days ago          521 MB
mariadb               latest              dd208bafcc33        2 weeks ago         302.2 MB
</pre></div>
<div>And we can archive this image locally if we dont want to push it now to the Docker Hub:</div>
<div>
<pre class="ipsCode prettyprint">
bash-3.2$ docker save php-5.6-pw-celedev:0.11 | gzip &gt; php-5.6-pw-celedev-0.11.tar.gz </pre>
<p><strong>And that's it!</strong></p>

<p>You now have a portable image of your ProcessWire website that you can run directly on any docker-compatible system.</p>
</div>]]></description><guid isPermaLink="false">10736</guid><pubDate>Fri, 21 Aug 2015 16:03:02 +0000</pubDate></item><item><title>Contact Form tutorial</title><link>https://processwire.com/talk/topic/14206-contact-form-tutorial/</link><description><![CDATA[<p>
	<span style="font-size:26px;">Simple Contact Form</span><br>
	Using <strong>Google Recaptcha</strong> &amp; <strong>Valitron</strong> validation library
</p>

<p>
	<img class="ipsImage ipsImage_thumbnailed" data-fileid="8583" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt="1.png" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2016_09/1.png.7fe1a00742df80002aa5e0f6761c5dc4.png" width="393" data-ratio="113.74"></p>

<p>
	<img class="ipsImage ipsImage_thumbnailed" data-fileid="8584" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt="2.png" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2016_09/2.png.43e8d4a3edc280087b47e571b6d5a008.png" width="398" data-ratio="131.41"></p>

<p>
	<img class="ipsImage ipsImage_thumbnailed" data-fileid="8585" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt="3.png" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2016_09/3.png.d6b40e5d9e392dbb9326d853c99029fa.png" width="399" data-ratio="129.57"></p>

<p>
	I just finished creating a simple Contact-Form for a client's website so i thought it would be really helpfull to share it with the community.
</p>

<p>
	The contact form uses:
</p>

<ul><li>
		<strong>Google recaptcha</strong> for validating the user is not a robot (<a href="https://github.com/google/recaptcha" rel="external nofollow">https://github.com/google/recaptcha</a>)
	</li>
	<li>
		<strong>Valitron</strong> validation library for validating the form fields (<a href="https://github.com/vlucas/valitron" rel="external nofollow">https://github.com/vlucas/valitron</a>)
	</li>
	<li>
		<strong>Twitter Bootstrap 3.0.0</strong> for form HTML
	</li>
</ul><p>
	The contact-form is located inside a contact-page, so the bare minimum you need in order to setup your own is:
</p>

<ol><li>
		contact.php (template file used by your contact-page)
	</li>
	<li>
		 _contact-controller.php (file used as a controller for your contact-form functionality like send email, validate fields etc)
	</li>
	<li>
		2 extra lines inside your composer.json file
	</li>
</ol><p>
	<br>
	So, let's start:
</p>

<p>
	First you need to update your composer.json file adding 2 lines inside the require object:
</p>

<pre class="ipsCode prettyprint lang-javascript prettyprinted">
<span class="str">"vlucas/valitron"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"^1.2"</span><span class="pun">,</span><span class="pln">
</span><span class="str">"google/recaptcha"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"~1.1"</span></pre>

<p>
	Here is a sample composer.json file:
</p>

<pre class="ipsCode prettyprint lang-javascript prettyprinted">
<span class="pun">{</span><span class="pln">
  </span><span class="str">"name"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"processwire/processwire"</span><span class="pun">,</span><span class="pln">
  </span><span class="str">"type"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"library"</span><span class="pun">,</span><span class="pln">
  </span><span class="str">"description"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"ProcessWire CMS/CMF"</span><span class="pun">,</span><span class="pln">
  </span><span class="str">"keywords"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln"> </span><span class="str">"cms"</span><span class="pun">,</span><span class="str">"cmf"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"content management system"</span><span class="pln"> </span><span class="pun">],</span><span class="pln">
  </span><span class="str">"homepage"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"https://processwire.com"</span><span class="pun">,</span><span class="pln">
  </span><span class="str">"authors"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln">
    </span><span class="pun">{</span><span class="pln">
      </span><span class="str">"name"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"Ryan Cramer"</span><span class="pun">,</span><span class="pln">
      </span><span class="str">"email"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"ryan@processwire.com"</span><span class="pun">,</span><span class="pln">
      </span><span class="str">"homepage"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"https://processwire.com"</span><span class="pun">,</span><span class="pln">
      </span><span class="str">"role"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"Developer"</span><span class="pln">
    </span><span class="pun">}</span><span class="pln">
  </span><span class="pun">],</span><span class="pln">
  </span><span class="str">"require"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    </span><span class="str">"php"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"&gt;=5.3.8"</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"ext-gd"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"*"</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"vlucas/valitron"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"^1.2"</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"google/recaptcha"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"~1.1"</span><span class="pln">
  </span><span class="pun">},</span><span class="pln">
  </span><span class="str">"autoload"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    </span><span class="str">"files"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln"> </span><span class="str">"wire/core/ProcessWire.php"</span><span class="pln"> </span><span class="pun">]</span><span class="pln">
  </span><span class="pun">},</span><span class="pln">
  </span><span class="str">"minimum-stability"</span><span class="pun">:</span><span class="pln"> </span><span class="str">"dev"</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	open console and navigate to processwire root folder (where composer.json file is)<br>
	on this step i assume you have already setup composer for your project, otherwise google it <img alt=":)" data-emoticon="" height="20" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" srcset="https://processwire-forums.s3.us-west-2.amazonaws.com/emoticons/smile@2x.png 2x" title=":)" width="20" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/emoticons/default_smile.png"><br>
	run the following command:
</p>

<pre class="ipsCode">
composer update</pre>

<p>
	this will create a vendor folder (if it does not already exist) and download valitron and google recaptcha libraries.
</p>

<p>
	Then open your contact-page template file(usually named <strong>contact.php</strong> inside your templates directory) and add the following:<br>
	* Note: The form below uses <strong>bootstrap</strong> 3.0.0 css, so if you are using something else you need to make the appropriate changes.
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted">
<span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">namespace</span><span class="pln"> </span><span>ProcessWire</span><span class="pun">;</span><span class="pln">
include</span><span class="pun">(</span><span class="str">'_contact-controller.php'</span><span class="pun">)</span><span class="pln">
</span><span class="pun">?&gt;</span><span class="pln">
</span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"container"</span><span class="tag">&gt;</span><span class="pln">
	</span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"row"</span><span class="tag">&gt;</span><span class="pln">
		</span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">" col-md-4"</span><span class="tag">&gt;</span><span class="pln">
            </span><span class="tag">&lt;h2&gt;</span><span class="pln">Contact Form</span><span class="tag">&lt;/h2&gt;</span><span class="pln">

            </span><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$session</span><span class="pun">-&gt;</span><span class="pln">flashMessage</span><span class="pun">):?&gt;</span><span class="pln">
            &lt;div class="alert </span><span class="pun">&lt;?=!</span><span class="pln">$session</span><span class="pun">-&gt;</span><span class="pln">sent </span><span class="pun">&amp;&amp;</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">$v</span><span class="pun">-&gt;</span><span class="pln">validate</span><span class="pun">()</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> </span><span class="pun">!</span><span class="pln">$resp</span><span class="pun">-&gt;</span><span class="pln">isSuccess</span><span class="pun">())</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> </span><span class="str">'alert-danger'</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="str">'alert-success'</span><span class="pun">?&gt;</span><span class="pln">" role="alert"&gt;
              </span><span class="pun">&lt;?</span><span class="pln">php echo $session</span><span class="pun">-&gt;</span><span class="pln">flashMessage</span><span class="pun">;?&gt;</span><span class="pln">
            </span><span class="tag">&lt;/div&gt;</span><span class="pln">
            </span><span class="pun">&lt;?</span><span class="pln">php endif</span><span class="pun">;?&gt;</span><span class="pln">

            </span><span class="tag">&lt;form</span><span class="pln"> </span><span class="atn">id</span><span class="pun">=</span><span class="atv">"contact-form"</span><span class="pln"> </span><span class="atn">method</span><span class="pun">=</span><span class="atv">"post"</span><span class="tag">&gt;</span><span class="pln">
                &lt;div class="form-group </span><span class="pun">&lt;?=</span><span class="pln">$v</span><span class="pun">-&gt;</span><span class="pln">errors</span><span class="pun">(</span><span class="str">'name'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> </span><span class="str">'has-error'</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="str">''</span><span class="pun">?&gt;</span><span class="pln">"&gt;
                    </span><span class="tag">&lt;label</span><span class="pln"> </span><span class="atn">for</span><span class="pun">=</span><span class="atv">"name"</span><span class="tag">&gt;</span><span class="pln">Name</span><span class="tag">&lt;/label&gt;</span><span class="pln">
                    &lt;input class="form-control" name="name" id="name" type="text" value="</span><span class="pun">&lt;?=</span><span class="pln">$sanitizer</span><span class="pun">-&gt;</span><span class="pln">text</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">name</span><span class="pun">)?&gt;</span><span class="pln">"&gt;
                </span><span class="tag">&lt;/div&gt;</span><span class="pln">
                &lt;div class="form-group </span><span class="pun">&lt;?=</span><span class="pln">$v</span><span class="pun">-&gt;</span><span class="pln">errors</span><span class="pun">(</span><span class="str">'email'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> </span><span class="str">'has-error'</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="str">''</span><span class="pun">?&gt;</span><span class="pln">"&gt;
                    </span><span class="tag">&lt;label</span><span class="pln"> </span><span class="atn">for</span><span class="pun">=</span><span class="atv">"email"</span><span class="tag">&gt;</span><span class="pln">Email</span><span class="tag">&lt;/label&gt;</span><span class="pln">
                    &lt;input class="form-control" name="email" id="email" type="text" value="</span><span class="pun">&lt;?=</span><span class="pln">$sanitizer</span><span class="pun">-&gt;</span><span class="pln">text</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">email</span><span class="pun">)?&gt;</span><span class="pln">"&gt;
                </span><span class="tag">&lt;/div&gt;</span><span class="pln">
                &lt;div class="form-group </span><span class="pun">&lt;?=</span><span class="pln">$v</span><span class="pun">-&gt;</span><span class="pln">errors</span><span class="pun">(</span><span class="str">'message'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> </span><span class="str">'has-error'</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="str">''</span><span class="pun">?&gt;</span><span class="pln">"&gt;
                    </span><span class="tag">&lt;label</span><span class="pln"> </span><span class="atn">for</span><span class="pun">=</span><span class="atv">"message"</span><span class="tag">&gt;</span><span class="pln">Message</span><span class="tag">&lt;/label&gt;</span><span class="pln">
                    </span><span class="tag">&lt;textarea</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"form-control"</span><span class="pln"> </span><span class="atn">name</span><span class="pun">=</span><span class="atv">"message"</span><span class="pln"> </span><span class="atn">id</span><span class="pun">=</span><span class="atv">"message"</span><span class="tag">&gt;</span><span class="pun">&lt;?=</span><span class="pln">$sanitizer</span><span class="pun">-&gt;</span><span class="pln">text</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">message</span><span class="pun">)?&gt;</span><span class="tag">&lt;/textarea&gt;</span><span class="pln">
                </span><span class="tag">&lt;/div&gt;</span><span class="pln">
                </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"form-group"</span><span class="tag">&gt;</span><span class="pln">

                    </span><span class="com">&lt;!-- Google Recaptcha code START --&gt;</span><span class="pln">
                    &lt;div class="g-recaptcha" data-sitekey="</span><span class="pun">&lt;?=</span><span class="pln">$googleSiteKey</span><span class="pun">?&gt;</span><span class="pln">"&gt;</span><span class="tag">&lt;/div&gt;</span><span class="pln">
                    </span><span class="tag">&lt;script</span><span class="pln"> </span><span class="atn">type</span><span class="pun">=</span><span class="atv">"text/javascript"</span><span class="pln">
                            </span><span class="atn">src</span><span class="pun">=</span><span class="atv">"https://www.google.com/recaptcha/api.js"</span><span class="tag">&gt;</span><span class="pln">
                    </span><span class="tag">&lt;/script&gt;</span><span class="pln">
                    </span><span class="com">&lt;!-- Google Recaptcha code END --&gt;</span><span class="pln">

                </span><span class="tag">&lt;/div&gt;</span><span class="pln">
                </span><span class="tag">&lt;button</span><span class="pln"> </span><span class="atn">type</span><span class="pun">=</span><span class="atv">"submit"</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"btn btn-primary"</span><span class="tag">&gt;</span><span class="pln">SEND</span><span class="tag">&lt;/button&gt;</span><span class="pln">
            </span><span class="tag">&lt;/form&gt;</span><span class="pln">
      	</span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="tag">&lt;/div&gt;</span><span class="pln">
</span><span class="tag">&lt;/div&gt;</span><span class="pln">
</span><span class="pun">&lt;?</span><span class="pln">php
    </span><span class="com">//here we remove the flash-message because it is already shown above the form.</span><span class="pln">
    $session</span><span class="pun">-&gt;</span><span class="pln">remove</span><span class="pun">(</span><span class="str">'flashMessage'</span><span class="pun">);</span><span class="pln">
    </span><span class="com">//reset 'sent' variable for future submit</span><span class="pln">
	$session</span><span class="pun">-&gt;</span><span class="pln">sent </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">;</span><span class="pln">
</span><span class="pun">?&gt;</span></pre>

<p>
	<br>
	Next create a file inside you templates directory with name: <strong>_contact-controller.php</strong>:<br>
	and set the required variables($googleSiteKey, $contactFormRecipient, $contactPageID)
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted">
<span class="pun">&lt;?</span><span class="pln">php
</span><span class="kwd">namespace</span><span class="pln"> </span><span>ProcessWire</span><span class="pun">;</span><span class="pln">

</span><span class="com">/**
*    here we include Valitron &amp; Google recaptcha libraries
*    make sure the path is correct in your template
*/</span><span class="pln">
include</span><span class="pun">(</span><span class="pln">dirname</span><span class="pun">(</span><span class="pln">__FILE__</span><span class="pun">)</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> </span><span class="str">"/../../vendor/vlucas/valitron/src/Valitron/Validator.php"</span><span class="pun">);</span><span class="pln">
include</span><span class="pun">(</span><span class="pln">dirname</span><span class="pun">(</span><span class="pln">__FILE__</span><span class="pun">)</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> </span><span class="str">'/../../vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php'</span><span class="pun">);</span><span class="pln">

</span><span class="com">/**
*    here we add the form field values to Valitron
*/</span><span class="pln">
$v </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">new</span><span class="pln"> \Valitron\Validator</span><span class="pun">(</span><span class="pln">array</span><span class="pun">(</span><span class="pln">
        </span><span class="str">'name'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">text</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">name</span><span class="pun">),</span><span class="pln">
        </span><span class="str">'email'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">email</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">email</span><span class="pun">),</span><span class="pln">
        </span><span class="str">'message'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">text</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">message</span><span class="pun">),</span><span class="pln">
    </span><span class="pun">)</span><span class="pln">
</span><span class="pun">);</span><span class="pln">

</span><span class="com">/**
*    validation rules set for each form field
*    For more details on Valitron/Validator usage visit: 
*    https://github.com/vlucas/valitron
*/</span><span class="pln">
$v</span><span class="pun">-&gt;</span><span class="pln">rule</span><span class="pun">(</span><span class="str">'required'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">[</span><span class="str">'name'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'email'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'message'</span><span class="pun">]);</span><span class="pln">
$v</span><span class="pun">-&gt;</span><span class="pln">rule</span><span class="pun">(</span><span class="str">'lengthMin'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'name'</span><span class="pun">,</span><span class="pln"> </span><span class="lit">5</span><span class="pun">);</span><span class="pln">
$v</span><span class="pun">-&gt;</span><span class="pln">rule</span><span class="pun">(</span><span class="str">'email'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'email'</span><span class="pun">);</span><span class="pln">

</span><span class="com">/**
*    set Google recaptcha site-key &amp; secret-key
*    create a new key from: https://www.google.com/recaptcha/admin
*/</span><span class="pln">
$googleSiteKey </span><span class="pun">=</span><span class="pln"> </span><span class="str">'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'</span><span class="pun">;</span><span class="pln">
$googleSecretKey </span><span class="pun">=</span><span class="pln"> </span><span class="str">'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY'</span><span class="pun">;</span><span class="pln">

</span><span class="com">/**
*    set the email of the contact form recipient(usually the website owner)
*/</span><span class="pln">
$contactFormRecipient </span><span class="pun">=</span><span class="pln"> </span><span class="str">'your@company.com'</span><span class="pun">;</span><span class="pln">

</span><span class="com">/**
*    set the id of contact-page in order to redirect there when the form is sent
*/</span><span class="pln">
$contactPageID </span><span class="pun">=</span><span class="pln"> </span><span class="str">'1045'</span><span class="pun">;</span><span class="pln">

</span><span class="com">//here we check whether the 'name' field exists inside post variables (which means the form is posted)</span><span class="pln">
</span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">name</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

    </span><span class="com">//if fields validation passes</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$v</span><span class="pun">-&gt;</span><span class="pln">validate</span><span class="pun">())</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

        $reCaptcha </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">new</span><span class="pln"> \ReCaptcha\ReCaptcha</span><span class="pun">(</span><span class="pln">$googleSecretKey</span><span class="pun">);</span><span class="pln">
        $resp </span><span class="pun">=</span><span class="pln"> $reCaptcha</span><span class="pun">-&gt;</span><span class="pln">verify</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;{</span><span class="str">'g-recaptcha-response'</span><span class="pun">},</span><span class="pln"> $_SERVER</span><span class="pun">[</span><span class="str">"REMOTE_ADDR"</span><span class="pun">]);</span><span class="pln">

        </span><span class="com">//if google-recaptcha validation passes</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$resp</span><span class="pun">-&gt;</span><span class="pln">isSuccess</span><span class="pun">())</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        
            </span><span class="com">//This is the HTML message</span><span class="pln">
            $message </span><span class="pun">=</span><span class="pln"> </span><span class="str">'
                &lt;html&gt;
                    &lt;head&gt;
                        &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&gt;
                        &lt;title&gt;Contact Form | '</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">name </span><span class="pun">.</span><span class="pln"> </span><span class="str">'&lt;/title&gt;
                    &lt;/head&gt;
                    &lt;body&gt;
                        &lt;p&gt;'</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">message </span><span class="pun">.</span><span class="pln"> </span><span class="str">'&lt;/p&gt;
                    &lt;/body&gt;
                &lt;/html&gt;'</span><span class="pun">;</span><span class="pln">

            </span><span class="com">//here we send the form to $contactFormRecipient</span><span class="pln">
            wireMail</span><span class="pun">(</span><span class="pln">$contactFormRecipient</span><span class="pun">,</span><span class="pln"> $input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">email</span><span class="pun">,</span><span class="pln"> </span><span class="str">"Contact Form | "</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $input</span><span class="pun">-&gt;</span><span class="pln">post</span><span class="pun">-&gt;</span><span class="pln">name</span><span class="pun">,</span><span class="pln"> $message</span><span class="pun">);</span><span class="pln">
            
            </span><span class="com">//here we set a flash-message to notify the user that the form was successfully sent</span><span class="pln">
            $session</span><span class="pun">-&gt;</span><span class="pln">flashMessage </span><span class="pun">=</span><span class="pln"> </span><span class="str">'Thank you for your message! We will get in touch with you shortly.'</span><span class="pun">;</span><span class="pln">
            
            </span><span class="com">//save in session that the form is sent</span><span class="pln">
			$session</span><span class="pun">-&gt;</span><span class="pln">sent </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">;</span><span class="pln">

            </span><span class="com">//finally redirect user to contact-page</span><span class="pln">
            $session</span><span class="pun">-&gt;</span><span class="pln">redirect</span><span class="pun">(</span><span class="pln">$pages</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="pln">$contactPageID</span><span class="pun">)-&gt;</span><span class="pln">url</span><span class="pun">);</span><span class="pln">
        </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            </span><span class="com">//self explain</span><span class="pln">
            $session</span><span class="pun">-&gt;</span><span class="pln">flashMessage </span><span class="pun">=</span><span class="pln"> </span><span class="str">'Error while validating you are not a robot!'</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
    </span><span class="pun">}</span><span class="pln">
</span><span class="pun">}</span><span class="pln">
</span><span class="pun">?&gt;</span></pre>

<p>
	<strong>Thats all!</strong><br>
	You now have a simple contact-form working with <u>captcha</u> and field <u>validation</u>!<br>
	I would be more than happy to help anyone having problems on the setup.<br>
	 
</p>]]></description><guid isPermaLink="false">14206</guid><pubDate>Tue, 06 Sep 2016 19:26:30 +0000</pubDate></item><item><title>How to make a Lister / ListerPro Lister load much faster on a site with both many fields and many filters</title><link>https://processwire.com/talk/topic/31957-how-to-make-a-lister-listerpro-lister-load-much-faster-on-a-site-with-both-many-fields-and-many-filters/</link><description><![CDATA[<p>
	My print-on-demand webapp is very complex.  It has:
</p>

<ul>
	<li>
		hundreds of fields
	</li>
	<li>
		tens of thousands of pages (let's focus on orders)
	</li>
	<li>
		listers (again, let's focus on my "Orders" lister)
	</li>
	<li>
		many filters on listers (my Orders lister has about 20 filters)
	</li>
</ul>

<p>
	When you load a ListerPro lister page, it technically does 2 requests:
</p>

<ul>
	<li>
		first request is to build the interface
	</li>
	<li>
		second request (ajax) loads the results based on the filters, etc.
	</li>
</ul>

<p>
	I noticed it became very slow over time, but I didn't look into it deeply until now.
</p>

<p>
	Even though I have thousands of order pages, that's actually not the issue.
</p>

<p>
	First, part of the issue is what type of page reference fields you are using, which I wrote a <a href="https://processwire.com/talk/topic/31778--" rel="">tutorial</a> about, so make sure to view that as well. (also keep in mind TracyDebugger slows things down a lot here as well)
</p>

<p>
	But even with that, it's still slow on the first request.  It would take <strong>8-14</strong> seconds to load a ListerPro page on the first request!
</p>

<p>
	The issue?  Having too many filters (20 for example) combined with the fact my site has hundreds of fields means that generating that filters list is incredibly intensive.  That filters list is powered by the InputfieldSelector field.
</p>

<p>
	I worked with DeepSeek and it came up with a simple solution: when rendering the field list (ie, the first dropdown in InputfieldSelector), don't show every single field as an option... instead only grab the fields that I have actually used.  This keeps the InputfieldSelector field working correctly, but eliminates a bunch of fields I wouldn't have wanted to use in my filters anyway.  So, we're sacrificing a little (unneeded) flexibility for extreme performance gain. Now my ListerPro page loads in less than a second!
</p>

<p>
	Use this hook in ready.php:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="kwd">if</span><span class="pun">(</span><span class="pln">wire</span><span class="pun">(</span><span class="str">'page'</span><span class="pun">)-&gt;</span><span class="pln">process </span><span class="pun">==</span><span class="pln"> </span><span class="str">'ProcessPageListerPro'</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> </span><span class="pun">!</span><span class="pln">wire</span><span class="pun">(</span><span class="str">'config'</span><span class="pun">)-&gt;</span><span class="pln">ajax </span><span class="pun">&amp;&amp;</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'input'</span><span class="pun">)-&gt;</span><span class="pln">urlSegment1 </span><span class="pun">!==</span><span class="pln"> </span><span class="str">'config'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  
    wire</span><span class="pun">()-&gt;</span><span class="pln">addHookBefore</span><span class="pun">(</span><span class="str">'InputfieldSelector::render'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="pln">$event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        $is </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">object</span><span class="pun">;</span><span class="pln">
        
        </span><span class="com">// Don't interfere if limitFields was already explicitly configured</span><span class="pln">
        $existing </span><span class="pun">=</span><span class="pln"> $is</span><span class="pun">-&gt;</span><span class="pln">limitFields</span><span class="pun">;</span><span class="pln">
        </span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">empty</span><span class="pun">(</span><span class="pln">$existing</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
        
        </span><span class="com">// Parse field names from the current selector value</span><span class="pln">
        $value </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">string</span><span class="pun">)</span><span class="pln"> $is</span><span class="pun">-&gt;</span><span class="pln">attr</span><span class="pun">(</span><span class="str">'value'</span><span class="pun">);</span><span class="pln">
        </span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">strlen</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
        
        $fieldNames </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[];</span><span class="pln">
        </span><span class="kwd">try</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            $selectors </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="kwd">new</span><span class="pln"> \ProcessWire\Selectors</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">));</span><span class="pln">
            </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$selectors </span><span class="kwd">as</span><span class="pln"> $s</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$s</span><span class="pun">-&gt;</span><span class="pln">fields </span><span class="kwd">as</span><span class="pln"> $field</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                    </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$field</span><span class="pun">,</span><span class="pln"> </span><span class="str">'.'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                        list</span><span class="pun">(</span><span class="pln">$field</span><span class="pun">,)</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> explode</span><span class="pun">(</span><span class="str">'.'</span><span class="pun">,</span><span class="pln"> $field</span><span class="pun">,</span><span class="pln"> </span><span class="lit">2</span><span class="pun">);</span><span class="pln">
                    </span><span class="pun">}</span><span class="pln">
                    $fieldNames</span><span class="pun">[</span><span class="pln">$field</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $field</span><span class="pun">;</span><span class="pln">
                </span><span class="pun">}</span><span class="pln">
            </span><span class="pun">}</span><span class="pln">
        </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">catch</span><span class="pun">(</span><span class="pln">\Exception $e</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
        
        </span><span class="com">// Always include system fields (needed for the template row)</span><span class="pln">
        </span><span class="kwd">foreach</span><span class="pun">([</span><span class="str">'id'</span><span class="pun">,</span><span class="str">'name'</span><span class="pun">,</span><span class="str">'title'</span><span class="pun">,</span><span class="str">'template'</span><span class="pun">,</span><span class="str">'parent'</span><span class="pun">,</span><span class="str">'status'</span><span class="pun">,</span><span class="pln">
                 </span><span class="str">'created'</span><span class="pun">,</span><span class="str">'modified'</span><span class="pun">,</span><span class="str">'published'</span><span class="pun">,</span><span class="str">'path'</span><span class="pun">,</span><span class="str">'has_parent'</span><span class="pun">,</span><span class="pln">
                 </span><span class="str">'created_users_id'</span><span class="pun">,</span><span class="str">'modified_users_id'</span><span class="pun">,</span><span class="str">'num_children'</span><span class="pun">,</span><span class="pln">
                 </span><span class="str">'count'</span><span class="pun">,</span><span class="str">'include'</span><span class="pun">,</span><span class="str">'limit'</span><span class="pun">,</span><span class="str">'sort'</span><span class="pun">,</span><span class="str">'_custom'</span><span class="pun">]</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $f</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            $fieldNames</span><span class="pun">[</span><span class="pln">$f</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $f</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
        
        </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">count</span><span class="pun">(</span><span class="pln">$fieldNames</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            $is</span><span class="pun">-&gt;</span><span class="kwd">set</span><span class="pun">(</span><span class="str">'limitFields'</span><span class="pun">,</span><span class="pln"> array_values</span><span class="pun">(</span><span class="pln">$fieldNames</span><span class="pun">));</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
    </span><span class="pun">});</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	 
</p>
]]></description><guid isPermaLink="false">31957</guid><pubDate>Tue, 30 Jun 2026 19:57:54 +0000</pubDate></item><item><title>Recommended way for creating a page reference field (using "select" inputfield) for repeater pages and avoiding unnecessary page loads</title><link>https://processwire.com/talk/topic/31778-recommended-way-for-creating-a-page-reference-field-using-select-inputfield-for-repeater-pages-and-avoiding-unnecessary-page-loads/</link><description><![CDATA[<p>
	There may be a time where you need to create a page reference field using the Select inputfield and it's selecting repeater pages.
</p>

<p>
	Let's say I have a repeater field called "order_line_items" and I want to create a page reference field called "order_line_item" that allows me to select a repeater item (which is a page) of the "order_line_items" repeater field.
</p>

<p>
	Repeater pages are a bit different from regular pages in that their "parent" is a container admin page associated with the page in which it exists (dig into /admin/repeaters/order_line_items/ in your page tree to see what I mean).  So when you are configuring your page reference field, you can't really choose a Parent.  However when configuring your field, your instinct would be to choose the Template of "repeater_order_line_items".
</p>

<p>
	Then because you need extra precision in what pages are actually available for selection (rather than all of them across all pages), your instinct will be to implement custom PHP code:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">$wire</span><span class="pun">-&gt;</span><span class="pln">addHookAfter</span><span class="pun">(</span><span class="str">'InputfieldPage::getSelectablePages'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="pln">$event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$event</span><span class="pun">-&gt;</span><span class="kwd">object</span><span class="pun">-&gt;</span><span class="pln">hasField </span><span class="pun">==</span><span class="pln"> </span><span class="str">'order_line_item'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    $event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">pages</span><span class="pun">-&gt;</span><span class="pln">find</span><span class="pun">(</span><span class="str">'your selector here'</span><span class="pun">);</span><span class="pln">
  </span><span class="pun">}</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	The problem with that approach is that even though you have defined the custom PHP code and the select field correctly shows the selectable repeater pages in the select field, behind-the-scenes, ProcessWire has still loaded EVERY SINGLE REPEATER that has the "repeater_order_line_items" template (you can see this is TracyDebugger's pages loaded list)!  Your site will definitely be slower as a result, dramatically so if you have thousands or tens of thousands of repeater pages of that template.
</p>

<p>
	I hit this issue years ago (2018) and I thought it was a bug.  I discussed it with Ryan and it's technically not a bug, but kind of the way ProcessWire works, which is beyond this tutorial.
</p>

<p>
	While you can circumvent this using the PageAutocomplete field, I don't like the ergonomics of that field in certain situations.  I want the good-old select field.
</p>

<p>
	The solution to this is to <strong><u>NOT</u></strong> select anything for the "Template" when configuring your field.  So in my example, I chose "repeater_order_line_items", but instead, it should be left blank.
</p>

<p>
	Now the field will just rely on the code portion and all the unnecessary page loads will be eliminated.
</p>
]]></description><guid isPermaLink="false">31778</guid><pubDate>Wed, 18 Mar 2026 16:10:25 +0000</pubDate></item><item><title>Automated PNG compression</title><link>https://processwire.com/talk/topic/31763-automated-png-compression/</link><description><![CDATA[<p>
	Not really sure this is a tutorial, but if you're struggling with large PNG files, here is a nice little hook to compress them with pngquant.
</p>

<p>
	Obviously you need to install pngquant (<a href="https://pngquant.org/" rel="external nofollow">https://pngquant.org/</a>) first and exec() can't be disabled.
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">$wire-&gt;addHookAfter('Pageimage::size', function(HookEvent $event) {
    $img = $event-&gt;return; // the resized Pageimage variation
    $path = $img-&gt;filename;
    
    // Only process PNGs
    if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'png') return;
    
    // Skip if already optimized (optional marker file approach)
    $marker = $path . '.pngquant';
    if (file_exists($marker)) return;
    
    // Run pngquant (overwrites in place, quality 65-80)
    $cmd = sprintf(
        'pngquant --quality=65-80 --force --output %s -- %s 2&gt;&amp;1',
        escapeshellarg($path),
        escapeshellarg($path)
    );
    exec($cmd, $output, $returnCode);
    
    // Mark as optimized so we don't re-process on subsequent calls
    if ($returnCode === 0 || $returnCode === 99) {
        // 99 = quality target not met, kept original
        touch($marker);
    }
});</span></pre>

<p>
	 
</p>
]]></description><guid isPermaLink="false">31763</guid><pubDate>Sun, 08 Mar 2026 23:33:17 +0000</pubDate></item><item><title>Single-File Loader for ProcessWire</title><link>https://processwire.com/talk/topic/31625-single-file-loader-for-processwire/</link><description><![CDATA[<p>
	Hi everyone,
</p>

<p>
	First of all I had no idea, which category would fit best ...
</p>

<p>
	I'd like to share a little tool I've been working on to make the initial setup of ProcessWire even faster, especially when working on remote servers without SSH access.
</p>

<p>
	<b>What is it?</b> <code>kickstart.php</code> is a modern, single-file installer/loader for ProcessWire. Instead of uploading thousands of files via FTP, you just upload this one file and it handles the rest.
</p>

<p>
	<b>Key Features:</b>
</p>

<ul>
	<li>
		<p>
			<b>Version Selection:</b> Choose between the <code>master</code> (stable) or <code>dev</code> branch directly from GitHub.
		</p>
	</li>
	<li>
		<p>
			<b>Smart Multi-Language:</b> Built-in support for English, German, Spanish, and French (with automatic browser language detection).
		</p>
	</li>
	<li>
		<p>
			<b>Modern UI:</b> Built with Tailwind CSS, AlpineJS, and smooth animations using Anime.js.
		</p>
	</li>
	<li>
		<p>
			<b>Pre-flight Checks:</b> Automatically checks for PHP version requirements and prevents overwriting existing installations.
		</p>
	</li>
	<li>
		<p>
			<b>Automatic Cleanup:</b> Removes the downloaded ZIP archive and temporary folders after extraction.
		</p>
	</li>
</ul>

<p>
	<b>How to use it:</b>
</p>

<ol start="1">
	<li>
		<p>
			Upload <code>kickstart.php</code> to your webroot.
		</p>
	</li>
	<li>
		<p>
			Open it in your browser.
		</p>
	</li>
	<li>
		<p>
			Choose your version and click install.
		</p>
	</li>
	<li>
		<p>
			Once finished, click the button to start the official ProcessWire installer.
		</p>
	</li>
</ol>

<p>
	I hope some of you find this useful for your workflow! Feedback and suggestions are always welcome.
</p>

<p>
	Cheers, Markus
</p>

<p>
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="28660" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2026_01/image.png.b31a36e35d36687bf0ecab54b6e425d3.png" rel=""><img alt="image.thumb.png.82118dc57b191ff6feb396cc7b297e6c.png" class="ipsImage ipsImage_thumbnailed" data-fileid="28660" data-ratio="123.76" style="height:auto;" width="606" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2026_01/image.thumb.png.82118dc57b191ff6feb396cc7b297e6c.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" /></a>
</p>

<p>
	<a class="ipsAttachLink" data-fileext="php" data-fileid="28659" href="https://processwire.com/talk/applications/core/interface/file/attachment.php?id=28659&amp;key=31b993ff3e94b9cdae7809c4993aacbf" rel="">kickstart.php</a>
</p>

<p>
	Improved Version now available on GitHub: <a href="https://github.com/markusthomas/ProcessWireKickstart" rel="external nofollow">https://github.com/markusthomas/ProcessWireKickstart</a>
</p>
]]></description><guid isPermaLink="false">31625</guid><pubDate>Sat, 17 Jan 2026 15:23:32 +0000</pubDate></item><item><title>Using .env files with ProcessWire</title><link>https://processwire.com/talk/topic/31086-using-env-files-with-processwire/</link><description><![CDATA[<p>
	Hello!
</p>

<p>
	I use .env files on every ProcessWire project to manage environment-specific configurations and settings. I've built a ProcessWire specific utility that makes using .env files a breeze. This post isn't intended to debate .env vs. config.php, use what you're comfortable with and prefer. That said, here are a few benefits to using .env files that may make it worth considering:
</p>

<ul>
	<li>
		Native support on web servers, including Apache, they are not served via http request by default
	</li>
	<li>
		True environment based secrets and settings management
	</li>
	<li>
		A standard file widely used and accepted as the method for managing secrets and sensitive values
	</li>
	<li>
		Able to store any value whether sensitive or not and access them globally
	</li>
</ul>

<p>
	Building a dedicated solution came from a <a href="https://processwire.com/talk/topic/29940-enhancing-security-and-efficiency-in-processwire-installations/" rel="">discussion here on the forums</a> where I threw together a rough implementation that needed little polish for real world use. It makes use of <a href="https://github.com/vlucas/phpdotenv/" rel="external nofollow">phpdotenv</a>.
</p>

<p>
	This utility delivers the following:
</p>

<ul>
	<li>
		Easy use of and access to .env variables
	</li>
	<li>
		Caching the parsed .env for performance. This is a significant part of this utility and <a href="https://github.com/vlucas/phpdotenv/issues/549" rel="external nofollow">addresses a known need</a>
	</li>
	<li>
		Automatic .env change recognition and re-caching
	</li>
	<li>
		Utilities to make working with environment variables feel ProcessWire native and a few extra nifty things
	</li>
</ul>

<p>
	What it isn't:
</p>

<ul>
	<li>
		A module. It's not possible to make a module for this need because the information kept in a .env file needs to be available <em>before</em> ProcessWire boots.
	</li>
</ul>

<p>
	Adding this to a new or existing project is very easy. It's designed to implement quickly and use immediately in your projects. Full documentation is provided in the <a href="https://github.com/SkyLundy/ProcessWire-Env-Config" rel="external nofollow">Github repository</a>.
</p>

<p>
	Here are a few examples of using this tool:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">namespace</span><span class="pln"> </span><span class="typ">ProcessWire</span><span class="pun">;</span><span class="pln">

</span><span class="kwd">use</span><span class="pln"> </span><span class="typ">Env</span><span class="pln">\Env</span><span class="pun">;</span><span class="pln">

</span><span class="kwd">if</span><span class="pun">(!</span><span class="kwd">defined</span><span class="pun">(</span><span class="str">"PROCESSWIRE"</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">die</span><span class="pun">();</span><span class="pln">

$env </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Env</span><span class="pun">::</span><span class="pln">load</span><span class="pun">(</span><span class="pln">__DIR__ </span><span class="pun">.</span><span class="pln"> </span><span class="str">'/../'</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Make env available throughout the application</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">env </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">;</span><span class="pln">

$config</span><span class="pun">-&gt;</span><span class="pln">dbName </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'DB_NAME'</span><span class="pun">);</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">dbUser </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'DB_USER'</span><span class="pun">);</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">dbPass </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'DB_PASS'</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Env::get() takes a second argument that is the fallback value if for any reason DEBUG doesn't exist </span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">debug </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'DEBUG'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Conditional values. By default, if the condition is falsey, Env::if() returns null   </span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">adminEmail </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">if</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'production'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'you@youremail.com'</span><span class="pun">);</span><span class="pln">

</span><span class="com">// A fourth argument will be returned if condition is false, truthy/falsey output can be env var names or specific values</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">adminEmail </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">if</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'production'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ADMIN_EMAIL'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'you@youremail.com'</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Conversely, you can also check if a condition is not met.</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">adminEmail </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="pln">ifNot</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'development'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ADMIN_EMAIL'</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Use one env value to set multiple config properties</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">advanced </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">if</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'production'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ENABLE_ADVANCED'</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Never in production, change locally in env as needed</span><span class="pln">
$config</span><span class="pun">-&gt;</span><span class="pln">adminEmail </span><span class="pun">=</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="pln">ifNot</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'development'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ADMIN_EMAIL'</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Never send an email in dev, always on staging/production</span></pre>

<p>
	These helper methods make is very straightforward to implement a dynamic config file. This can be useful for using secure .env values while retaining the ability to commit and upload some types of changes to your config.php file without needing to touch .env values on the server.
</p>

<p>
	You can also use Env::pushToConfig(). As long as you use the "screaming snake case" naming convention for your environment variable names, type and value recognition are handled automatically.
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php

$env</span><span class="pun">-&gt;</span><span class="pln">pushToConfig</span><span class="pun">(</span><span class="pln">$config</span><span class="pun">,</span><span class="pln"> </span><span class="pun">[</span><span class="pln">
    </span><span class="str">'usePageClasses'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">,</span><span class="pln">
    </span><span class="str">'templateCompile'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="str">'TEMPLATE_COMPILE'</span><span class="pun">,</span><span class="pln">
    </span><span class="str">'debug'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="str">'DEBUG'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">],</span><span class="pln"> </span><span class="com">// Fallback to false</span><span class="pln">
    </span><span class="str">'advanced'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="kwd">if</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'production'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ENABLE_ADVANCED'</span><span class="pun">),</span><span class="pln">
    </span><span class="str">'adminEmail'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $env</span><span class="pun">-&gt;</span><span class="pln">ifNot</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'development'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'ADMIN_EMAIL'</span><span class="pun">),</span><span class="pln">
    </span><span class="str">'httpHosts'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="pln">
        </span><span class="str">'something.com'</span><span class="pun">,</span><span class="pln">
      	</span><span class="str">'staging.something.com'</span><span class="pun">,</span><span class="pln">
        </span><span class="str">'something.ddev.site'</span><span class="pln">
    </span><span class="pun">],</span><span class="pln">
</span><span class="pun">]);</span></pre>

<p>
	Using Env in your application files and templates can be very useful. In the above example we assigned the Env object to $config-&gt;env. This lets you access your .env variables globally and use some helpful methods.
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">env</span><span class="pun">-&gt;</span><span class="pln">eq</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'development'</span><span class="pun">)):</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
  </span><span class="tag">&lt;script</span><span class="pln"> </span><span class="atn">src</span><span class="pun">=</span><span class="atv">"/some/development/stuff.js"</span><span class="tag">&gt;&lt;/script&gt;</span><span class="pln">
</span><span class="pun">&lt;?</span><span class="pln">php endif </span><span class="pun">?&gt;</span><span class="pln">

</span><span class="pun">&lt;?</span><span class="pln">php
</span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">env</span><span class="pun">-&gt;</span><span class="pln">exists</span><span class="pun">(</span><span class="str">'GOOGLE_API_KEY'</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  $wire</span><span class="pun">-&gt;</span><span class="pln">error</span><span class="pun">(</span><span class="str">'A Google API key could not be loaded from the environment file.'</span><span class="pun">);</span><span class="pln">
</span><span class="pun">}</span><span class="pln">

</span><span class="kwd">try</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="com">// Do something that could fail	</span><span class="pln">
</span><span class="pun">}</span><span class="pln"> </span><span class="kwd">catch</span><span class="pln"> </span><span class="pun">(</span><span class="typ">Exception</span><span class="pln"> $e</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    $message </span><span class="pun">=</span><span class="pln"> $config</span><span class="pun">-&gt;</span><span class="pln">env</span><span class="pun">-&gt;</span><span class="kwd">if</span><span class="pun">(</span><span class="str">'APP_ENV'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'production'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'Oh no. Friendly message here'</span><span class="pun">,</span><span class="pln"> $e</span><span class="pun">-&gt;</span><span class="pln">getMessage</span><span class="pun">());</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	This utility also automatically casts 'true' and 'false' values in .env files to booleans, and casts numbers to integers. It also includes several configuration options.
</p>

<p>
	I have been using this tool in production and have been happy with it. Maybe you might find it helpful in your projects as well. If you like it, throw a star on the repo. If you run into any bugs, file an issue on Github. I may publish it as a composer package at some point.
</p>

<p>
	<a href="https://github.com/SkyLundy/ProcessWire-Env-Config" rel="external nofollow">Env utility for ProcessWIre on Github.</a>
</p>
]]></description><guid isPermaLink="false">31086</guid><pubDate>Mon, 03 Mar 2025 17:15:54 +0000</pubDate></item><item><title>Turn Your Boring ProcessWire Emails into Beautiful Masterpieces! &#x1F3A8;</title><link>https://processwire.com/talk/topic/31017-turn-your-boring-processwire-emails-into-beautiful-masterpieces-%F0%9F%8E%A8/</link><description><![CDATA[<p>
	Ever felt like your ProcessWire emails look like they're stuck in 1999? You know the drill - sending emails is super easy with WireMail:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">$m </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">new</span><span class="pln"> </span><span class="typ">WireMail</span><span class="pun">();</span><span class="pln">
$m</span><span class="pun">-&gt;</span><span class="kwd">from</span><span class="pun">(</span><span class="str">'foo@bar.com'</span><span class="pun">);</span><span class="pln">
$m</span><span class="pun">-&gt;</span><span class="pln">to</span><span class="pun">(</span><span class="str">'xxx@yyy.com'</span><span class="pun">);</span><span class="pln">
$m</span><span class="pun">-&gt;</span><span class="pln">subject</span><span class="pun">(</span><span class="str">'Hello there!'</span><span class="pun">);</span><span class="pln">
$m</span><span class="pun">-&gt;</span><span class="pln">bodyHTML</span><span class="pun">(</span><span class="str">'&lt;h1&gt;This is great!&lt;/h1&gt;&lt;p&gt;I am an ugly mail...&lt;/p&gt;'</span><span class="pun">);</span><span class="pln">
$m</span><span class="pun">-&gt;</span><span class="pln">send</span><span class="pun">();</span></pre>

<p>
	But let's be honest - they look about as pretty as a website built with Microsoft FrontPage! <span class="ipsEmoji">😅</span>
</p>

<p>
	<img alt="Mv8sVRE.png" class="ipsImage" data-ratio="75.08" height="689" style="height:auto;width:500px;" width="1000" data-src="https://i.imgur.com/Mv8sVRE.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	<strong><span class="ipsEmoji">🪄</span> Enter the Mail Pimp Hook!</strong>
</p>

<p>
	Drop this magical hook into your /site/ready.php (or even better Site.module.php), and watch your emails transform from ugly ducklings into beautiful swans:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php
$wire</span><span class="pun">-&gt;</span><span class="pln">addHookBefore</span><span class="pun">(</span><span class="str">'WireMail::send'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="typ">HookEvent</span><span class="pln"> $event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="com">// double check that we got a wiremail instance</span><span class="pln">
  </span><span class="com">// this also tells the IDE what $mail is (to get IntelliSense)</span><span class="pln">
  $mail </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">object</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">$mail </span><span class="kwd">instanceof</span><span class="pln"> </span><span class="typ">WireMail</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
  
  </span><span class="com">// get current mail body</span><span class="pln">
  $html </span><span class="pun">=</span><span class="pln"> $mail</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'bodyHTML'</span><span class="pun">);</span><span class="pln">
  </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">$html</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
  
  </span><span class="com">// get email layout markup</span><span class="pln">
  $layoutFile </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">()-&gt;</span><span class="pln">config</span><span class="pun">-&gt;</span><span class="pln">paths</span><span class="pun">-&gt;</span><span class="pln">templates </span><span class="pun">.</span><span class="pln"> </span><span class="str">'mails/default.html'</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">is_file</span><span class="pun">(</span><span class="pln">$layoutFile</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
  
  </span><span class="com">// replace ##content## with actual mail content</span><span class="pln">
  $html </span><span class="pun">=</span><span class="pln"> str_replace</span><span class="pun">(</span><span class="pln">
    </span><span class="str">'##content##'</span><span class="pun">,</span><span class="pln">
    $html</span><span class="pun">,</span><span class="pln">
    wire</span><span class="pun">()-&gt;</span><span class="pln">files</span><span class="pun">-&gt;</span><span class="pln">render</span><span class="pun">(</span><span class="pln">$layoutFile</span><span class="pun">)</span><span class="pln">
  </span><span class="pun">);</span><span class="pln">
  
  </span><span class="com">// write new body to mail</span><span class="pln">
  $mail</span><span class="pun">-&gt;</span><span class="pln">bodyHTML</span><span class="pun">(</span><span class="pln">$html</span><span class="pun">);</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	 
</p>

<p>
	<img alt="yKOVv4D.png" class="ipsImage" data-ratio="75.08" height="729" style="height:auto;width:500px;" width="1000" data-src="https://i.imgur.com/yKOVv4D.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	<strong>The HTML</strong>
</p>

<p>
	Just create a beautiful MJML template at /site/templates/mails/default.mjml, put ##content## where your email content should go, convert it to HTML and BOOM! <span class="ipsEmoji">💥</span> Every email gets automatically wrapped in your gorgeous template.
</p>

<p>
	No more CSS wrestling matches, no more "Why does this look different in Outlook?" headaches. Just pure email beauty, automagically! <span class="ipsEmoji">✨</span>
</p>

<p>
	Now your clients will think you spent days crafting those emails, when in reality, you're sipping coffee while your hook does all the heavy lifting. Work smarter, not harder! <span class="ipsEmoji">🚀</span>
</p>

<p>
	#ProcessWire #EmailMagic #NoMoreUglyEmails
</p>

<p>
	PS: This is the MJML template that I used:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="tag">&lt;mjml&gt;</span><span class="pln">
  </span><span class="tag">&lt;mj-head&gt;</span><span class="pln">
    </span><span class="tag">&lt;mj-attributes&gt;</span><span class="pln">
      </span><span class="tag">&lt;mj-all</span><span class="pln"> </span><span class="atn">font-family</span><span class="pun">=</span><span class="atv">"Tahoma"</span><span class="pln"> </span><span class="tag">/&gt;</span><span class="pln">
      </span><span class="tag">&lt;mj-text</span><span class="pln"> </span><span class="atn">line-height</span><span class="pun">=</span><span class="atv">"140%"</span><span class="pln"> </span><span class="tag">/&gt;</span><span class="pln">
    </span><span class="tag">&lt;/mj-attributes&gt;</span><span class="pln">
  </span><span class="tag">&lt;/mj-head&gt;</span><span class="pln">
  </span><span class="tag">&lt;mj-body</span><span class="pln"> </span><span class="atn">background-color</span><span class="pun">=</span><span class="atv">"#efefef"</span><span class="tag">&gt;</span><span class="pln">
    </span><span class="tag">&lt;mj-section</span><span class="pln">
      </span><span class="atn">background-color</span><span class="pun">=</span><span class="atv">"#ffffff"</span><span class="pln">
      </span><span class="atn">background-repeat</span><span class="pun">=</span><span class="atv">"repeat"</span><span class="pln">
      </span><span class="atn">padding-bottom</span><span class="pun">=</span><span class="atv">"30px"</span><span class="pln">
      </span><span class="atn">padding-top</span><span class="pun">=</span><span class="atv">"30px"</span><span class="pln">
      </span><span class="atn">text-align</span><span class="pun">=</span><span class="atv">"center"</span><span class="pln">
    </span><span class="tag">&gt;</span><span class="pln">
      </span><span class="tag">&lt;mj-column&gt;</span><span class="pln">
        </span><span class="tag">&lt;mj-image</span><span class="pln">
          </span><span class="atn">align</span><span class="pun">=</span><span class="atv">"center"</span><span class="pln">
          </span><span class="atn">padding</span><span class="pun">=</span><span class="atv">"25px"</span><span class="pln">
          </span><span class="atn">src</span><span class="pun">=</span><span class="atv">"xxx"</span><span class="pln">
          </span><span class="atn">target</span><span class="pun">=</span><span class="atv">"_blank"</span><span class="pln">
          </span><span class="atn">width</span><span class="pun">=</span><span class="atv">"200px"</span><span class="pln">
          </span><span class="atn">alt</span><span class="pun">=</span><span class="atv">"Logo"</span><span class="pln">
        </span><span class="tag">&gt;&lt;/mj-image&gt;</span><span class="pln">
        </span><span class="tag">&lt;mj-text&gt;</span><span class="pln">##content##</span><span class="tag">&lt;/mj-text&gt;</span><span class="pln">
      </span><span class="tag">&lt;/mj-column&gt;</span><span class="pln">
    </span><span class="tag">&lt;/mj-section&gt;</span><span class="pln">
    </span><span class="tag">&lt;mj-section&gt;</span><span class="pln">
      </span><span class="tag">&lt;mj-column&gt;</span><span class="pln">
        </span><span class="tag">&lt;mj-text</span><span class="pln">
          </span><span class="atn">font-size</span><span class="pun">=</span><span class="atv">"10px"</span><span class="pln">
          </span><span class="atn">color</span><span class="pun">=</span><span class="atv">"#a0a0a0"</span><span class="pln">
          </span><span class="atn">align</span><span class="pun">=</span><span class="atv">"center"</span><span class="pln">
        </span><span class="tag">&gt;</span><span class="pln">
          powered by
          </span><span class="tag">&lt;a</span><span class="pln">
            </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"https://www.baumrock.com/"</span><span class="pln">
            </span><span class="atn">style</span><span class="pun">=</span><span class="atv">"</span><span class="kwd">color</span><span class="pun">:</span><span class="pln"> </span><span class="lit">#158f66</span><span class="atv">"</span><span class="pln">
            </span><span class="tag">&gt;</span><span class="pln">baumrock.com</span><span class="tag">&lt;/a</span><span class="pln">
          </span><span class="tag">&gt;</span><span class="pln">
        </span><span class="tag">&lt;/mj-text&gt;</span><span class="pln">
      </span><span class="tag">&lt;/mj-column&gt;</span><span class="pln">
    </span><span class="tag">&lt;/mj-section&gt;</span><span class="pln">
  </span><span class="tag">&lt;/mj-body&gt;</span><span class="pln">
</span><span class="tag">&lt;/mjml&gt;</span></pre>

<p>
	VSCode has an extension to get a live preview and export MJML to HTML:
</p>

<p>
	<img alt="HylErcF.png" class="ipsImage" data-ratio="75.08" height="500" style="height:auto;" width="1000" data-src="https://i.imgur.com/HylErcF.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	And here are some other free templates: <a href="https://mjml.io/templates" rel="external nofollow">https://mjml.io/templates</a>
</p>

<p>
	I use <a href="https://www.base64-image.de/" rel="external nofollow">https://www.base64-image.de/</a> to add the logo to my mail template as src="data:image/jpeg;base64,/9j/4QAWRXhpZgAATU0AKgAAAA..." to avoid headaches with image paths, remote assets blocking etc.
</p>
]]></description><guid isPermaLink="false">31017</guid><pubDate>Thu, 06 Feb 2025 14:10:22 +0000</pubDate></item><item><title>Precautions when rendering repeater item fields in the page editor in a custom way and correctly saving field values</title><link>https://processwire.com/talk/topic/31513-precautions-when-rendering-repeater-item-fields-in-the-page-editor-in-a-custom-way-and-correctly-saving-field-values/</link><description><![CDATA[<p>
	Let's say you have a repeater field.  ProcessWire renders it in the default ProcessWire way in the page editor, which is fine, however there are times where you may want to render a particular repeater field in a more streamlined way, like a table, <em>while maintaining</em> the ability for the user to make changes to field values of the respective repeater items.
</p>

<p>
	The problem with this however if that you must be very careful because if you do not represent a field of that repeater (and represent it correctly with the correct form "name"), then ProcessWire will wipe out the data because it expects it to exist when saving!!!  This includes the "native" fields for a repeater item (which are "loaded", "sort" and "publish").  THOSE MUST ALSO BE REPRESENTED!
</p>

<p>
	For fields in your repeater that are hidden however, they do not need to be represented, so you can ignore rendering those.
</p>

<p>
	You have to be <em><strong>very</strong></em> careful here because you may one day decide to add a new field to a repeater and keep it as "Open", and forget this requirement and not add it to the your custom renderer.  Then if you wrote data to the field in some way, and then a user went to edit a page that contains that repeater field and they save, your data will get wiped out!
</p>
]]></description><guid isPermaLink="false">31513</guid><pubDate>Sat, 25 Oct 2025 16:53:27 +0000</pubDate></item><item><title>Adding a clone button to Page Edit</title><link>https://processwire.com/talk/topic/24845-adding-a-clone-button-to-page-edit/</link><description><![CDATA[<p>
	If you have the core <a href="https://processwire.com/blog/posts/processwire-2.6.19-plus-guide-to-optional-core-modules/#page-clone-processpageclone" rel="">ProcessPageClone</a> module installed you can copy a page via Page List:
</p>

<p>
	<img alt="2020-12-21_150048.png.95af4987daf701c2fbd11d919a2f1bd5.png" class="ipsImage ipsImage_thumbnailed" data-fileid="20653" data-ratio="11.96" width="903" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2020_12/2020-12-21_150048.png.95af4987daf701c2fbd11d919a2f1bd5.png"></p>

<p>
	 
</p>

<p>
	But sometimes I don't want to hunt around through Page List to find the page I want to copy, and instead I have found the page via the admin search and have it open in Page Edit. So I used this hook in /site/ready.php to add a "Clone this page" button to the bottom of the Settings tab.
</p>

<p>
	<img alt="2020-12-21_150341.png.55582c1011e29f3755732d0e95435e62.png" class="ipsImage ipsImage_thumbnailed" data-fileid="20654" data-ratio="70.12" width="482" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2020_12/2020-12-21_150341.png.55582c1011e29f3755732d0e95435e62.png"></p>

<pre class="ipsCode prettyprint lang-php prettyprinted">
<span class="com">// Add a clone button to the Settings tab of Page Edit if this page is allowed to be cloned</span><span class="pln">
$wire</span><span class="pun">-&gt;</span><span class="pln">addHookAfter</span><span class="pun">(</span><span class="str">'ProcessPageEdit::buildFormSettings'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="typ">HookEvent</span><span class="pln"> $event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
	$wrapper </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
	$modules </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">wire</span><span class="pun">()-&gt;</span><span class="pln">modules</span><span class="pun">;</span><span class="pln">
	$page </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">process</span><span class="pun">-&gt;</span><span class="pln">getPage</span><span class="pun">();</span><span class="pln">
	</span><span class="com">/** @var ProcessPageClone $ppc */</span><span class="pln">
	$ppc </span><span class="pun">=</span><span class="pln"> $modules</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'ProcessPageClone'</span><span class="pun">);</span><span class="pln">
	</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$page </span><span class="pun">&amp;&amp;</span><span class="pln"> $ppc</span><span class="pun">-&gt;</span><span class="pln">hasPermission</span><span class="pun">(</span><span class="pln">$page</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
		</span><span class="com">/** @var InputfieldButton $f */</span><span class="pln">
		$f </span><span class="pun">=</span><span class="pln"> $modules</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'InputfieldButton'</span><span class="pun">);</span><span class="pln">
		$f</span><span class="pun">-&gt;</span><span class="kwd">value</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> </span><span class="str">'Clone this page'</span><span class="pun">;</span><span class="pln">
		$f</span><span class="pun">-&gt;</span><span class="pln">href </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">wire</span><span class="pun">()-&gt;</span><span class="pln">config</span><span class="pun">-&gt;</span><span class="pln">urls</span><span class="pun">-&gt;</span><span class="pln">admin </span><span class="pun">.</span><span class="pln"> </span><span class="str">'page/clone/?id='</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $page</span><span class="pun">-&gt;</span><span class="pln">id</span><span class="pun">;</span><span class="pln">
		$wrapper</span><span class="pun">-&gt;</span><span class="kwd">add</span><span class="pun">(</span><span class="pln">$f</span><span class="pun">);</span><span class="pln">
	</span><span class="pun">}</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	Maybe somebody else has this need and finds this useful.
</p>]]></description><guid isPermaLink="false">24845</guid><pubDate>Mon, 21 Dec 2020 02:06:23 +0000</pubDate></item><item><title>Tutorial: Browser sync or Live reload (small footprint)</title><link>https://processwire.com/talk/topic/31028-tutorial-browser-sync-or-live-reload-small-footprint/</link><description><![CDATA[<p>
	This hook is used to monitor file changes and page updates in your ProcessWire project. It operates when <strong>debug mode is active</strong> and <strong>returns file changes</strong> and <strong>latest page update</strong> information in <strong>JSON</strong> format.
</p>

<p>
	The script hook is automatically injected before the <strong>&lt;/body&gt;</strong> tag in your <strong>HTML</strong>. It periodically checks for any file or page updates in your system at the interval you specify. If the previous check time is older than the new check time (indicating changes), the page automatically refreshes.
</p>

<p>
	<strong>~/site/ready.php</strong>
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">if ($wire-&gt;config-&gt;debug) {

    $wire-&gt;addHookAfter('Page::render', function (HookEvent $event) {
        $script = &lt;&lt;&lt;HTML
        </span><span class="tag">&lt;script&gt;</span><span class="pln">
        </span><span class="kwd">const</span><span class="pln"> browserSync </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            mtime</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln">
            init</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                </span><span class="kwd">this</span><span class="pun">.</span><span class="pln">sync</span><span class="pun">();</span><span class="pln">
            </span><span class="pun">},</span><span class="pln">
            sync</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                fetch</span><span class="pun">(</span><span class="str">'/browser-sync'</span><span class="pun">)</span><span class="pln">
                </span><span class="pun">.</span><span class="pln">then</span><span class="pun">(</span><span class="pln">response </span><span class="pun">=&gt;</span><span class="pln"> response</span><span class="pun">.</span><span class="pln">json</span><span class="pun">())</span><span class="pln">
                </span><span class="pun">.</span><span class="pln">then</span><span class="pun">(</span><span class="pln">data </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="kwd">this</span><span class="pun">.</span><span class="pln">mtime</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                        </span><span class="kwd">this</span><span class="pun">.</span><span class="pln">mtime </span><span class="pun">=</span><span class="pln"> data</span><span class="pun">.</span><span class="pln">max</span><span class="pun">;</span><span class="pln">
                    </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                        </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">data</span><span class="pun">.</span><span class="pln">max </span><span class="pun">&gt;</span><span class="pln"> </span><span class="kwd">this</span><span class="pun">.</span><span class="pln">mtime</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
                            location</span><span class="pun">.</span><span class="pln">reload</span><span class="pun">();</span><span class="pln">
                            </span><span class="pun">}</span><span class="pln">
                        </span><span class="pun">}</span><span class="pln">
                    </span><span class="pun">});</span><span class="pln">
                </span><span class="pun">}</span><span class="pln">
            </span><span class="pun">};</span><span class="pln">
            browserSync</span><span class="pun">.</span><span class="pln">init</span><span class="pun">();</span><span class="pln">
            setInterval</span><span class="pun">(()</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> browserSync</span><span class="pun">.</span><span class="pln">sync</span><span class="pun">(),</span><span class="pln"> </span><span class="lit">2500</span><span class="pun">);</span><span class="pln">
        </span><span class="tag">&lt;/script&gt;</span><span class="pln">
        HTML;

        $event-&gt;return = str_replace('</span><span class="tag">&lt;/body&gt;</span><span class="pln">', "{$script}</span><span class="tag">&lt;/body&gt;</span><span class="pln">", $event-&gt;return);
    });

    $wire-&gt;addHook('/browser-sync', function (HookEvent $event) {

        $wire = $event-&gt;wire();
        $root = $wire-&gt;config-&gt;paths-&gt;root;
        $siteRoot = $wire-&gt;config-&gt;paths-&gt;site;
        $extensions = '*.{php,js,css}';

        $paths = [
            "{$root}src/{$extensions}", // custom composer package
            "{$root}src/src/{$extensions}", // custom composer package
            "{$siteRoot}{$extensions}", // ready.php, init.php, finished.php
            "{$siteRoot}classes/{$extensions}", // page classes
            "{$siteRoot}templates/{$extensions}",
            "{$siteRoot}templates/*/{$extensions}",
            "{$siteRoot}templates/*/*/{$extensions}",
            "{$siteRoot}templates/*/*/*/{$extensions}",
            "{$siteRoot}templates/*/*/*/*/{$extensions}",
        ];

        $files = [];
        foreach ($paths as $pattern) {
            // Try with GLOB_BRACE first
            $result = glob($pattern, GLOB_NOSORT | GLOB_BRACE);
            // If no results with GLOB_BRACE, try without it
            if (!$result) {
                $result = glob($pattern, GLOB_NOSORT) ?: [];
            }
            $files = array_merge($files, $result);
        }

        $filemtime = 0;
        foreach ($files as $file) {
            if (!is_file($file)) {
                continue;
            }
            try {
                $mtime = @filemtime($file);
                if ($mtime &amp;&amp; $mtime &gt; $filemtime) {
                    $filemtime = $mtime;
                }
            } catch (\Exception $e) {
                continue;
            }
        }

        $latest = $wire-&gt;pages-&gt;get('sort=-created|-modified');
        $latestTime = $latest-&gt;modified &gt; $latest-&gt;created ? $latest-&gt;modified : $latest-&gt;created;
        
        header('Content-Type: application/javascript');
        echo json_encode([
            'max' =&gt; max($filemtime, $latestTime),
            'mtime' =&gt; $filemtime,
            'modified' =&gt; $latest-&gt;modified,
            'created' =&gt; $latest-&gt;created
        ]);
        exit;
    });
}</span></pre>

<p>
	This development tool helps streamline your workflow by automatically refreshing the browser when you make changes to your files or content, eliminating the need for manual page refreshes during development.
</p>
]]></description><guid isPermaLink="false">31028</guid><pubDate>Mon, 10 Feb 2025 22:09:29 +0000</pubDate></item><item><title>InputfieldDatetime Minimum and Maximum</title><link>https://processwire.com/talk/topic/31361-inputfielddatetime-minimum-and-maximum/</link><description><![CDATA[<p>
	I had set an <a href="https://processwire.com/api/ref/inputfield-datetime/" rel="external">InputfieldDatetime</a> field's setting of "yearRange" to "0:+2" and apparently 0, for the number of years <em>before</em> the current year simply is defined as <em>no limit</em>. (This should work for similar datetime field settings that offer a range.)
</p>

<p>
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="28249" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2025_07/image.png.db0743e481bd6601371fdd72f9187950.png" rel=""><img alt="image.thumb.png.59a12f3d1dd21bda8f8dd6a4ec1d0bbd.png" class="ipsImage ipsImage_thumbnailed" data-fileid="28249" data-ratio="19.40" style="height:auto;" width="1000" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2025_07/image.thumb.png.59a12f3d1dd21bda8f8dd6a4ec1d0bbd.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" /></a>
</p>

<p>
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="28250" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2025_07/image.png.04b3c553591b1318afb0156a27b95554.png" rel=""><img alt="image.thumb.png.344f8fc80b193a04e2580a3ac6da2f9e.png" class="ipsImage ipsImage_thumbnailed" data-fileid="28250" data-ratio="272.73" style="height:auto;" width="275" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2025_07/image.thumb.png.344f8fc80b193a04e2580a3ac6da2f9e.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" /></a>
</p>

<p>
	The formatting expectation for the jQuery-UI datetime picker component is a string value that always has a negative value, a colon separator, and a positive value, indicated by "-x:+y".
</p>

<p>
	The solution here is simple, but in case anyone else runs into this problem, it just takes the following form instead:
</p>

<pre class="ipsCode">yearRange="-0:+2"</pre>

<p>
	Simply include the negative sign in front of the zero! <span class="ipsEmoji">🤦‍♂️</span>
</p>

<p>
	<img alt="image.png.cab07ad354c30adb003de0ede6798694.png" class="ipsImage ipsImage_thumbnailed" data-fileid="28251" data-ratio="97.78" style="height:auto;" width="270" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2025_07/image.png.cab07ad354c30adb003de0ede6798694.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>
]]></description><guid isPermaLink="false">31361</guid><pubDate>Tue, 15 Jul 2025 15:02:51 +0000</pubDate></item><item><title>No-Module Color Picker (One Simple Hook)</title><link>https://processwire.com/talk/topic/31178-no-module-color-picker-one-simple-hook/</link><description><![CDATA[<p>
	Ever needed a color picker on some kind of settings page? Didn't want to install and setup a full-blown colorpicker module? Here's a quick and dirty hook to change a regular text field into an &lt;input type="color"&gt; type of input:
</p>

<p>
	Before:
</p>

<p>
	<img alt="IVR83Bx.png" class="ipsImage" data-ratio="29.70" height="259" style="height:auto;" width="872" data-src="https://i.imgur.com/IVR83Bx.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	After:
</p>

<p>
	<img alt="bhifzzr.png" class="ipsImage" data-ratio="43.99" height="399" style="height:auto;" width="907" data-src="https://i.imgur.com/bhifzzr.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php

</span><span class="kwd">public</span><span class="pln"> </span><span class="kwd">function</span><span class="pln"> init</span><span class="pun">():</span><span class="pln"> </span><span class="kwd">void</span><span class="pln">
</span><span class="pun">{</span><span class="pln">
  wire</span><span class="pun">()-&gt;</span><span class="pln">addHookBefore</span><span class="pun">(</span><span class="str">'Inputfield::render'</span><span class="pun">,</span><span class="pln"> $this</span><span class="pun">,</span><span class="pln"> </span><span class="str">'changeFieldType'</span><span class="pun">);</span><span class="pln">
</span><span class="pun">}</span><span class="pln">

</span><span class="kwd">public</span><span class="pln"> </span><span class="kwd">function</span><span class="pln"> changeFieldType</span><span class="pun">(</span><span class="typ">HookEvent</span><span class="pln"> $event</span><span class="pun">):</span><span class="pln"> </span><span class="kwd">void</span><span class="pln">
</span><span class="pun">{</span><span class="pln">
  $f </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">object</span><span class="pun">;</span><span class="pln">
  $colorFields </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[</span><span class="pln">
    </span><span class="typ">Site</span><span class="pun">::</span><span class="pln">field_col_primary</span><span class="pun">,</span><span class="pln">
    </span><span class="typ">Site</span><span class="pun">::</span><span class="pln">field_col_secondary</span><span class="pun">,</span><span class="pln">
    </span><span class="typ">Site</span><span class="pun">::</span><span class="pln">field_contrast_primary</span><span class="pun">,</span><span class="pln">
    </span><span class="typ">Site</span><span class="pun">::</span><span class="pln">field_contrast_secondary</span><span class="pun">,</span><span class="pln">
  </span><span class="pun">];</span><span class="pln">
  </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">in_array</span><span class="pun">(</span><span class="pln">$f</span><span class="pun">-&gt;</span><span class="pln">name</span><span class="pun">,</span><span class="pln"> $colorFields</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
  $f</span><span class="pun">-&gt;</span><span class="pln">attr</span><span class="pun">(</span><span class="str">'type'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'color'</span><span class="pun">);</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	So right before the text input is rendered we change its "type" property to "color" and the browser will render a default color picker <span class="ipsEmoji">😎</span>
</p>

<p>
	It once more shows how versatile ProcessWire is. And maybe it helps someone... <span><span class="ipsEmoji">🙂</span> </span>
</p>

<p>
	<span>PS: Be advised that with that hack you only modify the optics of the field. The field will under the hood still be a regular text field, which means you'll not get any sanitisation or such from it and you might have to take care of that on your own. In my case it's a superuser-only settings page. So it is no issue at all.</span>
</p>
]]></description><guid isPermaLink="false">31178</guid><pubDate>Sun, 06 Apr 2025 16:50:44 +0000</pubDate></item><item><title>Using the "download" attribute of Anchor (link) element for custom file names</title><link>https://processwire.com/talk/topic/31150-using-the-download-attribute-of-anchor-link-element-for-custom-file-names/</link><description><![CDATA[<p>
	Some time ago, I learned you can add the "download" attribute to a link to force a browser to download the file when its clicked, like this:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="tag">&lt;a</span><span class="pln"> </span><span class="atn">download</span><span class="pln"> </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"my-awesome-file.jpg"</span><span class="tag">&gt;</span><span class="pln">Download</span><span class="tag">&lt;/a&gt;</span></pre>

<p>
	What I didn't realize until today is that you can actually <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download" rel="external nofollow">specify a filename as a value for the download attribute</a> like this which will automatically use that filename instead!:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="tag">&lt;a</span><span class="pln"> </span><span class="atn">download</span><span class="pun">=</span><span class="atv">"my___awesome___file.jpg"</span><span class="pln"> </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"my-awesome-file.jpg"</span><span class="tag">&gt;</span><span class="pln">Download</span><span class="tag">&lt;/a&gt;</span></pre>

<p>
	This is incredibly convenient because it means if I want a user to download the same exact file 3 times but with specific filenames for each, I can just do this:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="tag">&lt;a</span><span class="pln"> </span><span class="atn">download</span><span class="pun">=</span><span class="atv">"my___awesome___file-1.jpg"</span><span class="pln"> </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"my-awesome-file.jpg"</span><span class="tag">&gt;</span><span class="pln">Download 1</span><span class="tag">&lt;/a&gt;</span><span class="pln">
</span><span class="tag">&lt;a</span><span class="pln"> </span><span class="atn">download</span><span class="pun">=</span><span class="atv">"my___awesome___file-2.jpg"</span><span class="pln"> </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"my-awesome-file.jpg"</span><span class="tag">&gt;</span><span class="pln">Download 2</span><span class="tag">&lt;/a&gt;</span><span class="pln">
</span><span class="tag">&lt;a</span><span class="pln"> </span><span class="atn">download</span><span class="pun">=</span><span class="atv">"my___awesome___file-3.jpg"</span><span class="pln"> </span><span class="atn">href</span><span class="pun">=</span><span class="atv">"my-awesome-file.jpg"</span><span class="tag">&gt;</span><span class="pln">Download 3</span><span class="tag">&lt;/a&gt;</span></pre>

<p>
	Furthermore, as you can see, I am using a triple underscore which ProcessWire cleans up and changes to a single underscore, as well as other changes (such as lowercasing everything) when uploading to a pagefile field.  I want my filenames to be exactly as I upload and I know there's ways to prevent ProcessWire from doing that, but using the download attribute in the way I described works perfectly for my use case.
</p>
]]></description><guid isPermaLink="false">31150</guid><pubDate>Thu, 27 Mar 2025 17:06:29 +0000</pubDate></item><item><title>Alphabetical pager nav</title><link>https://processwire.com/talk/topic/31103-alphabetical-pager-nav/</link><description><![CDATA[<p>
	Prompted by <a contenteditable="false" data-ipshover="" data-ipshover-target="https://processwire.com/talk/profile/2-ryan/?do=hovercard" data-mentionid="2" href="https://processwire.com/talk/profile/2-ryan/" rel="">@ryan</a>'s recent enhancement of the page tree display, I wanted to display an alphabetical index of pages rather than a numerical one, so I built a hook after MarkupPagerNav::render. You can see the result here: <a href="https://ncorchards.co.uk/main/fruit-orchards/apples/" rel="external nofollow">https://ncorchards.co.uk/main/fruit-orchards/apples/</a>
</p>

<p>
	Basically the hook just replaces render() with a modified version. The key modifications are to select all the items without limit:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">		$items </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">arguments</span><span class="pun">(</span><span class="lit">0</span><span class="pun">);</span><span class="pln">  </span><span class="com">//PageArray</span><span class="pln">
		$itemSelector </span><span class="pun">=</span><span class="pln"> $items</span><span class="pun">-&gt;</span><span class="pln">getSelectors</span><span class="pun">(</span><span class="kwd">true</span><span class="pun">);</span><span class="pln">  </span><span class="com">// true returns the string rather than the object</span><span class="pln">
		$noLimit </span><span class="pun">=</span><span class="pln"> preg_replace</span><span class="pun">(</span><span class="str">'/,\s*limit=-?\d+/i'</span><span class="pun">,</span><span class="pln"> </span><span class="str">''</span><span class="pun">,</span><span class="pln"> $itemSelector</span><span class="pun">);</span></pre>

<p>
	and then to modify the pager:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">		$pagerNav </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">new</span><span class="pln"> </span><span class="typ">PagerNav</span><span class="pun">(</span><span class="pln">$object</span><span class="pun">-&gt;</span><span class="pln">totalItems</span><span class="pun">,</span><span class="pln"> $object</span><span class="pun">-&gt;</span><span class="pln">itemsPerPage</span><span class="pun">,</span><span class="pln"> $object</span><span class="pun">-&gt;</span><span class="pln">pageNum</span><span class="pun">);</span><span class="pln">
		$pagerNav</span><span class="pun">-&gt;</span><span class="pln">setLabels</span><span class="pun">(</span><span class="pln">$objectOptions</span><span class="pun">[</span><span class="str">'previousItemLabel'</span><span class="pun">],</span><span class="pln"> $objectOptions</span><span class="pun">[</span><span class="str">'nextItemLabel'</span><span class="pun">]);</span><span class="pln">
		$pagerNav</span><span class="pun">-&gt;</span><span class="pln">setNumPageLinks</span><span class="pun">(</span><span class="pln">$object</span><span class="pun">-&gt;</span><span class="pln">numPageLinks</span><span class="pun">);</span><span class="pln">
		$pager </span><span class="pun">=</span><span class="pln"> $pagerNav</span><span class="pun">-&gt;</span><span class="pln">getPager</span><span class="pun">();</span><span class="pln">
		$allItems </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-&gt;</span><span class="pln">find</span><span class="pun">(</span><span class="pln">$noLimit</span><span class="pun">)-&gt;</span><span class="pln">explode</span><span class="pun">(</span><span class="str">'title'</span><span class="pun">);</span><span class="pln">
		$itemsPerPage </span><span class="pun">=</span><span class="pln"> $object</span><span class="pun">-&gt;</span><span class="pln">itemsPerPage</span><span class="pun">;</span><span class="pln">
		</span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$pager </span><span class="kwd">as</span><span class="pln"> $key </span><span class="pun">=&gt;</span><span class="pln"> $item</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
			</span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">ctype_digit</span><span class="pun">(</span><span class="str">"$item-&gt;label"</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span><span class="pln">
			$index </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">int</span><span class="pun">)</span><span class="pln"> $item</span><span class="pun">-&gt;</span><span class="pln">label</span><span class="pun">;</span><span class="pln">
			$itemTitle </span><span class="pun">=</span><span class="pln"> $allItems</span><span class="pun">[(</span><span class="pln">$index </span><span class="pun">-</span><span class="pln"> </span><span class="lit">1</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> $itemsPerPage</span><span class="pun">];</span><span class="pln">
			$item</span><span class="pun">-&gt;</span><span class="pln">label </span><span class="pun">=</span><span class="pln"> substr</span><span class="pun">(</span><span class="pln">$itemTitle</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">2</span><span class="pun">);</span><span class="pln">
		</span><span class="pun">}</span></pre>

<p>
	If anyone wants the full hook code, let me know, but you will probably want to modify it for your own use anyway and the above are the key bits.
</p>
]]></description><guid isPermaLink="false">31103</guid><pubDate>Sun, 09 Mar 2025 20:32:10 +0000</pubDate></item><item><title>Ajax-driven autocomplete search with native JavaScript</title><link>https://processwire.com/talk/topic/29348-ajax-driven-autocomplete-search-with-native-javascript/</link><description><![CDATA[<p>
	In this tutorial I want to show you how to set up a simple ajax-driven auto-complete search within ProcessWire.
</p>

<p>
	Something that looks like this:
</p>

<p>
	<img alt="image.gif.eca8c40f28098bac68fe1c7dfce7c050.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="25688" data-ratio="64.38" style="height:auto;" width="800" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2023_12/image.gif.eca8c40f28098bac68fe1c7dfce7c050.gif" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	<b>Requirements:</b>
</p>

<p>
	<strong>1. Use the Fieldtype Cache to create a search index</strong>
</p>

<p>
	For fast and easy search queries we will create a search index field. You can read more on this here: <a href="https://processwire.recipes/recipes/set-up-search-index-with-fieldtypecache/" rel="external nofollow">https://processwire.recipes/recipes/set-up-search-index-with-fieldtypecache/</a>
</p>

<ol style="background-color:#ffffff;border:0px solid #e5e7eb;color:#374151;font-size:16px;padding:0px 0px 0px 1.625em;">
	<li style="border:0px solid #e5e7eb;">
		First of all, go to Modules and install a Core module called FieldtypeCache.
	</li>
	<li style="border:0px solid #e5e7eb;">
		Add a new field “search_cache” and select “Cache” as it’s type. Save the field.
	</li>
	<li style="border:0px solid #e5e7eb;">
		On the Details tab choose the fields you want to make searchable. Save again.
	</li>
	<li style="border:0px solid #e5e7eb;">
		Add “search_cache” to all templates you want to include in your search results.
	</li>
	<li style="border:0px solid #e5e7eb;">
		Optional but recommended: use the “Regenerate Cache” option found from the Details tab of the field to make existing content instantly searchable.
	</li>
</ol>

<p>
	In my case i only want to search inside body and title fields, so I included those fields in the settings of the search cache field:
</p>

<p>
	<img alt="image.png.b22ea1df8bed315409b6174151842c9e.png" class="ipsImage ipsImage_thumbnailed" data-fileid="25689" data-ratio="54.36" style="height:auto;" width="688" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2023_12/image.png.b22ea1df8bed315409b6174151842c9e.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	<strong>2. Install the Pages2JSON module</strong>
</p>

<p>
	We want to make ajax requests to the search template and those results should be returned in JSON format so that our java script can process the results inside the auto-complete dropdown. Therefore we make use of this module here:
</p>

<p>
	<a href="https://processwire.com/modules/pages2-json/" rel="external">https://processwire.com/modules/pages2-json/</a>
</p>

<p>
	In the module settings we define what data will be included in the JSON object that is returned. Remember that this is the data that we want to display in our auto-complete dropdown. So for my case I only want the title and the url field.
</p>

<p>
	<img alt="image.png.f8e48a2ce1287860784e5b1195321542.png" class="ipsImage ipsImage_thumbnailed" data-fileid="25690" data-ratio="31.75" style="height:auto;" width="756" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2023_12/image.png.f8e48a2ce1287860784e5b1195321542.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	 
</p>

<p>
	<strong>Now let's start:</strong>
</p>

<p>
	<strong>1. Setting up the search template</strong>
</p>

<p>
	Set up a template file called „search“. Then create a page using this template. On most ProcessWire installations this is already the case and this template exits in the file system. This template will handle the search queries and list all the results for us and that in two ways:
</p>

<p>
	- you can send a search query and the search result page will list all the results (as you would expect)
</p>

<p>
	- you can send an ajax search query and the result page will return the results as a json object to be processed within javascript and displayed in the frontend in real-time
</p>

<p>
	<strong>search.php:</strong>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php
</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">ajax</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    </span><span class="com">// Return search results in JSON format</span><span class="pln">
    $q </span><span class="pun">=</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">selectorValue</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">-&gt;</span><span class="pln">q</span><span class="pun">);</span><span class="pln">
    $results </span><span class="pun">=</span><span class="pln"> $pages</span><span class="pun">-&gt;</span><span class="pln">find</span><span class="pun">(</span><span class="str">'search_cache%='</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $q</span><span class="pun">);;</span><span class="pln"> </span><span class="com">// Find all pages and save as $results</span><span class="pln">
    header</span><span class="pun">(</span><span class="str">"Content-type: application/json"</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Set header to JSON</span><span class="pln">
    echo $results</span><span class="pun">-&gt;</span><span class="pln">toJSON</span><span class="pun">();</span><span class="pln"> </span><span class="com">// Output the results as JSON via the toJSON function</span><span class="pln">
    </span><span class="kwd">return</span><span class="pln"> $this</span><span class="pun">-&gt;</span><span class="pln">halt</span><span class="pun">();</span><span class="pln">
</span><span class="pun">}</span><span class="pln">
</span><span class="pun">?&gt;</span><span class="pln">
</span><span class="tag">&lt;main&gt;</span><span class="pln">
  </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-container"</span><span class="tag">&gt;</span><span class="pln">
    </span><span class="pun">&lt;?</span><span class="pln">php

    </span><span class="com">// look for a GET variable named 'q' and sanitize it</span><span class="pln">
    $q </span><span class="pun">=</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">selectorValue</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">-&gt;</span><span class="pln">q</span><span class="pun">);</span><span class="pln">

    </span><span class="com">// did $q have anything in it?</span><span class="pln">
    </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$q</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

      </span><span class="com">// Find pages that match the selector</span><span class="pln">
      $matches </span><span class="pun">=</span><span class="pln"> $pages</span><span class="pun">-&gt;</span><span class="pln">find</span><span class="pun">(</span><span class="str">'search_cache%='</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $q</span><span class="pun">);</span><span class="pln">

      </span><span class="com">// did we find any matches? ...</span><span class="pln">
      </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$matches</span><span class="pun">-&gt;</span><span class="pln">count</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

        echo </span><span class="str">"&lt;h2&gt;We found $matches-&gt;count results:&lt;/h2&gt;"</span><span class="pun">;</span><span class="pln">

        echo </span><span class="str">"&lt;ul class='uk-list uk-list-square'&gt;"</span><span class="pun">;</span><span class="pln">

        </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$matches </span><span class="kwd">as</span><span class="pln"> $match</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          echo </span><span class="str">"&lt;li&gt;&lt;a href='$match-&gt;url'&gt;$match-&gt;title&lt;/a&gt;"</span><span class="pun">;</span><span class="pln">
          echo </span><span class="str">"&lt;div class='summary'&gt;$match-&gt;summary&lt;/div&gt;&lt;/li&gt;"</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">

        echo </span><span class="str">"&lt;/ul&gt;"</span><span class="pun">;</span><span class="pln">

      </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
    </span><span class="tag">&lt;h2&gt;</span><span class="pln">No results found.</span><span class="tag">&lt;/h2&gt;</span><span class="pln">
    </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">uk-grid</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-flex uk-flex-center"</span><span class="tag">&gt;</span><span class="pln">
      </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-width-1-1 uk-width-1-2@m"</span><span class="tag">&gt;</span><span class="pln">
        </span><span class="pun">&lt;?=</span><span class="pln"> $files</span><span class="pun">-&gt;</span><span class="pln">render</span><span class="pun">(</span><span class="str">'elements/_searchbox'</span><span class="pun">);</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
      </span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="pun">&lt;?</span><span class="pln"> </span><span class="pun">}</span><span class="pln">

    </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
    </span><span class="tag">&lt;h2&gt;</span><span class="pln">Search:</span><span class="tag">&lt;/h2&gt;</span><span class="pln">
    </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">uk-grid</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-flex uk-flex-center"</span><span class="tag">&gt;</span><span class="pln">
      </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-width-1-1 uk-width-1-2@m"</span><span class="tag">&gt;</span><span class="pln">
        </span><span class="pun">&lt;?=</span><span class="pln"> $files</span><span class="pun">-&gt;</span><span class="pln">render</span><span class="pun">(</span><span class="str">'elements/_searchbox'</span><span class="pun">);</span><span class="pln">  </span><span class="pun">?&gt;</span><span class="pln">
      </span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="pun">&lt;?</span><span class="pln"> </span><span class="pun">}</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
  </span><span class="tag">&lt;/div&gt;</span><span class="pln">
</span><span class="tag">&lt;/main&gt;</span></pre>

<p>
	<strong>Explanation:</strong>
</p>

<p>
	This part here at the top of the template handles the requests that are send via ajax. This is the important part for later on.
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php

</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">ajax</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    </span><span class="com">// Return search results in JSON format</span><span class="pln">
    $q </span><span class="pun">=</span><span class="pln"> $sanitizer</span><span class="pun">-&gt;</span><span class="pln">selectorValue</span><span class="pun">(</span><span class="pln">$input</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">-&gt;</span><span class="pln">q</span><span class="pun">);</span><span class="pln">
    $results </span><span class="pun">=</span><span class="pln"> $pages</span><span class="pun">-&gt;</span><span class="pln">find</span><span class="pun">(</span><span class="str">'search_cache%='</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $q</span><span class="pun">);;</span><span class="pln"> </span><span class="com">// Find all pages and save as $results</span><span class="pln">
    header</span><span class="pun">(</span><span class="str">"Content-type: application/json"</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Set header to JSON</span><span class="pln">
    echo $results</span><span class="pun">-&gt;</span><span class="pln">toJSON</span><span class="pun">();</span><span class="pln"> </span><span class="com">// Output the results as JSON via the toJSON function</span><span class="pln">
    </span><span class="kwd">return</span><span class="pln"> $this</span><span class="pun">-&gt;</span><span class="pln">halt</span><span class="pun">();</span><span class="pln">
</span><span class="pun">}</span><span class="pln">
</span><span class="pun">?&gt;</span></pre>

<p>
	What this does:
</p>

<ol>
	<li>
		check if the current request is an ajax request
	</li>
	<li>
		if so, search inside the search cache field
	</li>
	<li>
		return the results in JSON format
	</li>
	<li>
		then quit processing the rest of the template (we don’t want to render any markup in that case!)
	</li>
</ol>

<p>
	Everything below this part is the normal search template logic. If you send a search request via a form somewhere on the website you want to be redirected to the result page and all the results will be listed on the page just like you would expect.
</p>

<p>
	<strong>2. Make the search „work“</strong>
</p>

<p>
	Create a template file called "_searchbox.php" (this file is also included in the search template code above, so adjust your paths/names accordingly).
</p>

<p>
	To make search requests you want to include this search form anywhere on your page.
</p>

<p>
	<strong>_searchbox.php:</strong>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"search-wrapper uk-padding uk-text-center"</span><span class="tag">&gt;</span><span class="pln">
    </span><span class="tag">&lt;h4&gt;</span><span class="pln">Searchbox</span><span class="tag">&lt;/h4&gt;</span><span class="pln">
    &lt;form class="searchform uk-position-relative uk-flex" method="get" action="</span><span class="pun">&lt;?=</span><span class="pln"> $pages</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">"template=search"</span><span class="pun">)-&gt;</span><span class="pln">url </span><span class="pun">?&gt;</span><span class="pln">"&gt;
        </span><span class="tag">&lt;input</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-input"</span><span class="pln"> </span><span class="atn">id</span><span class="pun">=</span><span class="atv">"searchInput"</span><span class="pln"> </span><span class="atn">name</span><span class="pun">=</span><span class="atv">"q"</span><span class="pln"> </span><span class="atn">type</span><span class="pun">=</span><span class="atv">"search"</span><span class="pln"> </span><span class="atn">aria-label</span><span class="pun">=</span><span class="atv">"Suchen"</span><span class="pln"> </span><span class="atn">autocomplete</span><span class="pun">=</span><span class="atv">"off"</span><span class="pln"> </span><span class="atn">placeholder</span><span class="pun">=</span><span class="atv">"z.B. Bagger"</span><span class="tag">&gt;</span><span class="pln">
        </span><span class="tag">&lt;button</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-button uk-button-primary search-button"</span><span class="pln"> </span><span class="atn">type</span><span class="pun">=</span><span class="atv">"submit"</span><span class="tag">&gt;</span><span class="pln">
            </span><span class="tag">&lt;span</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-visible@s"</span><span class="tag">&gt;</span><span class="pln">Search</span><span class="tag">&lt;/span&gt;</span><span class="pln">
        </span><span class="tag">&lt;/button&gt;</span><span class="pln">
        </span><span class="tag">&lt;div</span><span class="pln"> </span><span class="atn">id</span><span class="pun">=</span><span class="atv">"suggestions"</span><span class="pln"> </span><span class="atn">class</span><span class="pun">=</span><span class="atv">"uk-box-shadow-medium"</span><span class="tag">&gt;</span><span class="pln">
        </span><span class="tag">&lt;/div&gt;</span><span class="pln">
    </span><span class="tag">&lt;/form&gt;</span><span class="pln">
</span><span class="tag">&lt;/div&gt;</span></pre>

<p>
	With this simple search form you should now be able to do a basic search that leads you to a search result page.
</p>

<p>
	<strong>3. Make the ajax-search „work“</strong>
</p>

<p>
	 
</p>

<p>
	Now comes the interesting part. We will add in a java script snippet into our just created _searchbox.php that sends ajax requests to the search template page while we are typing into the search intput field and it will display the results in a nice little dropdown. 
</p>

<p>
	Feel free to adjust the code to your needs!
</p>

<pre class="ipsCode prettyprint lang-javascript prettyprinted"><span class="pun">&lt;</span><span class="pln">script</span><span class="pun">&gt;</span><span class="pln">
  document</span><span class="pun">.</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">"DOMContentLoaded"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">()</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

    </span><span class="kwd">const</span><span class="pln"> searchForm </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">querySelector</span><span class="pun">(</span><span class="str">'.searchform'</span><span class="pun">);</span><span class="pln">
    </span><span class="kwd">const</span><span class="pln"> searchUrl </span><span class="pun">=</span><span class="pln"> searchForm</span><span class="pun">.</span><span class="pln">getAttribute</span><span class="pun">(</span><span class="str">'action'</span><span class="pun">);</span><span class="pln">

    </span><span class="kwd">const</span><span class="pln"> searchInput </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">getElementById</span><span class="pun">(</span><span class="str">'searchInput'</span><span class="pun">);</span><span class="pln">
    </span><span class="kwd">const</span><span class="pln"> suggestionsDiv </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">getElementById</span><span class="pun">(</span><span class="str">'suggestions'</span><span class="pun">);</span><span class="pln">
    </span><span class="kwd">let</span><span class="pln"> selectedSuggestionIndex </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pun">;</span><span class="pln">

    </span><span class="com">// close the auto-complete container when clicked outside the element</span><span class="pln">
    document</span><span class="pun">.</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">'click'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="pln">event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">suggestionsDiv</span><span class="pun">.</span><span class="pln">contains</span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">target</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        suggestionsDiv</span><span class="pun">.</span><span class="pln">style</span><span class="pun">.</span><span class="pln">display </span><span class="pun">=</span><span class="pln"> </span><span class="str">'none'</span><span class="pun">;</span><span class="pln">
      </span><span class="pun">}</span><span class="pln">
    </span><span class="pun">});</span><span class="pln">

    searchInput</span><span class="pun">.</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">'input'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">()</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

      </span><span class="kwd">const</span><span class="pln"> searchText </span><span class="pun">=</span><span class="pln"> searchInput</span><span class="pun">.</span><span class="pln">value</span><span class="pun">.</span><span class="pln">toLowerCase</span><span class="pun">();</span><span class="pln">

      </span><span class="com">// Immediately Invoked Function Expression (IIFE)</span><span class="pln">
      </span><span class="pun">(</span><span class="kwd">async</span><span class="pln"> </span><span class="pun">()</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

        </span><span class="kwd">try</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          </span><span class="kwd">const</span><span class="pln"> response </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">await</span><span class="pln"> fetch</span><span class="pun">(</span><span class="pln">searchUrl</span><span class="pun">+</span><span class="str">"?q="</span><span class="pun">+</span><span class="pln">searchText</span><span class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            headers</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
              </span><span class="str">'X-Requested-With'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'XMLHttpRequest'</span><span class="pln">
            </span><span class="pun">}</span><span class="pln">
          </span><span class="pun">});</span><span class="pln">

          </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">response</span><span class="pun">.</span><span class="pln">status </span><span class="pun">===</span><span class="pln"> </span><span class="lit">200</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            </span><span class="kwd">const</span><span class="pln"> data </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">await</span><span class="pln"> response</span><span class="pun">.</span><span class="pln">json</span><span class="pun">();</span><span class="pln">
            showResults</span><span class="pun">(</span><span class="pln">data</span><span class="pun">);</span><span class="pln">
          </span><span class="pun">}</span><span class="pln">
          </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            console</span><span class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="pln">response</span><span class="pun">.</span><span class="pln">status</span><span class="pun">);</span><span class="pln">
            console</span><span class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="pln">response</span><span class="pun">.</span><span class="pln">statusText</span><span class="pun">);</span><span class="pln">
          </span><span class="pun">}</span><span class="pln">

        </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">catch</span><span class="pln"> </span><span class="pun">(</span><span class="pln">error</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          console</span><span class="pun">.</span><span class="pln">error</span><span class="pun">(</span><span class="pln">error</span><span class="pun">);</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
      </span><span class="pun">})();</span><span class="pln">

      </span><span class="kwd">function</span><span class="pln"> showResults</span><span class="pun">(</span><span class="pln">data</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

        </span><span class="com">// Show suggestions only if the input has at least two characters</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">searchText</span><span class="pun">.</span><span class="pln">length </span><span class="pun">&gt;=</span><span class="pln"> </span><span class="lit">2</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

          </span><span class="kwd">const</span><span class="pln"> suggestionHTML </span><span class="pun">=</span><span class="pln"> data</span><span class="pun">.</span><span class="pln">map</span><span class="pun">(</span><span class="pln">item </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            </span><span class="com">// Highlight the matching characters using a &lt;span&gt; element with a CSS class</span><span class="pln">
            </span><span class="kwd">const</span><span class="pln"> highlightedTitle </span><span class="pun">=</span><span class="pln"> item</span><span class="pun">.</span><span class="pln">title</span><span class="pun">.</span><span class="pln">replace</span><span class="pun">(</span><span class="pln">
              </span><span class="kwd">new</span><span class="pln"> </span><span class="typ">RegExp</span><span class="pun">(</span><span class="pln">searchText</span><span class="pun">,</span><span class="pln"> </span><span class="str">'gi'</span><span class="pun">),</span><span class="pln">
              match </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">`&lt;</span><span class="pln">span </span><span class="kwd">class</span><span class="pun">=</span><span class="str">"highlight"</span><span class="pun">&gt;</span><span class="pln">$</span><span class="pun">{</span><span class="pln">match</span><span class="pun">}&lt;/</span><span class="pln">span</span><span class="pun">&gt;`</span><span class="pln">
            </span><span class="pun">);</span><span class="pln">
            </span><span class="kwd">return</span><span class="pln"> </span><span class="pun">`&lt;</span><span class="pln">li </span><span class="kwd">class</span><span class="pun">=</span><span class="str">"suggestion"</span><span class="pun">&gt;&lt;</span><span class="pln">a href</span><span class="pun">=</span><span class="str">"${item.url}"</span><span class="pun">&gt;</span><span class="pln">$</span><span class="pun">{</span><span class="pln">highlightedTitle</span><span class="pun">}&lt;</span><span class="str">/a&gt;&lt;/</span><span class="pln">li</span><span class="pun">&gt;`;</span><span class="pln">
          </span><span class="pun">}).</span><span class="pln">join</span><span class="pun">(</span><span class="str">''</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Array to string conversion</span><span class="pln">

          suggestionsDiv</span><span class="pun">.</span><span class="pln">innerHTML </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">;</span><span class="pln"> </span><span class="com">// Clear the suggestions if input length is less than two characters</span><span class="pln">

          </span><span class="com">// create list and append search results</span><span class="pln">
          </span><span class="kwd">const</span><span class="pln"> suggestionList </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">createElement</span><span class="pun">(</span><span class="str">"ul"</span><span class="pun">);</span><span class="pln">
          suggestionList</span><span class="pun">.</span><span class="pln">classList</span><span class="pun">.</span><span class="pln">add</span><span class="pun">(</span><span class="str">'suggestion-list'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'uk-list'</span><span class="pun">);</span><span class="pln">
          suggestionList</span><span class="pun">.</span><span class="pln">innerHTML </span><span class="pun">=</span><span class="pln"> suggestionHTML</span><span class="pun">;</span><span class="pln">

          suggestionsDiv</span><span class="pun">.</span><span class="pln">appendChild</span><span class="pun">(</span><span class="pln">suggestionList</span><span class="pun">);</span><span class="pln">

          selectedSuggestionIndex </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pun">;</span><span class="pln">

          </span><span class="com">// show the results</span><span class="pln">
          suggestionsDiv</span><span class="pun">.</span><span class="pln">style</span><span class="pun">.</span><span class="pln">display </span><span class="pun">=</span><span class="pln"> </span><span class="str">"block"</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          suggestionsDiv</span><span class="pun">.</span><span class="pln">innerHTML </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">;</span><span class="pln"> </span><span class="com">// Clear the suggestions if input length is less than two characters</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
      </span><span class="pun">}</span><span class="pln">

    </span><span class="pun">});</span><span class="pln">

    </span><span class="com">// Event listener for arrow key presses</span><span class="pln">
    searchInput</span><span class="pun">.</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">"keydown"</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pln"> </span><span class="pun">(</span><span class="pln">event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
      </span><span class="kwd">const</span><span class="pln"> suggestions </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">querySelectorAll</span><span class="pun">(</span><span class="str">".suggestion"</span><span class="pun">);</span><span class="pln">

      </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">key </span><span class="pun">===</span><span class="pln"> </span><span class="str">"ArrowDown"</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        event</span><span class="pun">.</span><span class="pln">preventDefault</span><span class="pun">();</span><span class="pln">
        selectedSuggestionIndex </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Math</span><span class="pun">.</span><span class="pln">min</span><span class="pun">(</span><span class="pln">
          selectedSuggestionIndex </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">,</span><span class="pln">
          suggestions</span><span class="pun">.</span><span class="pln">length </span><span class="pun">-</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
        </span><span class="pun">);</span><span class="pln">
      </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">key </span><span class="pun">===</span><span class="pln"> </span><span class="str">"ArrowUp"</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        event</span><span class="pun">.</span><span class="pln">preventDefault</span><span class="pun">();</span><span class="pln">
        selectedSuggestionIndex </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Math</span><span class="pun">.</span><span class="pln">max</span><span class="pun">(</span><span class="pln">selectedSuggestionIndex </span><span class="pun">-</span><span class="pln"> </span><span class="lit">1</span><span class="pun">,</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pun">);</span><span class="pln">
      </span><span class="pun">}</span><span class="pln"> </span><span class="kwd">else</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">key </span><span class="pun">===</span><span class="pln"> </span><span class="str">"Enter"</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        event</span><span class="pun">.</span><span class="pln">preventDefault</span><span class="pun">();</span><span class="pln">

        </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">selectedSuggestionIndex </span><span class="pun">===</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          searchForm</span><span class="pun">.</span><span class="pln">submit</span><span class="pun">();</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
        </span><span class="kwd">else</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
          window</span><span class="pun">.</span><span class="pln">location </span><span class="pun">=</span><span class="pln"> suggestions</span><span class="pun">[</span><span class="pln">selectedSuggestionIndex</span><span class="pun">].</span><span class="pln">querySelector</span><span class="pun">(</span><span class="str">'a'</span><span class="pun">).</span><span class="pln">href</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
      </span><span class="pun">}</span><span class="pln">

      </span><span class="com">// Highlight the selected suggestion</span><span class="pln">
      suggestions</span><span class="pun">.</span><span class="pln">forEach</span><span class="pun">((</span><span class="pln">suggestion</span><span class="pun">,</span><span class="pln"> index</span><span class="pun">)</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        suggestion</span><span class="pun">.</span><span class="pln">classList</span><span class="pun">.</span><span class="pln">toggle</span><span class="pun">(</span><span class="pln">
          </span><span class="str">"selected"</span><span class="pun">,</span><span class="pln">
          index </span><span class="pun">===</span><span class="pln"> selectedSuggestionIndex
        </span><span class="pun">);</span><span class="pln">
      </span><span class="pun">});</span><span class="pln">
    </span><span class="pun">});</span><span class="pln">
  </span><span class="pun">});</span><span class="pln">

</span><span class="pun">&lt;/</span><span class="pln">script</span><span class="pun">&gt;</span></pre>

<p>
	Keep in mind that you need some CSS styes to make it look good and actually work. These are my styles(in LESS format) but feel free to write you own stylesheet for this.
</p>

<p>
	<strong>search.less:</strong>
</p>

<pre class="ipsCode prettyprint lang-css prettyprinted"><span class="pun">.</span><span class="pln">search-wrapper </span><span class="pun">{</span><span class="pln">
  </span><span class="kwd">position</span><span class="pun">:</span><span class="pln"> relative</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">background</span><span class="pun">:</span><span class="pln"> </span><span class="pun">@</span><span class="pln">secondary-blue</span><span class="pun">;</span><span class="pln">

  h4 </span><span class="pun">{</span><span class="pln">
    </span><span class="kwd">color</span><span class="pun">:</span><span class="pln"> </span><span class="pun">@</span><span class="pln">primary-blue</span><span class="pun">;</span><span class="pln">
  </span><span class="pun">}</span><span class="pln">

  </span><span class="pun">.</span><span class="pln">highlight </span><span class="pun">{</span><span class="pln">
    </span><span class="kwd">color</span><span class="pun">:</span><span class="pln"> </span><span class="pun">@</span><span class="pln">primary-red</span><span class="pun">;</span><span class="pln">
  </span><span class="pun">}</span><span class="pln">

</span><span class="pun">}</span><span class="pln">

</span><span class="pun">#</span><span class="pln">suggestions </span><span class="pun">{</span><span class="pln">
  </span><span class="kwd">display</span><span class="pun">:</span><span class="pln"> none</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">position</span><span class="pun">:</span><span class="pln"> absolute</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">top</span><span class="pun">:</span><span class="pln"> </span><span class="lit">100%</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">left</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">right</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">z-index</span><span class="pun">:</span><span class="pln"> </span><span class="lit">10</span><span class="pun">;</span><span class="pln">
  </span><span class="kwd">background</span><span class="pun">:</span><span class="pln"> </span><span class="pun">@</span><span class="pln">white</span><span class="pun">;</span><span class="pln">

  </span><span class="pun">.</span><span class="pln">suggestion-list </span><span class="pun">{</span><span class="pln">
    </span><span class="kwd">margin</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span><span class="pln">

    li </span><span class="pun">{</span><span class="pln">
      </span><span class="kwd">transition</span><span class="pun">:</span><span class="pln"> background-color </span><span class="lit">150ms</span><span class="pln"> ease-in-out</span><span class="pun">;</span><span class="pln">

      a </span><span class="pun">{</span><span class="pln">
        </span><span class="kwd">padding</span><span class="pun">:</span><span class="pln"> </span><span class="lit">10px</span><span class="pun">;</span><span class="pln">
        </span><span class="kwd">display</span><span class="pun">:</span><span class="pln"> block</span><span class="pun">;</span><span class="pln">
        </span><span class="kwd">text-align</span><span class="pun">:</span><span class="pln"> left</span><span class="pun">;</span><span class="pln">

        </span><span class="pun">&amp;:</span><span class="pln">hover</span><span class="pun">,</span><span class="pln">
        </span><span class="pun">&amp;:</span><span class="pln">focus</span><span class="pun">,</span><span class="pln">
        </span><span class="pun">&amp;:</span><span class="pln">active </span><span class="pun">{</span><span class="pln">
          </span><span class="kwd">text-decoration</span><span class="pun">:</span><span class="pln"> none</span><span class="pun">;</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
      </span><span class="pun">}</span><span class="pln">

      </span><span class="pun">&amp;:</span><span class="pln">hover</span><span class="pun">,</span><span class="pln">
      </span><span class="pun">&amp;:</span><span class="pln">focus</span><span class="pun">,</span><span class="pln">
      </span><span class="pun">&amp;:</span><span class="pln">active</span><span class="pun">,</span><span class="pln">
      </span><span class="pun">&amp;.</span><span class="pln">selected </span><span class="pun">{</span><span class="pln">
        </span><span class="kwd">background</span><span class="pun">:</span><span class="pln"> </span><span class="pun">@</span><span class="pln">secondary-blue</span><span class="pun">;</span><span class="pln">
      </span><span class="pun">}</span><span class="pln">
    </span><span class="pun">}</span><span class="pln">

    li </span><span class="pun">+</span><span class="pln"> li </span><span class="pun">{</span><span class="pln">
      </span><span class="kwd">border-top</span><span class="pun">:</span><span class="pln"> </span><span class="lit">1px</span><span class="pln"> solid </span><span class="pun">@</span><span class="pln">secondary-blue</span><span class="pun">;</span><span class="pln">

    </span><span class="pun">}</span><span class="pln">
  </span><span class="pun">}</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	That's it!
</p>

<p>
	Again feel free to adjust all of the code to your needs. If you have any suggestions how to achieve things a bit easier just let me know.
</p>
]]></description><guid isPermaLink="false">29348</guid><pubDate>Wed, 13 Dec 2023 10:47:29 +0000</pubDate></item><item><title>Automatically login users after successful password reset</title><link>https://processwire.com/talk/topic/31082-automatically-login-users-after-successful-password-reset/</link><description><![CDATA[<p>
	<a contenteditable="false" data-ipshover="" data-ipshover-target="https://processwire.com/talk/profile/2-ryan/?do=hovercard" data-mentionid="2" href="https://processwire.com/talk/profile/2-ryan/" rel="">@ryan</a>'s ForgotPassword module has been in PW since Day Dot. It does it's job BUT it's onerous, especially for Frontend Users, eg those using LoginRegisterPro.<br />
	My client, quite reasonably, wanted the FE users to be logged in automatically after a successful password reset. The module doesn't have many hooks.<br />
	Here's one solution to automatically logging in frontend users after a successful password reset.<br />
	In site/ready.php:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="com">//  Force login after successful password reset</span><span class="pln">
$this</span><span class="pun">-&gt;</span><span class="pln">addHookAfter</span><span class="pun">(</span><span class="str">'ProcessForgotPassword::renderMessage'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="typ">HookEvent</span><span class="pln"> $event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    $msg </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">arguments</span><span class="pun">(</span><span class="lit">0</span><span class="pun">);</span><span class="pln">
    $input </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'input'</span><span class="pun">);</span><span class="pln">
    $session </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'session'</span><span class="pun">);</span><span class="pln">
    $pages </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'pages'</span><span class="pun">);</span><span class="pln">
    $page </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'page'</span><span class="pun">);</span><span class="pln">
	$users </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">(</span><span class="str">'users'</span><span class="pun">);</span><span class="pln">

    $return </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
    </span><span class="com">// This uses the module's default text. </span><span class="pln">
    </span><span class="com">// Adapt to suit on multi-language sites or if you want it to work for admins too</span><span class="pln">
    </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">stripos</span><span class="pun">(</span><span class="pln">$msg</span><span class="pun">,</span><span class="pln"> </span><span class="str">'success'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">false</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> $page</span><span class="pun">-&gt;</span><span class="kwd">template</span><span class="pun">-&gt;</span><span class="pln">name </span><span class="pun">!==</span><span class="pln"> </span><span class="str">'admin'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        $userid </span><span class="pun">=</span><span class="pln"> $input</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'u'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'int'</span><span class="pun">);</span><span class="pln">
		$user </span><span class="pun">=</span><span class="pln"> $users</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="pln">$userid</span><span class="pun">);</span><span class="pln">
		</span><span class="com">// limit access to specific users or roles</span><span class="pln">
        </span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">$user</span><span class="pun">-&gt;</span><span class="pln">isSuperuser</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> $user</span><span class="pun">-&gt;</span><span class="pln">hasRole</span><span class="pun">(</span><span class="str">'login-register'</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
            $session</span><span class="pun">-&gt;</span><span class="pln">forceLogin</span><span class="pun">(</span><span class="pln">$user</span><span class="pun">);</span><span class="pln">
            $session</span><span class="pun">-&gt;</span><span class="pln">redirect</span><span class="pun">(</span><span class="pln">$pages</span><span class="pun">-&gt;</span><span class="kwd">get</span><span class="pun">(</span><span class="str">"name=mypage"</span><span class="pun">)-&gt;</span><span class="pln">url</span><span class="pun">);</span><span class="pln">
        </span><span class="pun">}</span><span class="pln">
    </span><span class="pun">}</span><span class="pln">
    $event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $return</span><span class="pun">;</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	 
</p>
]]></description><guid isPermaLink="false">31082</guid><pubDate>Fri, 28 Feb 2025 09:40:33 +0000</pubDate></item><item><title>How-To use different languages for different users (or different domains)</title><link>https://processwire.com/talk/topic/30998-how-to-use-different-languages-for-different-users-or-different-domains/</link><description><![CDATA[<p>
	Imagine you have a multisite setup where you need different languages for different domains: One website might be single-language (DE only), another one might need DE/EN/CZ, and another one might need DE/EN/HU:
</p>

<p>
	<img class="ipsImage" data-ratio="52.20" height="356" style="height:auto;width:400px;" width="682" alt="yHZz2iO.png" data-src="https://i.imgur.com/yHZz2iO.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	I didn't know how to do it, and I didn't find any other posts with a similar need or solution. When looking into the code I even thought that my need was not possible yet without modifying a core file. So I asked Ryan if he was willing to make the process of adding languages to the $languages array hookable.
</p>

<p>
	Turns out that this is not necessary! All you have to do is to grab the Iterator of the languages array and remove languages as you need:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="com">// /site/ready.php</span><span class="pln">

</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$anyCondition</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="com">// remove CZ language</span><span class="pln">
  $languages</span><span class="pun">-&gt;</span><span class="pln">getIterator</span><span class="pun">()-&gt;</span><span class="kwd">remove</span><span class="pun">(</span><span class="str">"name=cz"</span><span class="pun">);</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	<img class="ipsImage" data-ratio="58.82" height="360" style="height:auto;width:350px;" width="612" alt="0W3X5Ya.png" data-src="https://i.imgur.com/0W3X5Ya.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	<span> </span>So simple <span class="ipsEmoji">😍</span> Maybe it helps <span><span class="ipsEmoji">🙂</span> </span>
</p>
]]></description><guid isPermaLink="false">30998</guid><pubDate>Sun, 02 Feb 2025 16:57:22 +0000</pubDate></item><item><title><![CDATA[🚀 Master ProcessWire Deployments with RockMigrations & GitHub Actions]]></title><link>https://processwire.com/talk/topic/30981-%F0%9F%9A%80-master-processwire-deployments-with-rockmigrations-github-actions/</link><description><![CDATA[<div class="ipsEmbeddedVideo">
	<div>
		<iframe allowfullscreen="" frameborder="0" height="113" src="https://www.youtube-nocookie.com/embed/4wS7xWUtFes?feature=oembed" title="🚀 Master ProcessWire Deployments with RockMigrations &amp; GitHub Actions" width="200"></iframe>
	</div>
</div>

<p>
	 
</p>
]]></description><guid isPermaLink="false">30981</guid><pubDate>Wed, 29 Jan 2025 16:47:55 +0000</pubDate></item><item><title>Sharing some helpful hooks and a couple of extras</title><link>https://processwire.com/talk/topic/30605-sharing-some-helpful-hooks-and-a-couple-of-extras/</link><description><![CDATA[<p>
	I didn't know where the right place to share this was on the forums, so I'll post it here since it may be helpful to those getting started with ProcessWire hooks, or some experienced ProcessWire developers who might find them useful. Either way, dear reader, If someone already wrote it, why write it again?
</p>

<p>
	If you're someone with experience, feedback is welcome! If there are better or more efficient ways to do something, I would love being a student. Some of these may either address challenges that others have experienced as well or were inspired by the awesome community sharing their solutions. Kudos to all of the people out there helping all of us. If someone sees something that was solved elsewhere please share it in the comments to give credit.
</p>

<p>
	I have to make a disclaimer- these have worked for me and while most of them are ready to copy/paste, a few of them are going to need customization or tweaking to make them work for your use case. I don't currently have the resources (time) to provide a lot of support. Some of these were slightly rewritten or adapted for the examples. If you run into issues, the best thing to do is research the solution so that you know exactly what is happening in your application. If you adapt something, fix a bug, or address an edge case, it would be great if you can come back and share that.
</p>

<p>
	<strong>Be smart</strong>, if you're going to run hooks that modify or create data, run a DB backup first. This is the part where I say "I'm not responsible if your site blows up". I don't think that's possible, but do the right thing.
</p>

<p>
	There are <em>dozens</em> of hooks in the project I am sharing these from, and to manage that I created a file structure to handle this because there were far too many to put in one file and keeping the init.php and ready.php files clean really makes a huge difference in maintainability. Being able to jump between files by filename is a supremely efficient way to work as well. The filenames don't matter, they're there to identify the files and make it easy to locate/switch between them.
</p>

<p>
	Here's my approach to directory organization:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">/site
- hooks
-- HookUtils
-- init
-- lazy_cron
-- ready
- init.php
- ready.php</span></pre>

<p>
	The ready.php file contents:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">namespace</span><span class="pln"> </span><span class="typ">ProcessWire</span><span class="pun">;</span><span class="pln">

</span><span class="kwd">if</span><span class="pun">(!</span><span class="kwd">defined</span><span class="pun">(</span><span class="str">"PROCESSWIRE"</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">die</span><span class="pun">();</span><span class="pln">

</span><span class="com">/** @var ProcessWire $wire */</span><span class="pln">

</span><span class="com">// Import all ready hooks</span><span class="pln">
</span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">glob</span><span class="pun">(</span><span class="pln">__DIR__ </span><span class="pun">.</span><span class="pln"> </span><span class="str">'/hooks/ready/*.php'</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $hook</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  require_once $hook</span><span class="pun">;</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	The init.php file contents:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">namespace</span><span class="pln"> </span><span class="typ">ProcessWire</span><span class="pun">;</span><span class="pln">

</span><span class="kwd">if</span><span class="pun">(!</span><span class="kwd">defined</span><span class="pun">(</span><span class="str">"PROCESSWIRE"</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">die</span><span class="pun">();</span><span class="pln">

</span><span class="com">/** @var ProcessWire $wire */</span><span class="pln">

</span><span class="com">// Import all init hooks</span><span class="pln">
</span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">glob</span><span class="pun">(</span><span class="pln">__DIR__ </span><span class="pun">.</span><span class="pln"> </span><span class="str">'/hooks/init/*.php'</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $hook</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  require_once $hook</span><span class="pun">;</span><span class="pln">
</span><span class="pun">}</span><span class="pln">

</span><span class="com">// Import all LazyCron hooks, import after init hooks as there may be dependencies</span><span class="pln">
</span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">glob</span><span class="pun">(</span><span class="pln">__DIR__ </span><span class="pun">.</span><span class="pln"> </span><span class="str">'/hooks/lazy_cron/*.php'</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $hook</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  require_once $hook</span><span class="pun">;</span><span class="pln">
</span><span class="pun">}</span></pre>

<p>
	 
</p>

<p>
	<strong><span style="font-size:20px;">Operational Hooks</span></strong>
</p>

<p>
	Here are some favorites.
</p>

<p>
	<strong><span style="font-size:16px;">Sort items in a Repeater matrix when a page is saved</span></strong>
</p>

<p>
	This one helped sort RM items by a date subfield to help the user experience when editing pages. This implementation is configured to only fire on a specific template but can be modified to fire everywhere if modified to check that a field exists on the page being saved first. This was adapted from an answer here in the PW forum but can't find the original post, so I'm going to include it. If you're having issues getting items to sort the way you want, <a href="https://processwire.com/talk/topic/29283-repeater-field-sort-issue/" rel="">check out this post about natural sorting</a>, which also works elsewhere in ProcessWire.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/bbe2160ef208bfbd390b0fc3e6b4d078" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong><span style="font-size:16px;">Automatically add a new child page when a page with a specific template is created</span></strong>
</p>

<p>
	This automatically creates a new child page and saves it when a page having a specific template is created. This also has the ability to show a message to the user in the admin when new page(s) have been created by this hook. It is also error safe by catching any potential exceptions which will show an informative error to the admin user and log the exception message. The messaging/logging operation is abstracted to a separate object to allow reuse if creating multiple pages.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/aeee8166178bcbdef3332f102b35ccd4" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong><span style="font-size:16px;">Conditionally show the user a message while editing a page</span></strong>
</p>

<p>
	This one shows a message on a page with a specific template under specific conditions. May be page status, field value, type of user, etc. Visual feedback when editing complex pages can be very helpful, especially when an operation may or may not take place depending on factors like the values of multiple fields. This can reduce the amount of explanations needed on each field or training required for users to use a ProcessWire application. In my case, a message is shown if the state of a page indicates that another operation that is triggered by other hooks <em>will</em> or <em>will not</em> run, which is something that the user doesn't directly trigger or may not be aware of.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/621b44e7cf303df0ae33bd6b3998734d" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong>Show the user a message when viewing the page tree</strong>
</p>

<p>
	This is intended to display a message, warning, or error when the page tree is viewed, such as on login, but in this case executes any time the main page tree is viewed to provide consistent communication and awareness. In my case it displays if there is an activity page located under an "Uncategorized" page for an event. This is something that may be buried in the page hierarchy and not noticeable, but if an activity isn't categorized, then is isn't visible on the website, and if it's not visible on the website, people aren't seeing it or buying tickets. So having a persistent message can bring visibility to important but otherwise potentially unnoticed issues. Or you can just say hi and something nice.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/ad2175df01c84c658dc1cc9d7cd8328f" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong>Hook Enhancement - Fast user switching</strong>
</p>

<p>
	Hooks can run on triggers that vary widely. Some can and should be identified as those that are triggered by the current user, others may be more autonomous like executing via cron. There may be other hooks that are executed by a user that isn't logged in. Depending on the type of action and your need to identify or track it, switching from the current user to another user created specifically to handle certain tasks can be very helpful. ProcessWire tracks a number of things that are attributed to users- log entries note the user, the user that creates pages is stored, the user that last updated the page is stored, etc. You may want to know who did what when, or only take action if the last user that touched something was X and not Y.
</p>

<p>
	I created a separate user that has been provided <a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" rel="external nofollow">only the specific permissions</a> it needs to complete jobs that are triggered by hooks or crons. Creating a user with less permissions may also help prevent accidental behaviors, or at least help you be very intentional in determining what actions are delegated. Creating custom permissions is also useful. With a dedicated user I can see explicitly that the last update on some pages were made by an autonomous script that syncs information between the ProcessWire application and a third party platform.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/37e12481c62b7cb9ef02227ba9bd9f40" rel="external nofollow">Github Gist</a> - Fast user switcher
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/5efb3832d7bf3d1f61a649add9fb9ef5" rel="external nofollow">Github Gist</a> - Example of switching users in a hook
</p>

<p>
	<strong>Fast, powerful, and very (very) easy custom admin buttons</strong>
</p>

<p>
	I needed a way to add custom interactive buttons that had some specific requirements.
</p>

<ul>
	<li>
		Needs to be a button that can be clicked by the user and does something
	</li>
	<li>
		Can be conditionally shown to the user with an alternate message if that action is not available
	</li>
	<li>
		Needs to do something on the server and interact with ProcessWire
	</li>
</ul>

<p>
	Here's what that looked like for my application. The green "Refresh Activity" button in the top right. That's a custom button and you don't have to author an Inputfield module to get it. When a user clicks that button, it sends a request to the server with GET variables that are recognized in a hook, actions are taken, then a nice message indicating success or failure is shown to the user.
</p>

<p>
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="27388" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2024_11/image.png.3b3cb0ad691f7ea5d45c0f400977a045.png" rel=""><img alt="image.thumb.png.6f207df1ee9e73a60d8a30bea141a37a.png" class="ipsImage ipsImage_thumbnailed" data-fileid="27388" data-ratio="36.40" style="height:auto;" width="1000" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2024_11/image.thumb.png.6f207df1ee9e73a60d8a30bea141a37a.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" /></a>
</p>

<p>
	To do this you'll need to install <a href="https://processwire.com/modules/inputfield-runtime-only/" rel="external">FieldtypeRuntimeOnly</a> and create a new field. Following the documentation for that field, create a button with a URL to the current page with GET variables appended. Then create a hook that watches for the specific GET variable that executes if it's present. Shoutout to <a contenteditable="false" data-ipshover="" data-ipshover-target="https://processwire.com/talk/profile/2897-robin-s/?do=hovercard" data-mentionid="2897" href="https://processwire.com/talk/profile/2897-robin-s/" rel="">@Robin S</a> for helping make short work of a potentially complex task.
</p>

<p>
	Note that the field code contains JS that handles the URL on page load. Since the hook is looking for a GET variable in the URL, using the back button or refreshing the page will cause the action to run twice. The JS in that example removes the entry from the browser history and also removes the GET parameter after the page loads if it's present.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/d02dd532d20f0414a8f812b21371ea75" rel="external nofollow">Github Gist</a> - An example gist for the hook that handles the action
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/e541add21f7ac7e8c86d297f75d70ed4" rel="external nofollow">Github Gist</a> - An example of the FieldtypeRuntimeOnly code that is displayed and interacted with by the user.
</p>

<p>
	<strong>Automatically convert logged object or array data to JSON</strong>
</p>

<p>
	If you're using the outstanding <a href="https://processwire.com/modules/logs-json-viewer/" rel="external">Logs JSON Viewer</a> (yet another great one by <a contenteditable="false" data-ipshover="" data-ipshover-target="https://processwire.com/talk/profile/2897-robin-s/?do=hovercard" data-mentionid="2897" href="https://processwire.com/talk/profile/2897-robin-s/" rel="">@Robin S</a> module, then this hook makes for a thoroughly enjoyable logging experience. Using array or stdClass data when logging your values helps store additional information in an organized way
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/dfc533eca1856a9b94689ea0145155df" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php

$log</span><span class="pun">-&gt;</span><span class="pln">save</span><span class="pun">(</span><span class="str">'log_name_here'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'Regular string message'</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Remains a string</span><span class="pln">

$log</span><span class="pun">-&gt;</span><span class="pln">save</span><span class="pun">(</span><span class="str">'log_name_here'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">[</span><span class="str">'gets'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="str">'converted'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'to'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="str">'json'</span><span class="pun">]);</span><span class="pln">

$log</span><span class="pun">-&gt;</span><span class="pln">save</span><span class="pun">(</span><span class="str">'log_name_here'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">object</span><span class="pun">)</span><span class="pln"> </span><span class="pun">[</span><span class="str">'is'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="str">'stdClass'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'object'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="str">'friendly'</span><span class="pun">]);</span></pre>

<p>
	<strong>Use a separate field to store address data for a FieldtypeMapMarker field</strong>
</p>

<p>
	This one is really simple, more just sharing an implementation and idea, but proved valuable for reducing data redundancy. I have a <a href="https://processwire.com/modules/fieldtype-map-marker/" rel="external">FieldtypeMapMarker</a> field but the way that I needed to store address data was much better suited to using multiple fields for things like street, city, state, and zip code. I wanted those fields to be the "controlling" fields for the map marker field to prevent needing to edit 2 fields to keep updated, or accidental content diversion between them. On page save the value from the address fields are pulled and converted into a single string that is added to the FieldtypeMapMarker field's "address" property. I used a Custom Field (ProFields) for my address fields but this can be modified to suit your use case very easily.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/daecfe1172f73f43ea5e826bc5d25870" rel="external nofollow">Github Gist</a>
</p>

<p>
	You might also consider hiding the address input on the FieldtypeMapMarker field itself to reduce confusion since the values will be updated automatically anyway. You'll need to have this in a file that is appended to the Admin styles
</p>

<pre class="ipsCode prettyprint lang-css prettyprinted"><span class="com">/*
  You can find the appropriate class for the template you are applying this to in the &lt;body&gt; element when editing a page
  You can omit that if you want to apply this everywhere
*/</span><span class="pln">

</span><span class="pun">.</span><span class="pln">ProcessPageEdit-template-your_template_name </span><span class="pun">.</span><span class="pln">InputfieldMapMarker</span><span class="pun">.</span><span class="pln">Inputfield_activity_location </span><span class="pun">.</span><span class="pln">InputfieldMapMarkerAddress</span><span class="pun">,</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ProcessPageEdit-template-your_template_name </span><span class="pun">.</span><span class="pln">InputfieldMapMarker</span><span class="pun">.</span><span class="pln">Inputfield_activity_location </span><span class="pun">.</span><span class="pln">InputfieldMapMarkerToggle</span><span class="pun">,</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ProcessPageEdit-template-your_template_name </span><span class="pun">.</span><span class="pln">InputfieldMapMarker</span><span class="pun">.</span><span class="pln">Inputfield_activity_location </span><span class="pun">.</span><span class="pln">InputfieldMapMarkerLat</span><span class="pun">,</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ProcessPageEdit-template-your_template_name </span><span class="pun">.</span><span class="pln">InputfieldMapMarker</span><span class="pun">.</span><span class="pln">Inputfield_activity_location </span><span class="pun">.</span><span class="pln">InputfieldMapMarkerLng </span><span class="pun">{</span><span class="pln">
   </span><span class="kwd">display</span><span class="pun">:</span><span class="pln"> none </span><span class="kwd">!important</span><span class="pun">;</span><span class="pln">
</span><span class="pun">}</span></pre>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php
</span><span class="com">// Add this to your ready.php file or ready-firing hook to insert the file containing that CSS to your admin.</span><span class="pln">

$config</span><span class="pun">-&gt;</span><span class="pln">styles</span><span class="pun">-&gt;</span><span class="kwd">add</span><span class="pun">(</span><span class="str">"/path/to/your/custom/admin/css/file.css"</span><span class="pun">);</span></pre>

<p>
	<strong>Not-A-Hook Bonus - Here's code for an interactive Google Map</strong>
</p>

<p>
	Renders a Google Map using a <a href="https://processwire.com/modules/fieldtype-map-marker/" rel="external">FieldtypeMapMarker</a> field, a separate address field, <a href="https://alpinejs.dev/" rel="external nofollow">Alpine.js</a>, and <a href="https://tailwindcss.com/" rel="external nofollow">Tailwind</a>. You'll need a Google Maps API key, a styled map ID from your Google Developer account, and the aforementioned fields. I wrote it using the latest Google Maps API. Saved you some time. You'll probably need to tweak it. I adapted this so if you find a bug please let me know and I'll update the gist.
</p>

<p>
	Note- this makes use of the <a href="https://alpinejs.dev/plugins/intersect" rel="external nofollow">AlpineJS Intersect</a> plugin to improve performance by only loading/initializing the map when a user scrolls close enough to it. If you don't want that, remove the x-intersect directive. If you want to see it in action, <a href="https://modernismweek.com/2025/experiences/midcentury-mixology-cocktail-class-with-steen/" rel="external nofollow">you can check it out here</a>.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/2f0bea60aaf21b4441293823ed14dd61" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong>Hook Support Class - A static method class to translate a field into all languages automatically</strong>
</p>

<p>
	If you use the Fluency translation module, this is a class that will help out with translating a field into all languages programmatically. Sharing this here because the next hook uses this as a dependency. I keep this in the HookUtils directory noted in the file structure above. Usage is demonstrated in the next hook.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/3f082db9baf69316722290348f7dcac1" rel="external nofollow">Github Gist</a>
</p>

<p>
	<strong>Translate all translatable fields using Fluency on page save whether from UI or API.</strong>
</p>

<p>
	This is useful for instances where you want a page translated automatically and especially helpful when you are creating pages programmatically. This requires the above hook support class, as well as Fluency connected to an API account. Here are things that must be kept in mind. Please read them, the code for the hook, and the code for the support class to ensure that it works to your needs.
</p>

<ul>
	<li>
		<strong>You should modify Fluency before using this, really</strong>. Change the value of CACHE_EXPIRY on line 19 in the TranslationCache file to WireCache::expireNever. Do this to prevent chewing through your API usage from month to month on repeat translations. This will become standard in the next release of Fluency.
	</li>
	<li>
		This is an expensive operation in terms of API usage, which is why you very much should modify the caching behavior. This hook does not make an effort to determine which fields have changed before translating because it doesn't really matter if the translation is already cached.
	</li>
	<li>
		First time translations of pages with a significant amount of fields/content may be slow, like noticeably slower first time page save because this operation is only as fast as the speed of the request/response loop between ProcessWire and the translation API. Later page saves will be much faster thanks to cached translations. This will not attempt to translate empty fields, so those won't cause any delays.
	</li>
	<li>
		This works with multi-language text/textarea/TinyMCE/CKEditor fields, RepeaterMatrix fields, and the newer Custom Fields (ProFields). Other fields haven't been tested, but it's definitely possible to adapt this to those needs.
	</li>
	<li>
		I prefer to target specific templates with hooks, you can add multiple but be mindful of your use case.
	</li>
	<li>
		Consider adding excluded fields to the array in the hook if it makes sense
	</li>
	<li>
		Consider adding a field to enable/disable translations from the UI, a checkbox field or something
	</li>
	<li>
		This hook is probably one of the uglier ones, sorry.
	</li>
	<li>
		If you run out of API usage on your account, you're going to see a big ugly exception error on screen. This is due to Fluency not handling an account overage error properly because the return type was not as expected. Will be fixed in the next version of the module
	</li>
	<li>
		This is one that may be tailored to my PW application, I think it's general enough to use as-is for your project, but testing is definitely required.
	</li>
	<li>
		Read all the code please.
	</li>
</ul>

<p>
	<a href="https://gist.github.com/SkyLundy/df895f5bd7cdf72d542dc28fa8e6e572" rel="external nofollow">Github Gist</a>
</p>

<p>
	<span style="font-size:20px;"><strong>ProcessWire Object Method &amp; Property Hooks</strong></span>
</p>

<p>
	The following are custom methods that add functionality to native ProcessWire objects.
</p>

<p>
	<span style="font-size:16px;"><strong>Add a getMatrixChildren() method to RepeaterMatrixPage objects</strong></span>
</p>

<p>
	RepeaterMatrix fields represent nesting depth as an integer on each RepeaterMatrixPage item. So top level is 0, first nested level is 1, second 2, etc. When looping through RM items, determining nesting requires working with that integer. It works, but adding adding some functionality helps out. This is infinitely nestable, so accessing children, children of children, children of children of children, and so on works. Fun for the whole family. This was inspired by a forum post, another one I can't find...
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/ee3b8157175a7cbdf59da232b3f72bd4" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php

</span><span class="com">// Access nested RepeaterMatrix items as child PageArray objects</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">repeater_matrix_field</span><span class="pun">-&gt;</span><span class="pln">first</span><span class="pun">()-&gt;</span><span class="pln">getMatrixChildren</span><span class="pun">();</span><span class="pln"> </span><span class="com">// =&gt; PageArray</span><span class="pln">

</span><span class="pun">?&gt;</span><span class="pln">
</span><span class="com">&lt;!-- Assists with rendering nested RM items in templates Sponsors are nested under sponsorship levels in the RM field --&gt;</span><span class="pln">
</span><span class="tag">&lt;div&gt;</span><span class="pln">
  </span><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">sponsors </span><span class="kwd">as</span><span class="pln"> $sponsorshipLevel</span><span class="pun">):</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
    </span><span class="tag">&lt;h2&gt;</span><span class="pun">&lt;?=</span><span class="pln">$sponsorshipLevel</span><span class="pun">-&gt;</span><span class="pln">title</span><span class="pun">?&gt;</span><span class="tag">&lt;/h2&gt;</span><span class="pln">

    </span><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$sponsorshipLevel</span><span class="pun">-&gt;</span><span class="pln">getMatrixChildren</span><span class="pun">()-&gt;</span><span class="pln">count</span><span class="pun">()):</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
      </span><span class="tag">&lt;ul&gt;</span><span class="pln">
        
        </span><span class="pun">&lt;?</span><span class="pln">php </span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$sponsorshipLevel</span><span class="pun">-&gt;</span><span class="pln">getMatrixChildren</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $sponsor</span><span class="pun">):</span><span class="pln"> </span><span class="pun">?&gt;</span><span class="pln">
          </span><span class="tag">&lt;li&gt;</span><span class="pln">
            &lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$sponsor</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">" alt="</span><span class="pun">&lt;?=</span><span class="pln">$sponsor</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"&gt;
            </span><span class="pun">&lt;?=</span><span class="pln">$sponsor</span><span class="pun">-&gt;</span><span class="pln">title</span><span class="pun">?&gt;</span><span class="pln">
          </span><span class="tag">&lt;/li&gt;</span><span class="pln">
        </span><span class="pun">&lt;?</span><span class="pln">php endforeach </span><span class="pun">?&gt;</span><span class="pln">
        
      </span><span class="tag">&lt;/ul&gt;</span><span class="pln">
    </span><span class="pun">&lt;?</span><span class="pln">php endif </span><span class="pun">?&gt;</span><span class="pln">

  </span><span class="pun">&lt;?</span><span class="pln">php endforeach </span><span class="pun">?&gt;</span><span class="pln">
</span><span class="tag">&lt;/div&gt;</span></pre>

<p>
	 
</p>

<p>
	<span style="font-size:16px;"><strong>Add a resizeAspectRatio() method to PageImage objects</strong></span>
</p>

<p>
	Adds a simple way to quickly resize an image to a specific aspect ratio. Use cases include sizing images for Google Structured Data and formatting images for consistency in image carousels. Could be improved by accepting second argument to specify an image width, but didn't fit my use case.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/0299b397858e25103fe51bf1c2aca3fa" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php 

$page</span><span class="pun">-&gt;</span><span class="pln">image_field</span><span class="pun">-&gt;</span><span class="pln">resizeAspectRatio</span><span class="pun">(</span><span class="str">'square'</span><span class="pun">)-&gt;</span><span class="pln">url</span><span class="pun">;</span><span class="pln"> </span><span class="com">// Alias for 1:1</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">image_field</span><span class="pun">-&gt;</span><span class="pln">resizeAspectRatio</span><span class="pun">(</span><span class="str">'video'</span><span class="pun">)-&gt;</span><span class="pln">url</span><span class="pun">;</span><span class="pln"> </span><span class="com">// Alias for 16:9</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">image_field</span><span class="pun">-&gt;</span><span class="pln">resizeAspectRatio</span><span class="pun">(</span><span class="str">'17:10'</span><span class="pun">)-&gt;</span><span class="pln">url</span><span class="pun">;</span><span class="pln"> </span><span class="com">// Arbitrary values accepted</span></pre>

<p>
	 
</p>

<p>
	<span style="font-size:16px;"><strong>Add a responsiveAttributes() method to PageImage objects</strong></span>
</p>

<p>
	Adds a very helpful method to generate image variations and accompanying 'srcset' and 'sizes' attributes for any image. Designed to be very flexible and is Tailwind ready. Responsive sizing can be as simple or complex as your needs require. Includes an optional 'mobile' Tailwind breakpoint that matches a custom tailwind.config.js value:  screens: { 'mobile': '320px'}. I added this breakpoint largely to further optimize images for small screens. The array of Tailwind breakpoints and size definitions can be edited to suit your specific setup if there are customizations
</p>

<p>
	When sizing for Tailwind, the last media query generated will automatically be switched to "min-width" rather than "max-width" to prevent problems arising from restricting widths. Example, you can specify values only for 'sm' and 'md' and the 'md' size will have the media query correctly adjusted so that it applies to all breakpoints above it.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/41335611a59985985df121e921b427ba" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">&lt;--
    The responsiveAttributes() returns a renderable attribute string: srcset="{generated values}" sizes="{generated values}"
--&gt;
&lt;-- Create responsive images with arbitrary width and height at breakpoints --&gt;
&lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">"
     </span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">responsiveAttributes</span><span class="pun">([</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">240</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 300px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">225</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 600px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">280</span><span class="pun">,</span><span class="pln"> </span><span class="lit">165</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 900px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">210</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 1200px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">260</span><span class="pun">,</span><span class="pln"> </span><span class="lit">155</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(min-width: 1500px)'</span><span class="pun">],</span><span class="pln">
     </span><span class="pun">])?&gt;</span><span class="pln">
     width="240" height="125"
     alt="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"
&gt;

&lt;-- Heights can be selectively ommitted by setting the height value to null --&gt;
&lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">"
     </span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">responsiveAttributes</span><span class="pun">([</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">240</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 300px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">225</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 600px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">280</span><span class="pun">,</span><span class="pln"> </span><span class="lit">165</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 900px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">210</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 1200px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">260</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(min-width: 1500px)'</span><span class="pun">],</span><span class="pln">
     </span><span class="pun">])?&gt;</span><span class="pln">
     width="240" height="125"
     alt="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"
&gt;
  
&lt;-- Create responsive images with only widths at breakpoints --&gt;
&lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">"
     </span><span class="pun">&lt;?=</span><span class="pln">page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">responsiveAttributes</span><span class="pun">([</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">240</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 300px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">225</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 600px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">280</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 900px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">210</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(max-width: 1200px)'</span><span class="pun">],</span><span class="pln">
       </span><span class="pun">[</span><span class="lit">260</span><span class="pun">,</span><span class="pln"> </span><span class="str">'(min-width: 1500px)'</span><span class="pun">],</span><span class="pln">
     </span><span class="pun">])?&gt;</span><span class="pln">
     width="240"
     height="125"
     alt="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"
&gt;
  
&lt;-- Create custom sizes matched to Tailwind breakpoints --&gt;
&lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">"
     </span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">responsiveAttributes</span><span class="pun">([</span><span class="pln">
       </span><span class="str">'mobile'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="lit">240</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">],</span><span class="pln"> </span><span class="com">// Custom tailwind directive</span><span class="pln">
       </span><span class="str">'sm'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="lit">225</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">],</span><span class="pln">
       </span><span class="str">'md'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="lit">280</span><span class="pun">,</span><span class="pln"> </span><span class="lit">165</span><span class="pun">],</span><span class="pln">
       </span><span class="str">'lg'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="lit">210</span><span class="pun">,</span><span class="pln"> </span><span class="lit">125</span><span class="pun">],</span><span class="pln">
       </span><span class="str">'xl'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">[</span><span class="lit">260</span><span class="pun">,</span><span class="pln"> </span><span class="lit">155</span><span class="pun">],</span><span class="pln">
     </span><span class="pun">])?&gt;</span><span class="pln">
     width="240" height="125"
     alt="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"
&gt;
 
</span><span class="com">&lt;!--
     Resizes width of image to fit Tailwind breakpoints, useful for full width images such as hero
     images, doesn't change height. Also accepts 'tw' as an alias for 'tailwind'
--&gt;</span><span class="pln">
&lt;img src="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">url</span><span class="pun">?&gt;</span><span class="pln">"
     </span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">responsiveAttributes</span><span class="pun">(</span><span class="str">'tailwind'</span><span class="pun">)?&gt;</span><span class="pln">
     width="240" height="125"
     alt="</span><span class="pun">&lt;?=</span><span class="pln">$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">description</span><span class="pun">?&gt;</span><span class="pln">"
&gt;</span></pre>

<p>
	 
</p>

<p>
	<span style="font-size:16px;"><strong>Add PHP higher-order function methods to WireArray and WireArray derived objects</strong></span>
</p>

<p>
	WireArray objects are incredibly powerful and have tons of utility, but there are situations where I find myself needing to work with plain PHP arrays. I'm a very big fan of PHP's array functions that are efficient and make for clean readable code. I found myself often reaching for $wireArrayThing-&gt;getArray() to work with data <em>then</em> using functions like array_map, array_filter, and array_reduce. These return arrays, but could easily be modified to return WireArray objects if that is more helpful.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/64e5782984e2c747e3aeded374bed998" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php 

</span><span class="com">// The EventPage page class has a method that determines sold out status from more than one source of data/page fields</span><span class="pln">
</span><span class="com">// which means that it isn't queryable using a ProcessWire selector. This returns a single integer calculated from ticket availability</span><span class="pln">
</span><span class="com">// of all events from non-queryable data.</span><span class="pln">
$totalEventsAvailable </span><span class="pun">=</span><span class="pln"> $eventPages</span><span class="pun">-&gt;</span><span class="pln">reduce</span><span class="pun">(</span><span class="pln">
  fn </span><span class="pun">(</span><span class="pln">$total</span><span class="pun">,</span><span class="pln"> $eventPage</span><span class="pun">)</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $count </span><span class="pun">=</span><span class="pln"> $eventPage</span><span class="pun">-&gt;</span><span class="pln">isActive</span><span class="pun">()</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> $total</span><span class="pun">++</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> $total</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Requires using a page class to determine status reliant on multiple data points not queryable via a selector. Knowing what the event</span><span class="pln">
</span><span class="com">// page is for an activity can't be determined using a selector for activity pages.</span><span class="pln">
$displayableActivities </span><span class="pun">=</span><span class="pln"> $matches</span><span class="pun">-&gt;</span><span class="pln">filterToArray</span><span class="pun">(</span><span class="pln">
  fn </span><span class="pun">(</span><span class="pln">$activityPage</span><span class="pun">)</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $activityPage</span><span class="pun">-&gt;</span><span class="pln">eventPage</span><span class="pun">()-&gt;</span><span class="pln">isPublic</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> $activityPage</span><span class="pun">-&gt;</span><span class="pln">isActive</span><span class="pun">()</span><span class="pln">
</span><span class="pun">);</span><span class="pln">

</span><span class="com">// Iterating over each Page in a PageArray and processing data for sorting/ordering before rendering on a search results page</span><span class="pln">
</span><span class="com">// Executed within a page class</span><span class="pln">
$results </span><span class="pun">=</span><span class="pln"> $searchResults</span><span class="pun">-&gt;</span><span class="pln">mapToArray</span><span class="pun">(</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">$page</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="kwd">return</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">object</span><span class="pun">)</span><span class="pln"> </span><span class="pun">[</span><span class="pln">
    </span><span class="str">'page'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $page</span><span class="pun">,</span><span class="pln">
    </span><span class="str">'summary'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $this</span><span class="pun">-&gt;</span><span class="pln">createResultSummary</span><span class="pun">(</span><span class="pln">page</span><span class="pun">:</span><span class="pln"> $page</span><span class="pun">,</span><span class="pln"> maxLength</span><span class="pun">:</span><span class="pln"> </span><span class="lit">750</span><span class="pun">),</span><span class="pln">
    </span><span class="str">'keywordMatchCount'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> $this</span><span class="pun">-&gt;</span><span class="pln">getQueryMatchCount</span><span class="pun">(</span><span class="pln">page</span><span class="pun">:</span><span class="pln"> $page</span><span class="pun">),</span><span class="pln">
  </span><span class="pun">];</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	<span style="font-size:16px;"><strong>Add an image orientation method/property to PageImage objects</strong></span>
</p>

<p>
	Get the portrait or landscape orientation of a PageImage.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/fca699b61caf98a9df64540462c50898" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php 

$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">orientation</span><span class="pun">;</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">image</span><span class="pun">-&gt;</span><span class="pln">orientation</span><span class="pun">();</span></pre>

<p>
	<span style="font-size:16px;"><strong>Add the ability to get all related pages for Page objects at once</strong></span>
</p>

<p>
	Gets all of the related pages to a page at once by both page reference fields and links in fields. Transparently passes native arguments to Page methods for native behavior
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/68f97cba6e55ace598f160e0eb84ba63" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php 

$page</span><span class="pun">-&gt;</span><span class="pln">allPageReferences</span><span class="pun">();</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">allPageReferences</span><span class="pun">(</span><span class="kwd">true</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Optionally include all pages regardless of status</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">allPageReferences</span><span class="pun">(</span><span class="str">'your_selector=here'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'field_name'</span><span class="pun">);</span><span class="pln"> </span><span class="com">// Use with native Page::references() and Page::links() arguments</span></pre>

<p>
	<span style="font-size:16px;"><strong>Add a saveWithoutHooks() convenience method to Page objects</strong></span>
</p>

<p>
	The number of hooks in my most recent project was... a lot. There were many that hooked into the page save event and a lot of operations that happen in the background where pages needed to be modified and saved quietly to prevent clearing ProCache files or excessive DB operations through chained hooks. Being able to use a method to do this rather than passing options felt more deliberate and clear when working across hundreds of files and in critical areas of very expensive operations.
</p>

<p>
	This method also accepts page save options, but in a way that hooks will always be disabled even if an option is accidentally passed enabling them. Furthermore, it also accepts a string as the first argument that, if debug mode is enabled, will dump a message to the bar via Tracy.
</p>

<p>
	<a href="https://gist.github.com/SkyLundy/8b0bd00d7c90ba6da3d1b686a173b6c6" rel="external nofollow">Github Gist</a>
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pun">&lt;?</span><span class="pln">php 

</span><span class="com">// Adding a message can be very helpful during testing, especially when saving a page with/without hooks is conditionally based</span><span class="pln">
</span><span class="com">// where the result of another operation determines how a page is saved</span><span class="pln">
$page</span><span class="pun">-&gt;</span><span class="pln">saveWithoutHooks</span><span class="pun">(</span><span class="str">'updated event sync data hash, saved without hooks'</span><span class="pun">);</span><span class="pln">

$page</span><span class="pun">-&gt;</span><span class="pln">saveWithoutHooks</span><span class="pun">([</span><span class="str">'resetTrackChanges'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">]);</span><span class="pln">

$page</span><span class="pun">-&gt;</span><span class="pln">saveWithoutHooks</span><span class="pun">(</span><span class="str">'message and options'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">[</span><span class="str">'resetTrackChanges'</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">]);</span></pre>

<p>
	These are a few that I've used to show some diversity in application. Hooking to ProcessWire events makes it possible to build beyond simple websites and implement truly custom behavior. Hope these may be useful to others. If you have any favorite hooks of your own, have corrections of those I've shared, or improvements, sharing them in the comments would be stellar.
</p>

<p>
	Cheers!
</p>
]]></description><guid isPermaLink="false">30605</guid><pubDate>Wed, 06 Nov 2024 23:23:35 +0000</pubDate></item><item><title>Uncheck checked radio buttons in admin</title><link>https://processwire.com/talk/topic/12987-uncheck-checked-radio-buttons-in-admin/</link><description><![CDATA[<p>Ok, another one for @netcarver <img data-emoticon="true" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" alt=":)" srcset="https://processwire-forums.s3.us-west-2.amazonaws.com/emoticons/smile@2x.png 2x" width="20" height="20" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/emoticons/default_smile.png"></p>
<pre class="ipsCode prettyprint">
// enable unchecking radio buttons
// <a href="http://stackoverflow.com/questions/6191621/jquery-check-uncheck-radio-button-onclick#answer-13575528" rel="external nofollow">http://stackoverflow.com/questions/6191621/jquery-check-uncheck-radio-button-onclick#answer-13575528</a>
$page-&gt;addHookAfter('render', function ($event) {

    if ($this-&gt;page-&gt;template != 'admin') return;

    $js = &lt;&lt;&lt; HTML
 &lt;script&gt;
 $(document).ready(function(){
    $("input:radio:checked").data("chk",true);
    $("input:radio").click(function(){
        $("input[name='"+$(this).attr("name")+"']:radio").not(this).removeData("chk");
        $(this).data("chk",!$(this).data("chk"));
        $(this).prop("checked",$(this).data("chk"));
    });
});
&lt;/script&gt;
HTML;
</pre>
<p><a class="ipsAttachLink ipsAttachLink_image" href="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_04_2016/post-3156-0-74006600-1460377892.gif" rel="external"><img src="https://processwire.com/talk/applications/core/interface/js/spacer.png" data-fileid="%7B___base_url___%7D/applications/core/interface/file/attachment.php?id=7355" class="ipsImage ipsImage_thumbnailed" alt="post-3156-0-74006600-1460377892_thumb.gi" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_04_2016/post-3156-0-74006600-1460377892_thumb.gif"></a></p>]]></description><guid isPermaLink="false">12987</guid><pubDate>Mon, 11 Apr 2016 12:32:01 +0000</pubDate></item><item><title>Speed up modal page editing by adding a "Save + Close" button, plus easily add custom CSS and JS to the PW admin</title><link>https://processwire.com/talk/topic/29712-speed-up-modal-page-editing-by-adding-a-save-close-button-plus-easily-add-custom-css-and-js-to-the-pw-admin/</link><description><![CDATA[<p>
	I was browsing the <a href="https://github.com/processwire/processwire-requests/issues" rel="external nofollow">requests repo</a> and saw <a href="https://github.com/processwire/processwire-requests/issues/202" rel="external nofollow">a request</a> from <a contenteditable="false" data-ipshover="" data-ipshover-target="https://processwire.com/talk/profile/985-adrian/?do=hovercard" data-mentionid="985" href="https://processwire.com/talk/profile/985-adrian/" rel="">@adrian</a> that reminded me of a PW feature I had forgotten existed: from Page List you can open a page for editing in a modal window by long-clicking on the Edit button.
</p>

<p>
	<img alt="image.png.2ff0c169cea19a6918a1840f2e79406d.png" class="ipsImage ipsImage_thumbnailed" data-fileid="25984" data-ratio="34.00" style="height:auto;" width="350" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2024_02/image.png.2ff0c169cea19a6918a1840f2e79406d.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	This is quite handy for when you want to make a quick edit to a page. But as the request notes, it would speed things up if there was a "Save + Close" button in the modal.
</p>

<p>
	Until the request is actioned in the core I thought I'd try implementing it in some custom code, and it's also an opportunity to show an easy way you can add custom JS and CSS to the ProcessWire admin.
</p>

<p>
	 
</p>

<p>
	The first step is to create the following files at /site/templates/admin-assets/admin-custom.js and /site/templates/admin-assets/admin-custom.css. The CSS is optional.
</p>

<p>
	admin-custom.js
</p>

<pre class="ipsCode prettyprint lang-javascript prettyprinted"><span class="pln">$</span><span class="pun">(</span><span class="pln">document</span><span class="pun">).</span><span class="pln">ready</span><span class="pun">(</span><span class="kwd">function</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln">

	</span><span class="kwd">const</span><span class="pln"> $body </span><span class="pun">=</span><span class="pln"> $</span><span class="pun">(</span><span class="str">'body'</span><span class="pun">);</span><span class="pln">
	</span><span class="kwd">const</span><span class="pln"> is_modal </span><span class="pun">=</span><span class="pln"> $body</span><span class="pun">.</span><span class="pln">hasClass</span><span class="pun">(</span><span class="str">'modal'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> $body</span><span class="pun">.</span><span class="pln">hasClass</span><span class="pun">(</span><span class="str">'pw-iframe'</span><span class="pun">);</span><span class="pln">

	</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$body</span><span class="pun">.</span><span class="pln">hasClass</span><span class="pun">(</span><span class="str">'ProcessPageEdit'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> is_modal</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
		</span><span class="kwd">const</span><span class="pln"> params </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">new</span><span class="pln"> </span><span class="typ">URLSearchParams</span><span class="pun">(</span><span class="pln">window</span><span class="pun">.</span><span class="pln">location</span><span class="pun">.</span><span class="pln">search</span><span class="pun">);</span><span class="pln">
		</span><span class="kwd">const</span><span class="pln"> $save_button </span><span class="pun">=</span><span class="pln"> $</span><span class="pun">(</span><span class="str">'#submit_save'</span><span class="pun">);</span><span class="pln">
		</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">params</span><span class="pun">.</span><span class="pln">has</span><span class="pun">(</span><span class="str">'quick_edit'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> $save_button</span><span class="pun">.</span><span class="pln">length</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
			$save_button</span><span class="pun">.</span><span class="pln">parent</span><span class="pun">().</span><span class="pln">append</span><span class="pun">(</span><span class="str">'&lt;button type="submit" name="submit_save" value="Save" class="ui-button uk-hidden" id="save-and-close"&gt;Save + Close&lt;/button&gt;'</span><span class="pun">);</span><span class="pln">
		</span><span class="pun">}</span><span class="pln">
	</span><span class="pun">}</span><span class="pln">

	</span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$body</span><span class="pun">.</span><span class="pln">hasClass</span><span class="pun">(</span><span class="str">'ProcessPageList'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> </span><span class="pun">!</span><span class="pln">is_modal</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
		$</span><span class="pun">(</span><span class="pln">document</span><span class="pun">).</span><span class="pln">on</span><span class="pun">(</span><span class="str">'ajaxComplete'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
			$</span><span class="pun">(</span><span class="str">'.PageListActionEdit a:not([data-autoclose])'</span><span class="pun">).</span><span class="pln">each</span><span class="pun">(</span><span class="kwd">function</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
				$</span><span class="pun">(</span><span class="kwd">this</span><span class="pun">).</span><span class="pln">attr</span><span class="pun">(</span><span class="str">'data-autoclose'</span><span class="pun">,</span><span class="pln"> </span><span class="str">'#save-and-close'</span><span class="pun">).</span><span class="pln">attr</span><span class="pun">(</span><span class="str">'href'</span><span class="pun">,</span><span class="pln"> $</span><span class="pun">(</span><span class="kwd">this</span><span class="pun">).</span><span class="pln">attr</span><span class="pun">(</span><span class="str">'href'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> </span><span class="str">'&amp;quick_edit=1'</span><span class="pun">);</span><span class="pln">
			</span><span class="pun">});</span><span class="pln">
		</span><span class="pun">});</span><span class="pln">
	</span><span class="pun">}</span><span class="pln">

</span><span class="pun">});</span></pre>

<p>
	admin-custom.css
</p>

<pre class="ipsCode prettyprint lang-css prettyprinted"><span class="com">/* Avoid Tracy debugbar appearing on top of modal */</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ui-widget-overlay</span><span class="pun">.</span><span class="pln">ui-front </span><span class="pun">{</span><span class="pln"> </span><span class="kwd">z-index</span><span class="pun">:</span><span class="lit">40000</span><span class="pun">;</span><span class="pln"> </span><span class="pun">}</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ui-dialog </span><span class="pun">{</span><span class="pln"> </span><span class="kwd">z-index</span><span class="pun">:</span><span class="lit">40001</span><span class="pun">;</span><span class="pln"> </span><span class="pun">}</span><span class="pln">
</span><span class="com">/* Place modal buttons on the left rather than the right */</span><span class="pln">
</span><span class="pun">.</span><span class="pln">ui-dialog </span><span class="pun">.</span><span class="pln">ui-dialog-buttonpane </span><span class="pun">.</span><span class="pln">ui-dialog-buttonset </span><span class="pun">{</span><span class="pln"> </span><span class="kwd">float</span><span class="pun">:</span><span class="pln">none</span><span class="pun">;</span><span class="pln"> </span><span class="pun">}</span></pre>

<p>
	 
</p>

<p>
	The second step is to add the following hook to /site/ready.php
</p>

<p>
	One good thing about using a hook to AdminTheme::getExtraMarkup to add custom CSS is that the file gets loaded after any CSS files that are loaded by modules or the PW core, so you can override any CSS rules without needing to add extra specificity.
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="com">// Add custom JS and CSS to admin</span><span class="pln">
$wire</span><span class="pun">-&gt;</span><span class="pln">addHookAfter</span><span class="pun">(</span><span class="str">'AdminTheme::getExtraMarkup'</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">function</span><span class="pun">(</span><span class="typ">HookEvent</span><span class="pln"> $event</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
	$parts </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pun">;</span><span class="pln">
	$config </span><span class="pun">=</span><span class="pln"> $event</span><span class="pun">-&gt;</span><span class="pln">wire</span><span class="pun">()-&gt;</span><span class="pln">config</span><span class="pun">;</span><span class="pln">

	</span><span class="com">// JS file should exist at /site/templates/admin-assets/admin-custom.js</span><span class="pln">
	$js_url </span><span class="pun">=</span><span class="pln"> $config</span><span class="pun">-&gt;</span><span class="pln">urls</span><span class="pun">-&gt;</span><span class="pln">templates </span><span class="pun">.</span><span class="pln"> </span><span class="str">'admin-assets/admin-custom.js'</span><span class="pun">;</span><span class="pln">
	$modified </span><span class="pun">=</span><span class="pln"> filemtime</span><span class="pun">(</span><span class="pln">rtrim</span><span class="pun">(</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">paths</span><span class="pun">-&gt;</span><span class="pln">root</span><span class="pun">,</span><span class="pln"> </span><span class="str">'/'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $js_url</span><span class="pun">);</span><span class="pln">
	$parts</span><span class="pun">[</span><span class="str">'head'</span><span class="pun">]</span><span class="pln"> </span><span class="pun">.=</span><span class="pln"> </span><span class="str">"&lt;script type='text/javascript' src='$js_url?m=$modified'&gt;&lt;/script&gt;"</span><span class="pun">;</span><span class="pln">

	</span><span class="com">// CSS file should exist at /site/templates/admin-assets/admin-custom.css</span><span class="pln">
	$css_url </span><span class="pun">=</span><span class="pln"> $config</span><span class="pun">-&gt;</span><span class="pln">urls</span><span class="pun">-&gt;</span><span class="pln">templates </span><span class="pun">.</span><span class="pln"> </span><span class="str">'admin-assets/admin-custom.css'</span><span class="pun">;</span><span class="pln">
	$modified </span><span class="pun">=</span><span class="pln"> filemtime</span><span class="pun">(</span><span class="pln">rtrim</span><span class="pun">(</span><span class="pln">$config</span><span class="pun">-&gt;</span><span class="pln">paths</span><span class="pun">-&gt;</span><span class="pln">root</span><span class="pun">,</span><span class="pln"> </span><span class="str">'/'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> $css_url</span><span class="pun">);</span><span class="pln">
	$parts</span><span class="pun">[</span><span class="str">'head'</span><span class="pun">]</span><span class="pln"> </span><span class="pun">.=</span><span class="pln"> </span><span class="str">"&lt;link rel='stylesheet' href='$css_url?m=$modified'&gt;"</span><span class="pun">;</span><span class="pln">

	$event</span><span class="pun">-&gt;</span><span class="kwd">return</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $parts</span><span class="pun">;</span><span class="pln">
</span><span class="pun">});</span></pre>

<p>
	 
</p>

<p>
	The end result:
</p>

<p>
	<img alt="image.png.e908ad80ebc6dde46e525731cd584619.png" class="ipsImage ipsImage_thumbnailed" data-fileid="25985" data-ratio="57.14" style="height:auto;" width="910" data-src="//processwire-forums.s3.us-west-2.amazonaws.com/monthly_2024_02/image.png.e908ad80ebc6dde46e525731cd584619.png" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	 
</p>

<p>
	 
</p>
]]></description><guid isPermaLink="false">29712</guid><pubDate>Tue, 20 Feb 2024 23:02:34 +0000</pubDate></item><item><title>How to open a modal window from JavaScript</title><link>https://processwire.com/talk/topic/30354-how-to-open-a-modal-window-from-javascript/</link><description><![CDATA[<p>
	I'm working on <a href="https://www.baumrock.com/en/processwire/modules/rockcalendar/" rel="external nofollow">RockCalendar</a> which will be released soon if everything goes smoothly <span class="ipsEmoji">😎</span> I think it should be stable enough next month. If you want to get notified you can subscribe to the <a href="https://www.baumrock.com/en/rock-monthly/" rel="external nofollow">Rock Monthly Newsletter</a>.
</p>

<p>
	The module uses <a href="https://fullcalendar.io/" rel="external nofollow">https://fullcalendar.io/</a> for the calendar interface and when clicking an event it will open the page editor in a pw modal:
</p>

<p>
	<img alt="KJnKZrl.gif" class="ipsImage" data-ratio="88.70" height="746" style="height:auto;" width="841" data-src="https://i.imgur.com/KJnKZrl.gif" src="https://processwire.com/talk/applications/core/interface/js/spacer.png" />
</p>

<p>
	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.
</p>

<p>
	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:
</p>

<pre class="ipsCode prettyprint lang-javascript prettyprinted"><span class="pln">      calendar</span><span class="pun">.</span><span class="pln">on</span><span class="pun">(</span><span class="str">"eventClick"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">(</span><span class="pln">info</span><span class="pun">)</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
        </span><span class="com">// create a fake link element in body</span><span class="pln">
        </span><span class="kwd">let</span><span class="pln"> link </span><span class="pun">=</span><span class="pln"> document</span><span class="pun">.</span><span class="pln">createElement</span><span class="pun">(</span><span class="str">"a"</span><span class="pun">);</span><span class="pln">
        </span><span class="kwd">let</span><span class="pln"> $link </span><span class="pun">=</span><span class="pln"> $</span><span class="pun">(</span><span class="pln">link</span><span class="pun">);</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">attr</span><span class="pun">(</span><span class="pln">
          </span><span class="str">"href"</span><span class="pun">,</span><span class="pln">
          </span><span class="typ">ProcessWire</span><span class="pun">.</span><span class="pln">config</span><span class="pun">.</span><span class="pln">urls</span><span class="pun">.</span><span class="pln">admin </span><span class="pun">+</span><span class="pln"> </span><span class="str">"page/edit/?id="</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> info</span><span class="pun">.</span><span class="pln">event</span><span class="pun">.</span><span class="pln">id
        </span><span class="pun">);</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">addClass</span><span class="pun">(</span><span class="str">"pw-modal"</span><span class="pun">);</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">attr</span><span class="pun">(</span><span class="str">"data-autoclose"</span><span class="pun">,</span><span class="pln"> </span><span class="str">""</span><span class="pun">);</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">attr</span><span class="pun">(</span><span class="str">"data-buttons"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"button.ui-button[type=submit]"</span><span class="pun">);</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">on</span><span class="pun">(</span><span class="str">"click"</span><span class="pun">,</span><span class="pln"> pwModalOpenEvent</span><span class="pun">);</span><span class="pln">
        $</span><span class="pun">(</span><span class="pln">document</span><span class="pun">).</span><span class="pln">on</span><span class="pun">(</span><span class="str">"pw-modal-closed"</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">this</span><span class="pun">.</span><span class="pln">refresh</span><span class="pun">.</span><span class="pln">bind</span><span class="pun">(</span><span class="kwd">this</span><span class="pun">));</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">click</span><span class="pun">();</span><span class="pln">
        $link</span><span class="pun">.</span><span class="pln">remove</span><span class="pun">();</span><span class="pln">
      </span><span class="pun">});</span></pre>

<p>
	Maybe that helps anyone else looking for a solution to a similar problem.
</p>

<p>
	If anybody knows a better way how to do it, please let me know!
</p>

<p>
	PS: If you have input (feature requests) for the development of RockCalendar please let me know: <a href="https://processwire.com/talk/topic/30355-request-for-input-what-features-should-a-pw-calendar-module-have/" rel="">https://processwire.com/talk/topic/30355-request-for-input-what-features-should-a-pw-calendar-module-have/</a>
</p>

<p>
	 
</p>
]]></description><guid isPermaLink="false">30354</guid><pubDate>Fri, 30 Aug 2024 09:06:00 +0000</pubDate></item><item><title>How to build a (hacky) Hook Recorder</title><link>https://processwire.com/talk/topic/30827-how-to-build-a-hacky-hook-recorder/</link><description><![CDATA[<p>
	Ever wondered when certain hooks get executed? Which hooks got fired along the request? Or need to debug why your hook does not fire as expected?
</p>

<p>
	Open /wire/core/WireHooks.php, find the method "runHooks" and paste this at the very top of this method:
</p>

<pre class="ipsCode prettyprint lang-php prettyprinted"><span class="pln">		$logfile </span><span class="pun">=</span><span class="pln"> wire</span><span class="pun">()-&gt;</span><span class="pln">config</span><span class="pun">-&gt;</span><span class="pln">paths</span><span class="pun">-&gt;</span><span class="pln">root </span><span class="pun">.</span><span class="pln"> </span><span class="str">'hooks.txt'</span><span class="pun">;</span><span class="pln">
		$tooOld </span><span class="pun">=</span><span class="pln"> is_file</span><span class="pun">(</span><span class="pln">$logfile</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&amp;&amp;</span><span class="pln"> filemtime</span><span class="pun">(</span><span class="pln">$logfile</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&lt;</span><span class="pln"> time</span><span class="pun">()</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="lit">5</span><span class="pun">;</span><span class="pln">
		</span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$tooOld</span><span class="pun">)</span><span class="pln"> unlink</span><span class="pun">(</span><span class="pln">$logfile</span><span class="pun">);</span><span class="pln">
        $logData </span><span class="pun">=</span><span class="pln"> get_class</span><span class="pun">(</span><span class="pln">$object</span><span class="pun">)</span><span class="pln"> </span><span class="pun">.</span><span class="pln"> </span><span class="str">"::$method\n"</span><span class="pun">;</span><span class="pln">
		file_put_contents</span><span class="pun">(</span><span class="pln">$logfile</span><span class="pun">,</span><span class="pln"> $logData</span><span class="pun">,</span><span class="pln"> FILE_APPEND</span><span class="pun">);</span></pre>

<p>
	This will write a log of all hookable methods to hooks.txt and it will probably begin like this:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">ProcessWire\FileCompiler::compile
ProcessWire\FileCompiler::compile
ProcessWire\FileCompiler::compile
ProcessWire\FileCompiler::compile
ProcessWire\Fields::load
ProcessWire\Fieldgroups::load
ProcessWire\Templates::load
ProcessWire\Fieldgroup::setQuietly
ProcessWire\Fieldgroup::callUnknown
(maaaany more lines)</span></pre>

<p>
	And it will most likely end with "ProcessWire\ProcessWire::finished" <span><span class="ipsEmoji">🙂</span> </span>
</p>

<p>
	<span>Now you can play around with this logfile and view different pages. For example when opening a page for editing you will have a portion of this in the log (ProcessPageEdit::buildForm...):</span>
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">ProcessWire\ProcessPageEdit::breadcrumb
ProcessWire\JqueryCore::use
ProcessWire\JqueryUI::use
ProcessWire\ProcessPageEdit::execute
ProcessWire\HutPage::setEditor
ProcessWire\ProcessPageEdit::buildForm
ProcessWire\ProcessPageEdit::buildFormContent
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::changed
ProcessWire\Field::changed
ProcessWire\Field::getInputfield
ProcessWire\Field::changed
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\Field::getInputfield
ProcessWire\HutPage::viewable
ProcessWire\Pages::find
ProcessWire\PageFinder::find
ProcessWire\PageFinder::getQuery
ProcessWire\HutPage::addable
ProcessWire\ProcessPageEdit::buildFormChildren
ProcessWire\WireInputData::int
ProcessWire\WireInputData::callUnknown
ProcessWire\WireInputData::int
ProcessWire\WireInputData::callUnknown</span></pre>

<p>
	You can also use grep to find the hooks you are looking for, for example:
</p>

<pre class="ipsCode prettyprint lang-html prettyprinted"><span class="pln">cat hooks.txt | grep ProcessWire::

ProcessWire\ProcessWire::init
ProcessWire\ProcessWire::ready
ProcessWire\ProcessWire::finished
ProcessWire\ProcessWire::finished</span></pre>

<p>
	This has helped me today to find the correct spot to hook into. Maybe it also helps anyone else. Have fun!
</p>

<p>
	PS: Don't forget to remove this once you are done with inspecting, of course!
</p>
]]></description><guid isPermaLink="false">30827</guid><pubDate>Fri, 27 Dec 2024 16:47:29 +0000</pubDate></item></channel></rss>
