MODX forms without a page reload, protected from spam out of the box.
English | Русский
FetchIt submits the forms of a MODX site with the Fetch API: no page reload, no jQuery or other libraries. The forms are processed by FormIt, with all its hooks and validation, or by a snippet of your own. Every form is protected from spam from the start: a single-use token, a fill time, a trap field and a limit of submissions, and a proof of work and a captcha when you need them. One package for MODX 2.8 and MODX 3.
- Features
- Installation
- Quick start
- Snippet properties
- Spam protection
- Notifications
- Events and JavaScript
- TypeScript
- System settings
- Upgrading to FetchIt 4
- Requirements
- Development
- License
- FormIt out of the box. Properties such as
&hooks,&validateand&emailTogo to FormIt as they are, with no wrappers. Instead of FormIt you can name a snippet of your own that returns JSON (success,message,data). - Your own markup. All it takes is a form chunk with attributes:
data-errorfor the errors of fields,data-successanddata-validation-errorfor the messages of the form. The classes of invalid fields are system settings. - Works without JavaScript too. With FormIt the form is sent the usual way, and the messages and the values entered come back through its placeholders. Unless a proof of work or a captcha is on: they need JavaScript.
- Spam protection by default: a signed single-use token, a minimum fill time, a hidden trap field and a limit of submissions. Optionally a proof of work and a captcha: Cloudflare Turnstile, Google reCAPTCHA v3 or Yandex SmartCaptcha. Plugins add rules of their own.
- No dependencies. The script needs no jQuery or other libraries and uses the native Fetch API and
FormData(files included). It is loaded withdeferand loads no other files, except the script of the captcha provider when a captcha is on. - Built-in notifications, or your own through
FetchIt.Message: Bootstrap, SweetAlert2, anything. - Events
fetchit:before,fetchit:after,fetchit:success,fetchit:error,fetchit:reset: add data, cancel a submission, show a modal. - TypeScript types ship next to the script.
- Several forms on a page, each with its own key and handler, as long as the snippet calls differ in their properties.
- Fenom and
@FILEchunks through pdoTools on MODX 2 and MODX 3.
FetchIt is free. Install it with the Package Manager from the official modx.com repository or from the modstore.pro marketplace (how to add the repository). When FormIt is not installed, the installer downloads it from modx.com; if that fails, the install log says so, and FormIt has to be installed by hand.
You can also build the package from this repository, see Development.
Call the snippet where the form goes. The call must be uncached ([[!FetchIt]]): every output of the form has a token of its own, and the script is added only when the snippet ran in the request.
[[!FetchIt?
&form=`myForm.tpl`
&hooks=`email`
&emailTo=`info@example.com`
&emailSubject=`A request from the site`
&validate=`name:required,email:email:required`
&successMessage=`Your message has been sent`
]]
The chunk myForm.tpl, a plain form with the attributes of FetchIt:
<form action="[[~[[*id]]]]" method="post">
<label>Name
<input type="text" name="name" value="[[+fi.name]]">
<span data-error="name">[[+fi.error.name]]</span>
</label>
<label>Email
<input type="email" name="email" value="[[+fi.email]]">
<span data-error="email">[[+fi.error.email]]</span>
</label>
<button type="submit">Send</button>
<div data-success style="display: [[+fi.success:is=`1`:then=``:else=`none`]]">[[+fi.successMessage]]</div>
<div data-validation-error style="display: [[+fi.validation_error:is=`1`:then=``:else=`none`]]">[[+fi.validation_error_message]]</div>
</form>data-error="name"gets the error of the fieldname, and the field itself getsaria-invalidand the class fromfetchit.frontend.input.invalid.class.data-custom="name"(optional) gets the class fromfetchit.frontend.custom.invalid.class: the wrapper of the field, for example.data-successanddata-validation-errorshow the message of the form.- The
[[+fi.…]]placeholders are for submissions without JavaScript. If those do not matter, leave them out.
FetchIt adds the data-fetchit key and the service fields of the protection to the form, and adds the script to the page. The chunk tpl.FetchIt.example is a ready example.
The documentation (in Russian) has more examples: markup, notifications, modals, client-side validation, the JS API.
| Property | Default | What it does |
|---|---|---|
form |
tpl.FetchIt.example |
The chunk of the form. With pdoTools: @FILE, @INLINE and Fenom |
snippet |
FormIt |
The snippet that processes the form, see below |
actionUrl |
[[+assetsUrl]]action.php |
Where the form is sent |
clearFieldsOnSuccess |
1 |
Clear the fields after a successful submission |
All other properties go to the processing snippet: for FormIt, &hooks, &validate, &emailTo, &successMessage, &placeholderPrefix and the rest.
A snippet of your own gets the fields sent in $fields (without the service fields of the protection) and answers through FetchIt:
$FetchIt = FetchIt::service($modx);
if (empty($fields)) {
// The form is being output, not sent: the snippet runs on every output too.
return '';
}
if (empty($fields['email'])) {
return $FetchIt->error('Please check the form', ['email' => 'Enter your email']);
}
// ... save it, send an email
return $FetchIt->success('Thank you, we got your request');The empty($fields) check is a must: the snippet also runs on every output of the form, with an empty $fields, and without the check it would, for example, send an empty email on every page view. A form sent without JavaScript does not show what the snippet returns: a message appears on the page only if the snippet sets placeholders itself.
Every FetchIt form is protected with no setup. The checks run before FormIt or your snippet, both for FetchIt submissions and for forms sent without JavaScript:
- Token. The form gets a hidden
fetchit_tokenfield with a signature of the key of the form, the time it was output and a random number. A token is used once and needs no session. A new token is given only in answer to a well-signed token of the same form, so a bot cannot send the form without loading the page. The answer to a submission with a well-signed token brings the next token (except a refusal for the limit), so the form can be sent again without a reload. - Fill time. A form sent sooner than
fetchit.protection.min_timeseconds after the page was output (3 by default) is refused. A second submission counts from the same output, so a person who fixed a field after an error is not refused. - Trap. A hidden field with a random name per installation, which people do not see and browser autofill does not recognise. When it is filled in, the bot gets a success answer with the
successMessageof the form, but the form is not processed and no email goes out. Such cases are logged. - Limit. At most
fetchit.protection.rate_limitsubmissions of a form from one address withinfetchit.protection.rate_windowseconds (10 in 10 minutes by default). Every attempt with a well-signed token counts, refused ones too, so replaying an old token does not get around the limit.
The service fields are removed from $_POST before FormIt runs and never reach emails. The signing key is generated when the package is installed (fetchit.protection.secret).
Both are off by default and help when spam gets through the main checks. They work only with the protection on (fetchit.protection).
Proof of work. fetchit.protection.pow sets the difficulty in bits: 0 turns it off, 24 is the most (a larger value or one that is not a number is logged and replaced). Before sending, the browser looks for a number n such that SHA-256 of token:n starts with that many zero bits, and sends it in the fetchit_pow field. Solving starts as soon as the visitor enters the form, so the solution is usually ready by the time they press the button, while every submission costs a bot processor time. 16 bits take about 65 thousand hashes on average: about a third of a second on a computer, several times longer on a phone. Every bit doubles the time, 20 bits take about 16 times longer. When a page from a cache asks for less than the server, FetchIt solves again and sends the form once more by itself. With a proof of work on, a form cannot be sent without JavaScript.
Captcha. Set fetchit.captcha to turnstile (Cloudflare Turnstile), recaptcha (Google reCAPTCHA v3) or smartcaptcha (Yandex SmartCaptcha), and fetchit.captcha.site_key and fetchit.captcha.secret_key to the keys from the provider's dashboard. Until both keys are set, the captcha stays off and the log says what is missing (the same for a misspelt provider). FetchIt adds the provider's script and gets its answer before sending:
- Turnstile shows its widget in a
.fetchit-captchablock before the first button withtype="submit", or at the end of the form when there is none; - reCAPTCHA v3 has no widget: an answer is asked for each submission, with the action
fetchit. Answers scoring belowfetchit.captcha.min_score(0.5 by default) are refused; - SmartCaptcha works invisibly and shows a puzzle only when in doubt.
The server checks the answer last among the checks of the protection (OnFetchItBeforeProcess plugins run after it), so bots without a token never reach the provider. When the provider refuses the answer, the visitor sees fetchit_err_captcha. When the provider does not answer, answers with an error or does not accept the secret key, the form is refused too, but with fetchit_err_captcha_unavailable ("the check is unavailable right now"), and the cause is logged. When the provider's script did not load (an ad blocker, CSP), the visitor closed the puzzle or the widget failed, the form is not sent and the visitor sees fetchit_err_captcha_client. The captcha works only through FetchIt: a form sent without JavaScript does not pass it.
For a development site Turnstile has test keys: site 1x00000000000000000000AA, secret 1x0000000000000000000000000000000AA.
A refusal comes as an ordinary error of the form: the lexicon message fetchit_err_token (the form has expired), fetchit_err_too_fast, fetchit_err_rate, fetchit_err_store, fetchit_err_pow, fetchit_err_captcha or fetchit_err_captcha_unavailable, the fetchit:error event and a notification. When the token of the page is stale (a page from a cache, a page open for long, a new key), FetchIt sends the form once more with a new token by itself, and the visitor notices nothing. Without JavaScript the message goes to [[+fi.validation_error_message]], and the values entered are kept, as in the tpl.FetchIt.example chunk.
- A full-page cache (nginx, a CDN, static cache plugins) gives every visitor the same token. Through FetchIt this works thanks to the automatic resend, but pages with forms are better left out of such a cache: without JavaScript the first submission is refused.
- FormIt 5.2 and later can send forms over AJAX itself. For FetchIt forms that mode is turned off: FetchIt removes the properties FormIt stored, the
fi.ajaxTokenplaceholder and theformit.jsscript, as otherwise the form could be sent through FormIt'saction.php, around the protection. Forms that[[!FormIt]]itself outputs on the same page keep their AJAX mode. The reCAPTCHA of FormIt 5.2 gets its answer throughformit.js, so it does not work in FetchIt forms: usefetchit.captchainstead. Captcha hooks that read the answer from their field themselves work, as long asfetchit.captchais not set to the same provider. - Forms inserted over AJAX after the page has loaded are not picked up by FetchIt: the script finds the forms when the page loads. Such forms are sent the usual way.
- A script of your own instead of the bundled one must send the form to
action.phpwith anX-FetchIt-Actionheader equal to thedata-fetchitattribute of the form, and apageIdfield with the id of the page. It must send thefetchit_tokenfield of the form and take the next token from theX-FetchIt-Tokenheader of every answer. When an answer comes withX-FetchIt-Refused: token, send the form again with the new token. With a proof of work, send infetchit_powa solution for the token sent in that request, and solve again for a resend. Apowrefusal tells the difficulty in theX-FetchIt-Powheader. With a captcha, send the provider's answer in its field (cf-turnstile-response,g-recaptcha-responseorsmart-token), and execute reCAPTCHA v3 with the actionfetchit. - Behind a proxy or a CDN all visitors come from one address, and the limit becomes one for the whole site. List the addresses of the proxies in
fetchit.protection.proxies(IPs or CIDR ranges, separated by commas) and the header with the visitor's address infetchit.protection.ip_header(X-Forwarded-For,CF-Connecting-IP). - The marks of used tokens are kept in
core/cache/fetchit/tokens/, which "Clear cache" leaves alone. When the directory is not writable, forms are refused and the cause is logged (unless the log is off). On several servers without a sharedcore/cache, a token can be used once on each server. - The log:
fetchit.protection.log0 turns it off, 1 (the default) logs problems and refusals that may hit people (the trap, the limit, writing the marks, a captcha provider that is unavailable), 2 logs every refusal, answers the captcha provider refused included. Mistakes in the settings of the captcha and the proof of work are always logged. - On a development site and in automated tests, set
fetchit.protection.min_timeandfetchit.protection.rate_limitto 0. Turn the whole protection (fetchit.protection) off only for debugging.
A plugin on the OnFetchItBeforeProcess event gets $action, $fields (the fields sent, without the service fields and files), $properties (may be null) and $FetchIt. To refuse a submission, it outputs a message for the visitor or a lexicon key through $modx->event->output(). Returning a string with return does not refuse it: MODX only logs it.
// A plugin on OnFetchItBeforeProcess
$email = isset($fields['email']) && is_string($fields['email']) ? $fields['email'] : '';
if (preg_match('/@(mailinator|tempmail)\./i', $email)) {
$modx->event->output('Disposable addresses are not accepted.');
}The event also fires with the protection off.
With fetchit.frontend.default.notifier, the answers of the server and failed submissions are also shown as notifications in a corner of the page. The message is shown as text: tags are removed from it, HTML entities such as & stay as they are.
- Screen readers hear the notifications from two hidden live regions that are on the page from the start: an error at once (
role="alert"), a success when the reader is idle (role="status"). - A notification closes with its button or by itself after 6 seconds. While the pointer or the focus is on it, the countdown stops, and then starts over. When a notification is closed from the keyboard, the focus moves to the next notification or back to where it came from.
- At most three are shown at a time. The one with the focus is never the one dropped.
The styles come first in <head>, with single class selectors. They win over the site's rules for bare elements (button { … }) and lose to any rule of the site with a class. Rules inside @layer (in Tailwind 4, for example) lose to them: for those, change the variables. By default they are pastel colours of Tailwind CSS 4, as in its alerts: the 100 shade for the background, 200 for the border, 800 for the text (a text contrast of about 6.5:1, well above WCAG AA). Where the browser knows oklch() they are exact, elsewhere in hex:
.fetchit-toasts {
--fetchit-toast-success-bg: oklch(96.2% 0.044 156.743); /* green-100, #dcfce7 */
--fetchit-toast-success-border: oklch(92.5% 0.084 155.995); /* green-200, #b9f8cf */
--fetchit-toast-success-text: oklch(44.8% 0.119 151.328); /* green-800, #016630 */
--fetchit-toast-error-bg: oklch(93.6% 0.032 17.717); /* red-100, #ffe2e2 */
--fetchit-toast-error-border: oklch(88.5% 0.062 18.334); /* red-200, #ffc9c9 */
--fetchit-toast-error-text: oklch(44.4% 0.177 26.899); /* red-800, #9f0712 */
}A site on Tailwind 4 can take its own variables, for example --fetchit-toast-success-bg: var(--color-emerald-100).
When the site's Content-Security-Policy forbids inline styles (style-src without 'unsafe-inline'), give the FetchIt script a nonce: the styles get the same one. Otherwise FetchIt warns in the console, and the notifications need styles of your own.
A FetchIt.Message of your own wins over the setting. When it has neither success nor error (only a spinner in before and after, for example), the built-in notifications add them, provided FetchIt.Message is set before DOMContentLoaded: right in a deferred script added after fetchit.js. You can also turn the notifications on from your own script, with another label for the button or another time on screen (0 means until closed): FetchIt.Message = FetchIt.createNotifier({ closeLabel: 'Close', duration: 4000 }).
Before FetchIt 4 the setting loaded the Notyf library. It is gone: styles for .notyf__toast and scripts that call new Notyf() need to change, or load Notyf themselves.
The events are dispatched on document and do not bubble. Their event.detail holds the form (form), its data (formData) and the FetchIt instance (fetchit), and from fetchit:after on the answer of the server (response). A fetchit:error without an answer has the cause (error); fetchit:reset has only form and fetchit.
| Event | When | Cancelling it (event.preventDefault()) |
|---|---|---|
fetchit:before |
before sending; fields can be added to formData |
the form is not sent |
fetchit:after |
a FetchIt answer came | the answer is not handled: no field errors, no message, no notification, no fetchit:success or fetchit:error, no clearing |
fetchit:success |
the form was accepted | the fields are not cleared |
fetchit:error |
the form was refused or could not be sent; response is null when no FetchIt answer came, the cause is in error |
the field errors and the message of the form are not shown |
fetchit:reset |
the form is being reset: its button, form.reset() or after a success; the fields still hold their values |
not possible |
document.addEventListener('fetchit:before', ({ detail }) => {
detail.formData.append('page', location.pathname)
})
document.addEventListener('fetchit:success', ({ detail }) => {
gtag('event', 'form_submit', { form_id: detail.form.id })
})FetchIt.Message takes the hooks before, after(message), success(message), error(message) and reset. They run before the event of the same moment (except reset, which runs after fetchit:reset), so cancelling the event does not undo them:
FetchIt.Message = {
success (message) { Swal.fire({ icon: 'success', text: message }) },
error (message) { Swal.fire({ icon: 'error', text: message }) },
}The global FetchIt exists once the deferred fetchit.js has run, so set FetchIt.Message from a deferred script added after it, or on DOMContentLoaded. The instance of a form is FetchIt.instances.get(form), with setError(name, message), clearErrors(), setFormMessage(type, message) and other methods.
The types are in assets/components/fetchit/js/fetchit.d.ts. Copy the file into your project or reference it:
/// <reference path="../assets/components/fetchit/js/fetchit.d.ts" />
document.addEventListener('fetchit:error', event => {
// response is null when no FetchIt answer came: the network, a foreign answer, a captcha without an answer
if (event.detail.response === null) {
console.error(event.detail.error)
}
})
const form = document.querySelector('form')
if (form) {
FetchIt.instances.get(form)?.setError('email', 'Check the address')
}The script is type-checked against the same file: FetchIt with the instances of forms, and the detail of every event, so the types and the code cannot drift apart. Remove your own declaration of FetchIt if you had one (declare var FetchIt: any): the two would clash.
All settings are in the fetchit namespace.
| Setting | Default | What it does |
|---|---|---|
fetchit.frontend.js |
[[+assetsUrl]]js/fetchit.js |
The FetchIt script. The minified js/fetchit.min.js is about 19 KB instead of 36 (about 7 KB instead of 10 gzipped) |
fetchit.frontend.js.classname |
FetchIt |
The class that handles the forms, when you extended the bundled one |
fetchit.frontend.input.invalid.class |
is-invalid |
The class of an invalid field |
fetchit.frontend.custom.invalid.class |
The class for the data-custom elements of an invalid field |
|
fetchit.frontend.default.notifier |
No |
Show the answers as built-in notifications |
fetchit.protection |
Yes |
Spam protection. Turn it off only for debugging |
fetchit.protection.secret |
random | The key that signs the tokens, generated at install |
fetchit.protection.min_time |
3 |
The minimum fill time, seconds; 0 turns it off |
fetchit.protection.token_ttl |
86400 |
How many seconds a page with a form stays valid; 0 and anything above 30 days mean 30 days |
fetchit.protection.rate_limit |
10 |
Submissions of a form from one address within the window; 0 turns it off |
fetchit.protection.rate_window |
600 |
The window of the limit, seconds; 0 turns the limit off |
fetchit.protection.log |
1 |
The log of the protection: 0 nothing, 1 problems, 2 every refusal |
fetchit.protection.proxies |
Trusted proxies or CDNs, IPs or CIDR ranges separated by commas | |
fetchit.protection.ip_header |
X-Forwarded-For |
The header with the visitor's address from a trusted proxy |
fetchit.protection.pow |
0 |
The difficulty of the proof of work, bits; 0 turns it off, 24 at most |
fetchit.captcha |
turnstile, recaptcha or smartcaptcha; empty for none |
|
fetchit.captcha.site_key |
The site key from the captcha provider | |
fetchit.captcha.secret_key |
The secret key from the captcha provider | |
fetchit.captcha.min_score |
0.5 |
reCAPTCHA v3: the minimum score, from 0 to 1 |
FetchIt 4 is one package for MODX 2.8 and MODX 3 instead of the 1.x and 3.x lines. Install it over the version you have with the Package Manager: the system settings and chunks stay as they are. The FetchIt snippet and plugin are replaced, together with the default properties of the snippet, so changes to their code and properties are lost; the snippet calls on your pages need no changes. CI checks the upgrade from 1.1.3 on MODX 2 and from 3.1.4 on MODX 3.
The API of the earlier versions works on both MODX versions:
- in your snippets, get FetchIt with
$FetchIt = FetchIt::service($modx);. The 1.x call ($modx->getService('fetchit', 'FetchIt', MODX_CORE_PATH . 'components/fetchit/model/')) and the 3.x one ($modx->services->get('FetchIt'), MODX 3 only) return the same object; - the 3.x class
FetchIt\FetchItis the same class,instanceof \FetchIt\FetchItworks.get_class()now returnsFetchIt; storeActionProperties()/loadActionProperties()and their 3.x namessaveActionProperties()/getActionProperties();- chunks through pdoTools (Fenom,
@FILE) on MODX 2 and MODX 3.
What may affect your code:
- forms get the service fields of the protection; a script of your own instead of the bundled one must send the token (see Things to keep in mind);
- the notifications setting no longer loads Notyf (see Notifications);
fetchit:erroralso fires when a request fails;detail.responseis thennull, and the error is indetail.error;- the processing snippet gets only the form sent in
fields:$_POSTand the files from$_FILESwhen sent through FetchIt, only$_POSTfor a form sent the usual way; no GET values and no cookies; fetchit:successcan be cancelled:event.preventDefault()keeps the fields filled;- sending again while a request is running is ignored;
methodanddata-fetchitnow come last among the attributes of the form tag.
The full list of changes is in the changelog.
- MODX 2.8 or MODX 3 (checked on 2.8.6 and 3.2.4).
- PHP 7.4 or later. The syntax and PHPUnit are checked on PHP 7.4 to 8.4, the install and form submissions on MODX on PHP 7.4 and 8.3.
- FormIt, when it processes the forms; it is installed with FetchIt.
- pdoTools, for form chunks on Fenom or in files.
- The browsers of
browserslistinpackage.json(current versions of every major browser): the script is built with ES2019 syntax, and CI checks it. Without JavaScript, forms with FormIt work the usual way, unless a proof of work or a captcha is on.
You need Node.js 22.22+, 24.15+ or 26+ (the CI version is in .node-version), PHP 7.4+ with Composer, and Docker.
npm ci && composer install
npm run build # src/ → assets/components/fetchit/js/: the ES2019 script and fetchit.d.ts
npm run check:syntax # the built script parses as ES2019
npm run lint # oxlint
npm run typecheck # tsc: the code, and the built public types as a site sees them (tests/types, after npm run build)
npm test # Vitest
vendor/bin/phpunit # PHPUnit
npm run screenshots # screenshots of the notifications in docs/images/, after changing their styles
npm run logo # the logo, the square mark, the pictures for modstore.pro and extras.modx.com and the social previews in .github/logo/ (needs the network for Google Fonts)Two sites, MODX 2.8.6 on PHP 7.4 and MODX 3.2.4 on PHP 8.3. The component directories of the repository are mounted in both, so changes to PHP and the built JS show at once. Changes in src/ show after npm run build.
# if your uid/gid are not 1000: export HOST_UID=$(id -u) HOST_GID=$(id -g)
docker compose up -d --build
docker compose logs -f modx2 modx3 # wait for "[fetchit] Manager: …"
# MODX 2: http://localhost:8052/, MODX 3: http://localhost:8053/ (admin / FetchItDev2026)The package is built on MODX 2 and installed on both sites the same way CI does it:
docker compose exec -u www-data -e PKG_DIST=1 modx2 php /extra/_build/build.php
docker compose exec -u www-data modx2 php /extra/_build/ci/install.php /extra/_packages/fetchit-<version>-<release>.transport.zip
docker compose exec -u www-data modx3 php /extra/_build/ci/install.php /extra/_packages/fetchit-<version>-<release>.transport.zipWithout PKG_DIST=1, build.php also installs the package on the site it is built on. To check upgrades from earlier versions there are clean sites without the directories of the repository, see compose.clean.yml.
HTTP checks and browser tests on a local site:
fixtures=$(docker compose exec -T -u www-data modx2 php /extra/_build/ci/fixtures.php)
_build/ci/smoke.sh http://localhost:8052 "$fixtures"
npx playwright install chromium
BASE_URL=http://localhost:8052 FIXTURES="$fixtures" npm run e2e- The snippet (
core/components/fetchit/elements/snippets/) outputs the chunk of the form, gives it thedata-fetchitkey (a hash of the call's properties) and the service fields of the protection, stores its properties under that key, and processes a form sent without JavaScript itself. - The plugin on
OnWebPagePrerenderadds the script to<head>when an uncached snippet call ran in the request. - The script (
src/, built intoassets/components/fetchit/js/) sends the form toaction.phpwith anX-FetchIt-Actionheader, solves the proof of work, gets the captcha's answer, shows the errors and messages, and dispatches the events. action.phpfinds the properties of the form by its key, checks the protection and runs FormIt or your snippet.FetchItGuardchecks the token, the limit, the trap, the proof of work, the fill time and the captcha (FetchItCaptcha), then firesOnFetchItBeforeProcess.
CI checks every PR: PHP syntax on 7.4 to 8.4, PHPUnit, the linter, the types, the tests, that the committed JS matches src/ and has ES2019 syntax, the workflows and shell scripts, and that the versions agree. Then it builds the package on MODX 2.8.6, installs it on MODX 2.8.6 and 3.2.4, over 1.1.3 and 3.1.4 too, and sends forms over HTTP (with anonymous sessions and without) and from a browser (Playwright): with the protection, a proof of work, Turnstile and the built-in notifications.
To release:
- Raise the version in
_build/config.inc.php(andreleasewhen needed,alpha→plfor example) andcore/components/fetchit/model/fetchit.class.php, and inpackage.jsonandpackage-lock.jsonwithnpm version X.Y.Z --no-git-tag-version. - Add a
## [X.Y.Z] - YYYY-MM-DDsection tocore/components/fetchit/docs/changelog.txt. - Merge it into
masterand push a tag:git tag vX.Y.Z && git push origin vX.Y.Z.
The workflow checks the version, builds the package, runs it on MODX 2 and 3, and only then creates the GitHub release with the package. The release notes come from the feat, fix, perf and refactor commits (conventional commits); when there are none, from the changelog section.
FetchIt 4 is developed in master. The earlier lines are no longer developed: 1.x for MODX 2 (last release 1.1.3) stays in the history of master, 3.x for MODX 3 (last release 3.1.4) in the next branch.
