# Acknowledgify

A free WordPress plugin that credits the people who built a site, through a
virtual `humans.txt` file, acknowledgment meta tags and a footer link.

Built and maintained by [Made Neat](https://madeneat.com.au).
Published at [wordpress.org/plugins/acknowledgify](https://wordpress.org/plugins/acknowledgify/).

The user-facing documentation lives in `readme.txt`, which is what
WordPress.org renders. This file covers the code.

## Requirements

| Requirement | Supported version / details |
|---|---|
| WordPress | 6.4 or later, tested to 7.1 |
| PHP | 8.0 or later |
| Licence | GPL-2.0-or-later |
| Dependencies | None. No Composer, no npm, no build step. |

## Layout

```
acknowledgify.php          Plugin header, constants, activation/deactivation
includes/
  init.php                 The only place files are required
  constants.php            Option name, schema version, admin-post actions
  helpers.php              Settings schema, defaults, allow-lists, migrations
  tokens.php               Placeholder token registry and resolver
  hooks.php                Every action, filter and shortcode registration
  humans-txt.php           Rewrite rule, file body, response headers
  meta-tags.php            <head> meta tag output
  footer-link.php          Footer markup for all three placements
  php-snippet.php          Global acknowledgify_display_madeby_footer()
admin/
  admin-menu.php           Options page registration
  admin-settings.php       Settings screen, sanitiser, export and import
  js/admin-settings.js     Tabs, credits repeater, live preview, transfer
assets/css/                Admin stylesheet (admin only, never enqueued on the front end)
languages/                 acknowledgify.pot
uninstall.php              Removes the option, multisite aware
```

## Conventions

* Namespaced classes of static methods, `require_once`'d in `includes/init.php`.
  No autoloader, by choice: the file count is small and the load order is
  explicit.
* WordPress Coding Standards. Sanitise on input, escape at the point of output
  with the context-correct function.
* Every state-changing request carries both a capability check and a nonce.
* No direct database queries. Options and Settings APIs only.
* `flush_rewrite_rules()` is called on activation, on deactivation, and when
  the humans.txt switch changes value. Never on a normal page load.

## Settings

Everything lives in one autoloaded option, `acknowledgify_settings`, holding a
flat array of scalar settings, separate `agency_credits` and
`freelancer_credits` arrays of rows, and a `_schema_version` integer.
`Helper::get_defaults()` is the single source of truth for the key list and the
default values; `Helper::get_allowed_values()` is the same for fixed-choice
fields.

### Migrations

`Helper::migrate()` takes an array and returns the migrated array without
persisting the option itself. The same routine upgrades the live option, an
imported export file and a test fixture. The upgrade path runs on `init` at
priority 5; migration also runs inside `Settings::sanitize_settings()` so an
old export can be imported directly. Migration is versioned and idempotent.

| Settings schema | Shape |
|---|---|
| 1 | Flat scalars only, 1.0.0 to 1.1.5. Agency credits were four fixed fields. |
| 2 | Shared repeatable `credits` array used by earlier development builds. Retained as an upgrade/import source format. |
| 3 | Independent `agency_credits` and `freelancer_credits` in the public 1.2.0 release. Legacy schema-1 roles go only to Agency; schema-2 rows seed both lists once. |

The public release path is 1.1.5 to 1.2.0. Settings schema numbers are separate
from plugin release numbers: keep `ACKNOWLEDGIFY_SCHEMA_VERSION` at
`3`, including when consolidating development changes into release 1.2.0.

## Filters

| Filter | Purpose |
|---|---|
| `acknowledgify_tokens` | Add or remove placeholder tokens. |
| `acknowledgify_humans_txt_labels` | Change the section and field labels in the file. |
| `acknowledgify_humans_txt_header_lines` | Change the comment block at the top of the file. |
| `acknowledgify_humans_txt_max_age` | Cache lifetime in seconds. `0` sends no-cache headers. |
| `acknowledgify_allow_query_var_route` | Allow `/?acknowledgify_humans_txt=1` to serve the file. |
| `acknowledgify_meta_tags` | Add or suppress individual meta tags. |
| `acknowledgify_role_suggestions` | Change the suggested role names in the Credits repeater. |
| `acknowledgify_max_credits` | Change the credit row cap. Default 20 per template. |
| `acknowledgify_footer_wrapper_tag` | Change the footer wrapper element. Default `div`. |
| `acknowledgify_footer_link_text` | Change the footer credit text before tokens resolve. Still passes through the tag allow-list. |
| `acknowledgify_footer_link_url` | Change the footer credit destination before tokens resolve. Still passes through `esc_url()`. |
| `acknowledgify_footer_link_rel` | Rel keywords on the footer credit. Default `nofollow`; `noopener` is always added. |

## Front-end cost

Settings reads are memoised within the current blog context. Both settings
and token caches are cleared on `switch_blog`, including when restoring the
original blog. The plugin enqueues no front-end assets and makes no external
calls. Token resolution may invoke WordPress options/theme APIs.

## Multilingual sites

WPML, Polylang and similar plugins translate content and registered strings.
Acknowledgify stores one site-wide settings option and serves one
`humans.txt` file at the site root. It ships no dedicated integration with
these plugins. The `acknowledgify_footer_link_text` and
`acknowledgify_footer_link_url` filters let site code provide a per-language
footer credit without adding a plugin dependency.

## Translations

Strings use the `acknowledgify` text domain. All translation calls sit inside
functions that run on or after `init`, which keeps WordPress 6.7's
`_load_textdomain_just_in_time` notice quiet. Nothing user-facing is defined in
a constant, because constants are evaluated before the text domain is
available.

Regenerate the catalogue before a release:

```
wp i18n make-pot . languages/acknowledgify.pot --domain=acknowledgify
```

Ship only the `.pot`. The `.po` and `.mo` files come from
translate.wordpress.org.

## Before releasing

* Run `wp plugin check acknowledgify` and resolve errors.
* Run `phpcs --standard=WordPress` and review the findings.
* Save, reload and confirm persistence on a real install; check the PHP error log.
* Confirm `/humans.txt` responds when enabled and the plugin stops serving it when disabled. If no other file or handler provides that URL, it should return 404.
* Test direct upgrades from 1.1.5: preserve Agency roles in Agency, keep them hidden in Freelancer and Custom, and confirm the selected template's output. Comment-header formatting may differ.
* Test migration from an earlier development build with schema-2 shared rows separately. Confirm both lists are populated once and subsequent edits remain independent.
* Check copying, duplicate skipping and the per-template row limit; confirm an oversized copy makes no partial changes.
* Check deliberately blank footer fields, URL tokens after saving and import, selected-tab persistence, unchanged imports, rejection of newer schemas and multisite blog switching.
* Set the plugin header `Version`, `ACKNOWLEDGIFY_VERSION`, the `Stable tag` and current changelog/upgrade-notice headings in `readme.txt` to `1.2.0`. Keep the settings schema at `3`.
* Update `Tested up to` in `readme.txt` to the WordPress version actually tested, regenerate `languages/acknowledgify.pot`, and confirm its project version is `1.2.0`.
* If publishing through WordPress.org SVN, confirm the release code in `trunk` and `tags/1.2.0` matches the intended package.

## Version 1.2.0 behavior

Current forms include `_schema_version` and emit only one WordPress referer
field so saving returns to the selected tab. URL controls accept token
expressions as text; `Tokens::sanitize_url()` masks tokens during WordPress
URL sanitisation and stores unresolved expressions, including a bare
`%%SiteURL%%`. Footer destinations still pass through `esc_url()` after token
expansion. This is intentionally not an HTML escaping bypass.

Agency and Freelancer store independent credit lists. Credit field names and
DOM IDs include the template name. Both lists remain submitted when hidden,
so saving one template preserves edits in the other. Only the selected
template's list is rendered; Custom remains free-form content.

Copying appends rows from the other template without overwriting existing
rows or changing the source list. Exact duplicates are skipped. A copy that
would exceed the limit is refused without a partial copy. The default limit
is 20 rows per template. Save changes to persist copied rows.

Normal settings saves preserve deliberately empty footer text and URLs,
suppressing the footer credit. Legacy default backfilling applies only when
migrating legacy settings. Footer links use `nofollow` by default; the rel
filter can customise the keywords, and `noopener` is always retained.

Imports verify the stored settings before reporting success, accept unchanged
settings and reject exports from a newer settings schema.

Direct upgrades from 1.1.5 and earlier convert the four legacy Agency role
fields to Agency credit rows, preserving their order without adding them to
Freelancer or Custom. Earlier development builds with shared credit rows do
not retain the origin of each row. Schema 3 therefore seeds both independent
lists once to preserve existing data. Review each list and remove unwanted
credits manually; subsequent edits remain independent.

URL tokens already damaged by an earlier save must be re-entered. Back up the
database and export settings before upgrading. Replacing plugin files with
an older version does not reverse a settings migration; restoring a
pre-upgrade database backup is required for a complete rollback.
