Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa5c939602 |
@@ -1,5 +1,39 @@
|
||||
# Nolimit News: releases
|
||||
# Nolimit News
|
||||
|
||||
This repository holds the release zips for the Nolimit News WordPress plugin, and nothing else. WordPress reads it to offer updates.
|
||||

|
||||
|
||||
Download the latest zip from Releases.
|
||||
Newsletters for WordPress and WooCommerce, without the monthly bill per
|
||||
subscriber. Write an issue in the block editor, keep your lists, put a signup
|
||||
form anywhere, send it through your own mail setup, and show every issue as an
|
||||
archive on your own site. **Nothing leaves your site** — no click tracking, no
|
||||
telemetry, no account, no paid tier.
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `nolimit-news.zip` from the [latest release](https://gitea.nolimit.dk/kw/nolimit-news-dist/releases/latest).
|
||||
2. **Plugins → Add New → Upload Plugin**, then activate.
|
||||
3. Set up a real mail sender before the first send — [the setup guide](docs/SETUP.md)
|
||||
says why, and walks through the first ten minutes.
|
||||
|
||||
Later versions arrive in **Dashboard → Updates** like any other plugin.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Overview](docs/README.md) — what it does, and what it deliberately does not
|
||||
- [Setting it up, and knowing it works](docs/SETUP.md) — mail, the first issue,
|
||||
importing from Mailchimp, and what to do when something is wrong
|
||||
- [Hooks and integration](docs/CONTRACT.md) — for developers: what the plugin
|
||||
offers to other plugins and themes, and what it stores
|
||||
|
||||
## More of it
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  | **Sending.** Preview, test, send now or schedule; how many were mailed and, if you switch it on, how many opened it. A send that fails stops itself and picks up where it left off. |
|
||||
|  | **Lists.** Public lists the signup form offers and private ones you fill yourself, with pending, subscribed and unsubscribed counted apart. |
|
||||
|  | **Subscribers.** Double opt-in, so *confirmed* means the person followed their own link — and an import from Mailchimp that asks people to confirm. |
|
||||
|  | **Templates.** Plain HTML files: use the shipped ones, drop your own in the theme, or paste one an AI designed from the guide the plugin hands you. |
|
||||
|  | **Archive.** Every sent issue on your own site, in your theme's frame, and a block that lists the latest anywhere. |
|
||||
|
||||
Requires WordPress 5.9+ and PHP 7.4+. WooCommerce is not required; with it,
|
||||
an issue can carry live product cards.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,322 @@
|
||||
# The surface Nolimit News exposes
|
||||
|
||||
<!-- nlnws:version 0.2.1 -->
|
||||
|
||||
Everything another plugin, a theme or a site owner can bind to, store against,
|
||||
or break by changing. For developers; a site owner never needs to read it.
|
||||
|
||||
"Shipped source" means the files in the plugin zip. Anything else in the
|
||||
project is not on a site and is not part of this contract.
|
||||
|
||||
## Hooks this plugin provides
|
||||
|
||||
**None yet.** This plugin fires no `do_action()` and applies no
|
||||
`apply_filters()` of its own, so there is nothing here for anyone to hook.
|
||||
|
||||
That is a state, not a promise. Every extension point outlives the reason it
|
||||
was added, so each goes into this table in the same commit as the code.
|
||||
`nlnws_` is the prefix, and the release checks fail if a hook with it exists
|
||||
in the code and not in this table, or the other way round.
|
||||
|
||||
<!-- nlnws:hooks:start -->
|
||||
|
||||
## Hooks this plugin registers on
|
||||
|
||||
Deliberately not headed "consumes": `add_filter()` is how you **answer** a
|
||||
filter, and reading it as consumption gets the direction of the promise
|
||||
backwards. For an action the word is *listens*; for a filter it is *answers*.
|
||||
Priority and accepted-argument count are given where they are not WordPress'
|
||||
defaults of `10` and `1`, because those two are what break silently.
|
||||
|
||||
| Hook | Kind | Registered in | Why |
|
||||
| --- | --- | --- | --- |
|
||||
| `admin_init` | action | `includes/class-updater.php` | Drops the cached feed when the user presses "Check again". |
|
||||
| `admin_init` | action | `includes/class-privacy.php` | Offers a paragraph for Settings → Privacy → Policy Guide. |
|
||||
| `admin_enqueue_scripts` | action | `includes/class-admin.php` | Loads `assets/css/admin.css`, on this plugin's screen only. |
|
||||
| `admin_menu` | action | `includes/class-admin.php` | Registers the Newsletters screen, and a link to it in the shared Nolimit menu. |
|
||||
| `admin_post_nlnws_auto_update` | action | `includes/class-auto-update.php` | The auto-update toggle's form target. |
|
||||
| `admin_post_nlnws_export` | action | `includes/class-subscribers-screen.php` | Streams the subscriber CSV. A GET link carrying a nonce; nothing is written. |
|
||||
| `admin_post_nlnws_issue_action` | action | `includes/class-send-screen.php` | Send now, schedule, test send, stop, resume, or choose the template for one issue. |
|
||||
| `admin_post_nlnws_settings_save` | action | `includes/class-templates-screen.php` | Saves the brand colour, logo, postal address and default template. |
|
||||
| `admin_post_nlnws_template_delete` | action | `includes/class-templates-screen.php` | Removes one pasted-in template. |
|
||||
| `admin_post_nlnws_template_save` | action | `includes/class-templates-screen.php` | Checks a pasted-in template and stores it, or refuses it with the reasons. |
|
||||
| `admin_post_nlnws_list_delete` | action | `includes/class-lists-screen.php` | Deletes a list and its memberships. Subscribers stay. |
|
||||
| `admin_post_nlnws_list_save` | action | `includes/class-lists-screen.php` | Creates or renames a list. |
|
||||
| `admin_post_nlnws_forms_save` | action | `includes/class-forms-bridge.php` | Saves which Nolimit Forms form signs people up to which list. |
|
||||
| `admin_post_nlnws_import` | action | `includes/class-import.php` | Import a CSV file (a Mailchimp audience export, or any file with an address column): subscribed, unsubscribed or cleaned. |
|
||||
| `admin_post_nlnws_reconfirm` | action | `includes/class-import.php` | Mail the next 50 imported, unconfirmed people and ask whether they still want the newsletter. |
|
||||
| `admin_post_nopriv_nlnws_open` | action | `includes/class-opens.php` | The open-counting pixel, for a logged-out reader. Answers the same 1x1 GIF whether or not anything was recorded. A GET that records, deliberately: it changes no consent. |
|
||||
| `admin_post_nlnws_open` | action | `includes/class-opens.php` | The same, for a reader who happens to be logged in. |
|
||||
| `admin_post_nopriv_nlnws_confirm` | action | `includes/class-public.php` | The link in the confirmation mail, for a logged-out visitor. GET shows a button; only POST confirms. |
|
||||
| `admin_post_nlnws_confirm` | action | `includes/class-public.php` | The same, for a visitor who happens to be logged in. |
|
||||
| `admin_post_nopriv_nlnws_signup` | action | `includes/class-public.php` | The signup form's target, for a logged-out visitor. |
|
||||
| `admin_post_nlnws_signup` | action | `includes/class-public.php` | The same, for a logged-in visitor. |
|
||||
| `admin_post_nopriv_nlnws_unsubscribe` | action | `includes/class-public.php` | The unsubscribe link, and RFC 8058's one-click POST to it. GET shows a button; only POST unsubscribes. |
|
||||
| `admin_post_nlnws_unsubscribe` | action | `includes/class-public.php` | The same, for a logged-in visitor. |
|
||||
| `nlfrm_submission` | action, 3 args | `includes/class-forms-bridge.php` | Nolimit Forms' announcement of an accepted, non-spam submission. Acts only for a form the site owner mapped to a list, and only when `NLFRM_VERSION` is 0.1.7 or later. The person becomes pending and is mailed a confirmation. |
|
||||
| `admin_post_nlnws_subscriber_action` | action | `includes/class-subscribers-screen.php` | Unsubscribe, suppress, lift suppression, or delete one person. |
|
||||
| `admin_post_nlnws_subscriber_add` | action | `includes/class-subscribers-screen.php` | Adds one person by hand, as `subscribed` with no `confirmed_at`. |
|
||||
| `nlnws_t_paste_<user id>` | transient, 15 minutes | A refused template paste and the reasons, so the box on the Templates tab can be filled in again. Only the administrator who pasted it sees it. |
|
||||
| `nlnws_purge_pending` | action | `includes/class-plugin.php` | The daily purge of signups nobody confirmed in 30 days. Scheduled on load, cancelled on deactivate and on uninstall. |
|
||||
| `cron_schedules` | filter | `includes/class-send.php` | Adds a one-minute interval, `nlnws_minute`, because a mailing cannot wait an hour per batch. |
|
||||
| `nlnws_send_tick` | action | `includes/class-send.php` | The one-minute tick that starts due scheduled sends and mails one batch of the oldest send in progress. Scheduled on load, cancelled on deactivate and on uninstall. |
|
||||
| `init` | action | `includes/class-issues.php` | Registers the issue post type, and flushes the rewrite rules once per plugin version. |
|
||||
| `plugin_action_links_<basename>` | filter | `includes/class-admin.php` | The "Newsletters" link on the Plugins screen. |
|
||||
| `plugins_api` | filter, 3 args | `includes/class-updater.php` | The "View details" panel. Guarded on `$args->slug`. |
|
||||
| `plugins_loaded` | action | `includes/class-plugin.php` | Everything else is registered from here. |
|
||||
| `update_plugins_<hostname>` | filter, 3 args | `includes/class-updater.php` | Offers this plugin's own updates, and only its own: see below. |
|
||||
| `upgrader_process_complete` | action, 0 args | `includes/class-updater.php` | Forgets the cached feed after any update runs. |
|
||||
| `wp_privacy_personal_data_erasers` | filter | `includes/class-privacy.php` | Deletes a person's address and consent rows on an erasure request. |
|
||||
| `wp_privacy_personal_data_exporters` | filter | `includes/class-privacy.php` | Exports everything held about an address, with the consent dates. |
|
||||
|
||||
## The family's shared hooks
|
||||
|
||||
These belong to the Nolimit family, not to this plugin.
|
||||
|
||||
| Hook | This plugin's relationship | Where |
|
||||
| --- | --- | --- |
|
||||
| `wpmgr_dashboard_cards` | **answers**: supplies this plugin's card | `includes/class-admin.php` |
|
||||
| `wpmgr_dashboard_cards` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
||||
| `wpmgr_menu_title` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
||||
| `wpmgr_menu_glyph` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
||||
|
||||
<!-- nlnws:hooks:end -->
|
||||
|
||||
Shipping a file is not owning its hooks. `includes/shared-menu.php` is
|
||||
byte-identical across the family and frozen by SHA256; whichever copy loads
|
||||
first declares the functions, so on a real site the three `wpmgr_*` filters
|
||||
are applied by *a* copy of that file, probably another plugin's.
|
||||
|
||||
### 🔴 `update_plugins_<hostname>` is shared by every plugin on that host
|
||||
|
||||
The filter name is built from the hostname, not the repository, so every plugin
|
||||
whose `Update URI` is on gitea.nolimit.dk lands on the same one and
|
||||
WordPress runs every callback for every one of them. Answering unconditionally
|
||||
offers this plugin's release to a neighbour. The fix has two halves: register with three accepted arguments,
|
||||
**and** return early when `$plugin_file` is not this plugin. Leave the count at
|
||||
two and the argument never arrives, so the guard is dead code. Both halves are
|
||||
covered by a test that fails if either is removed.
|
||||
|
||||
### The public endpoints
|
||||
|
||||
`admin-post.php?action=nlnws_confirm&t=<token>` and
|
||||
`admin-post.php?action=nlnws_unsubscribe&t=<token>[&l=<list id>]` are the two
|
||||
links in every mail this plugin sends. They need no login and no nonce, because
|
||||
the 32-hex-character token is the authority and it can only act on its owner's
|
||||
own subscription. **A GET never changes anything**: it draws a page with a
|
||||
button, and only a POST acts. A mail scanner fetches every link in a message,
|
||||
and a GET that confirmed would let it confirm people who never asked. An
|
||||
unsubscribe POST is also what RFC 8058's `List-Unsubscribe-Post` sends, so the
|
||||
same URL serves the mail client's own button. An unknown, malformed or
|
||||
suppressed token answers 404 with a page that says nothing about which.
|
||||
|
||||
The signup form (`admin-post.php?action=nlnws_signup`) answers the same
|
||||
whether the address is new, already subscribed, suppressed or throttled, so it
|
||||
cannot be used to learn who is on a list. It accepts only lists marked public,
|
||||
whatever the request names, and mails one confirmation per address per hour.
|
||||
|
||||
### Shortcode
|
||||
|
||||
`[nolimit_news_signup lists="slug,slug" name="yes" button="Subscribe"]` draws the
|
||||
signup form. All attributes are optional: with no `lists` every public list is
|
||||
offered, one list becomes a hidden field, and boxes are never pre-ticked. The
|
||||
notice after a signup is chosen by `?nlnws=sent|bad`; any other value shows
|
||||
nothing.
|
||||
|
||||
### Blocks for the website
|
||||
|
||||
`nolimit-news/signup` draws the form `[nolimit_news_signup]` draws, from the
|
||||
same code, with attributes `lists` (slugs; none means every public list),
|
||||
`name` (ask for a name) and `button`. `nolimit-news/archive` lists published
|
||||
issues, newest first, with `count` (1 to 50), `dates` and `excerpt`. Both are
|
||||
dynamic and keep only their attributes. A draft or scheduled issue is never
|
||||
listed, whatever the block is told, and the editor script that registers them
|
||||
is not loaded on an issue's own screen. No hook is added.
|
||||
|
||||
### Signup from Nolimit Forms
|
||||
|
||||
`NLNWS_Forms_Bridge` listens to `nlfrm_submission` (`$values`, `$form`,
|
||||
`$entry_id`).
|
||||
It never acts unless the site owner has mapped that form to a list, Nolimit
|
||||
Forms is loaded and `NLFRM_VERSION` is 0.1.7 or later (the hook is not fired
|
||||
for spam-filed entries from that version; earlier ones would hand a bot's
|
||||
address to a mailing list). The email comes from the mapped field, or the
|
||||
form's first `email` field. When a consent field is mapped its value must be
|
||||
`1`. The address goes through `NLNWS_Subscribers::subscribe()` with source
|
||||
`form`, so suppression, unsubscribed people and double opt-in behave exactly
|
||||
as they do for the signup form, and a mapping may point at a list the signup
|
||||
form does not offer. Nothing is stored beyond what a signup stores, and
|
||||
nothing is added to the form's own entry.
|
||||
|
||||
### Who may use the screens
|
||||
|
||||
Every screen and every `admin_post_nlnws_*` handler requires `manage_options`
|
||||
(`NLNWS_Admin::CAP`), checked first and answered with a 403, and then a nonce.
|
||||
A logged-in user without the capability who posts to `admin-post.php` gets the
|
||||
403 and nothing is read or written; The test suite posts each handler as a
|
||||
subscriber-role user and asserts that. The CSV export is a subscriber list, so
|
||||
it is behind the same gate rather than being a lighter one for a read.
|
||||
|
||||
## Options and storage
|
||||
|
||||
Everything here is removed by `uninstall.php` on **delete**, not on deactivate:
|
||||
switching the plugin off for ten seconds must not destroy a mailing list. The
|
||||
subscriber tables hold personal data with no other owner, so a deleted plugin
|
||||
takes them with it. The test suite runs `uninstall.php` and asserts every
|
||||
row of this table is gone.
|
||||
|
||||
| Name | Kind | Holds |
|
||||
| --- | --- | --- |
|
||||
| `{prefix}nlnws_lists` | table | The lists: name, slug, description, whether the signup form offers it. |
|
||||
| `{prefix}nlnws_subscribers` | table | One row per address, stored lower-case: name, the secret token in that person's confirm and unsubscribe links, and a suppression reason. |
|
||||
| `{prefix}nlnws_memberships` | table | One row per person per list: status (`pending`, `subscribed`, `unsubscribed`), how they joined (`form`, `admin`, `import`), when they asked, when they confirmed, when they left. |
|
||||
| `{prefix}nlnws_opens` | table | Only when open counting is on: one row per person per issue with the two ids, the first and last time an image was loaded, and how many times. Deleted with the issue, with the person, and on uninstall. |
|
||||
| `nlnws_db_version` | option | The schema version the tables were last built at. |
|
||||
| `nlnws_rewrite_version` | option | The plugin version the rewrite rules were last flushed at, so the `/newsletters/` archive works after an update without a visit to Settings → Permalinks. |
|
||||
| `nlnws_settings` | option | Brand colour, logo URL and the postal address printed in every issue's footer. |
|
||||
| `nlnws_forms` | option | Which Nolimit Forms forms sign people up: form id => list id, email field id and consent field id (the last two may be empty). |
|
||||
| `nlnws_templates` | option | Email templates pasted in by an admin: slug => HTML. Each is validated on the way in and again when read. |
|
||||
| `_nlnws_template` | post meta on an issue | The slug of the template this issue is mailed in. Empty or unknown means the site default in `nlnws_settings`. |
|
||||
| `nlnws_send_lock` | option | The time a tick took the send lock, so two ticks never mail at once. A lock older than five minutes is taken over. Absent between ticks. |
|
||||
| `nlnws_send_tick` | WP-Cron event, every minute | Drives all sending and scheduled starts. |
|
||||
| `_nlnws_state`, `_nlnws_lists`, `_nlnws_send_at`, `_nlnws_cursor`, `_nlnws_total`, `_nlnws_sent`, `_nlnws_failed`, `_nlnws_started`, `_nlnws_finished`, `_nlnws_last_batch`, `_nlnws_error` | post meta on an issue | The send state: `''`, `scheduled`, `sending`, `stopped` or `sent`; the chosen lists; the start time; the id of the last subscriber mailed; the counts; the times; and the last mailer error. There is no per-message record. |
|
||||
| `nlnws_auto_update_seeded` | option | That the one-time auto-update offer was made. |
|
||||
| `nlnws_release_feed` | transient | The cached release feed. |
|
||||
| `nlnws_t_<32 hex>` | transient, one hour | "This address was just mailed a confirmation". The name is a keyed hash of the address, so the address is not written to the options table. |
|
||||
| `nlnws_purge_pending` | WP-Cron event, daily | Deletes a pending membership older than 30 days, and the person too when nothing else is left and they are not suppressed. |
|
||||
|
||||
The admin screens store nothing of their own: they read and write the tables
|
||||
above. A notice after a save is carried in the redirect's query string as
|
||||
a short code and printed from a fixed list, so a crafted URL cannot put words
|
||||
on the screen.
|
||||
|
||||
No IP address, user agent or referrer is stored anywhere, and no click is
|
||||
recorded. **Open counting is an option, off by default** (`track_opens` in
|
||||
`nlnws_settings`). When it is on, each mail to a person carries one 1x1 image at
|
||||
`admin-post.php?action=nlnws_open&i=<issue>&t=<token>`, and loading it writes
|
||||
that person's row in `nlnws_opens`. Nothing is read from the request except the
|
||||
issue and the token, so there is nothing else to store, and nothing leaves the
|
||||
site. Turning it off stops recording at once, for mail already sent too, and
|
||||
removes the pixel from every mail after it. A preview, a test send and the
|
||||
plain-text twin have no person and so no pixel. The number is an estimate: some
|
||||
mail programs load images for the reader and others never do. The suggested
|
||||
privacy text and the signup form's fine print change with the setting.
|
||||
`confirmed_at` is set only when the person followed the link in the
|
||||
confirmation mail; a row with `source = import` and no `confirmed_at` is
|
||||
visibly one nobody confirmed, which is the difference between consent you can
|
||||
show and consent you are asserting.
|
||||
|
||||
### Import
|
||||
|
||||
Subscribers → Import reads a CSV export. Only the address and the name are
|
||||
read (`First Name` and `Last Name` are joined; Danish Mailchimp headers, our own
|
||||
export, and comma, semicolon or tab separators are understood). The IP address,
|
||||
location and rating columns Mailchimp adds are not read and not stored, and the
|
||||
file itself is never kept. The screen makes the person importing tick that the
|
||||
people agreed to receive the newsletter. Three kinds of file:
|
||||
|
||||
* **Subscribed**: joined as `subscribed`, `source = import`, no `confirmed_at`.
|
||||
Somebody who unsubscribed here, or whose address is suppressed, is skipped and
|
||||
counted; an import can never undo a person's own decision.
|
||||
* **Unsubscribed**: recorded as already unsubscribed, so a later subscribed file
|
||||
cannot bring them back. An existing membership is never changed by it.
|
||||
* **Cleaned**: the addresses are suppressed as `bounced`, on no list.
|
||||
|
||||
A request works for 20 seconds and then stops and says which row to start at;
|
||||
the file can be uploaded again from there, and rows already in are counted as
|
||||
"already there". Files over 10 MB are refused. Separately, "Ask the imported
|
||||
people" mails each imported, unconfirmed person once (50 per click, one mail per
|
||||
address per hour) with their own confirm link. They stay subscribed whether they
|
||||
answer or not; the link only stamps `confirmed_at`. The place it has reached is
|
||||
the option `nlnws_reconfirm_cursor`, removed on uninstall and when the last
|
||||
batch is done.
|
||||
|
||||
A signup nobody confirms is deleted after 30 days; an address that is suppressed is
|
||||
never deleted by it, or it could be mailed again.
|
||||
|
||||
### Sending
|
||||
|
||||
An issue is `''` until sent, then `scheduled` (waiting for `_nlnws_send_at`),
|
||||
`sending`, `stopped` (by hand, or by the mailer failing the first five
|
||||
messages in a row) or `sent`. A send mails the people who are `subscribed` to
|
||||
any chosen list and not suppressed, once each however many of the lists they
|
||||
are on, in id order from the cursor, so a resume never mails anybody twice. The
|
||||
audience is read each batch: somebody who unsubscribes after the send began is
|
||||
not mailed. A failed address is counted and passed over, never retried. Each
|
||||
message goes through `wp_mail()` as HTML with a plain-text alternative and
|
||||
carries `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`
|
||||
for that person. When the last batch is done the issue is published, which puts
|
||||
it in the public archive. WP-Cron on a quiet site fires only when somebody
|
||||
visits, so a scheduled time is "not before", never "at".
|
||||
|
||||
### The email template
|
||||
|
||||
A template is one HTML file: a header comment (`Template:` is required;
|
||||
`Description:`, `Text:`, `Muted:`, `Line:` and `Font:` are optional), then a
|
||||
complete `<html>` document that uses `{{tokens}}`. `{{{content}}}` (three
|
||||
braces, unescaped) is the issue's body and `{{unsubscribe_url}}` is required.
|
||||
Sections `{{#token}}...{{/token}}` show when the value is not empty and
|
||||
`{{^token}}...{{/token}}` when it is. The full grammar, every token and a
|
||||
working example are in `templates/AUTHORING.md`, which is written to be handed
|
||||
to an AI that designs the template; a test keeps that guide and the token list
|
||||
in step.
|
||||
|
||||
Templates come from `templates/*.html` in the plugin, then
|
||||
`nolimit-news/*.html` in the parent and child theme, then the pasted ones in
|
||||
`nlnws_templates`; a later source with the same slug replaces an earlier one.
|
||||
**A template that fails its checks is skipped and reported, never sent**: no
|
||||
script, iframe, form, link, `@import`, `javascript:`, PHP or `on*=` handler, no
|
||||
token that does not exist, no unclosed section, at most 40 000 bytes. The
|
||||
refusal is worded so that an AI can fix what it names. The shipped files are
|
||||
`default` and `minimal`; the token set may grow, and a token is never renamed.
|
||||
|
||||
### The composer
|
||||
|
||||
Writing an issue is the block editor, narrowed. On an `nlnws_issue` screen only
|
||||
the blocks that survive a mail client are offered: paragraph, heading, list,
|
||||
quote, image, separator, and two of our own, `nolimit-news/product` and
|
||||
`nolimit-news/button`. Everything else is hidden there and untouched on every
|
||||
other post type. Both blocks are dynamic (the editor stores only their
|
||||
attributes, never markup), so a product's price and name are read when the mail
|
||||
is drawn and a template change reaches issues already written.
|
||||
|
||||
`nolimit-news/product` shows a live WooCommerce product when `productId` names a
|
||||
published one, and otherwise the fields typed in by hand, so it works on a site
|
||||
without a shop; a draft, trashed or missing product draws nothing rather than a
|
||||
broken card. The button takes the issue's brand colour and picks black or white
|
||||
text by contrast. Both draw single-line tables with inline styles, which is
|
||||
what mail clients keep.
|
||||
|
||||
The editor's product list is `GET /wp-json/nolimit-news/v1/products?search=`,
|
||||
which needs `manage_options` (`NLNWS_Admin::CAP`) and answers 401 or 403
|
||||
otherwise. It returns `woocommerce` (whether a shop is active) and at most 12
|
||||
published products. Dragging a product from that list into the text, or clicking
|
||||
it, inserts the block; image files are dropped onto the editor as on any post.
|
||||
No hook is added for any of this.
|
||||
|
||||
### The issue post type
|
||||
|
||||
`nlnws_issue` is a public post type: a newsletter is a post, with a permalink,
|
||||
revisions and the block editor. Its archive is at `/newsletters/` and each
|
||||
issue at `/newsletters/<slug>/`. **A draft is never public; a published issue
|
||||
is.** Every primitive capability of the type is `manage_options`, so an editor
|
||||
who can write posts cannot write, publish or delete an issue; reading a
|
||||
published one needs only `read`. Issues appear as a submenu of the Newsletters
|
||||
page. The send state above is stored on it as protected post meta.
|
||||
|
||||
Deleting the plugin deletes every issue, trashed ones and their revisions
|
||||
included. It leaves an ordinary post alone.
|
||||
|
||||
## Constants
|
||||
|
||||
| Name | Meaning |
|
||||
| --- | --- |
|
||||
| `NLNWS_VERSION` | The version, equal to the header's. |
|
||||
| `NLNWS_COLOUR` | The card colour, `#4a4fc4`, measured 6.55:1 against white. It exists three times, here, as `--nlnws-accent` in `assets/css/admin.css` and as the tile fill in `assets/icon.svg`; the release checks assert all three agree. |
|
||||
| `NLNWS_PLUGIN_FILE`, `NLNWS_PLUGIN_DIR`, `NLNWS_PLUGIN_URL`, `NLNWS_PLUGIN_BASENAME` | Paths, for this plugin's own use. |
|
||||
|
||||
## What a consumer must not do
|
||||
|
||||
Read this plugin's tables directly, once they exist: the schema is not part of
|
||||
the contract and will change. Call a class of ours; only the hooks above are
|
||||
promises.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Nolimit News
|
||||
|
||||
<!-- nlnws:version 0.2.1 -->
|
||||
|
||||
Newsletters for WordPress and WooCommerce: write an issue in the block editor,
|
||||
keep your lists, put a signup form anywhere, send it through your own mail
|
||||
setup, and show every issue as an archive on your own site. Written to replace
|
||||
Mailchimp for a shop that does not want a monthly bill per subscriber.
|
||||
|
||||
## What it does
|
||||
|
||||
* **Write** an issue in the block editor, inside a template, with images,
|
||||
columns, buttons and products from your shop.
|
||||
* **Lists and subscribers**, with double opt-in, a one-click unsubscribe in every
|
||||
mail, and a CSV export whenever you want your list back.
|
||||
* **Sign-up** through a block or a shortcode, and from a Nolimit Forms form you
|
||||
connect to a list.
|
||||
* **Send** now or at a time you choose, to one list or several, to each person
|
||||
once. A test send goes to one address and changes nobody.
|
||||
* **An archive** of every sent issue on your own site, in your theme's own
|
||||
frame, and a block that lists the latest issues on any page.
|
||||
* **Templates** that are plain HTML files: use the two that come with it, drop
|
||||
your own into your theme, or paste one in that an AI designed from the guide
|
||||
the plugin hands you.
|
||||
* **Import** a Mailchimp export (subscribed, unsubscribed and cleaned files) and
|
||||
ask the imported people to confirm, so leaving Mailchimp is not a weekend.
|
||||
|
||||
## What it refuses to do
|
||||
|
||||
* **No click tracking.** No link in a mail is rewritten, so no address is
|
||||
routed through anyone.
|
||||
* **No phone-home.** Nothing about your site or your subscribers is sent to
|
||||
anyone. There is no account and no paid tier.
|
||||
* **No opening-counting unless you switch it on.** It is an option, off by
|
||||
default. When it is on, it records who opened which issue in your own
|
||||
database and tells nobody else, and the suggested privacy text changes with
|
||||
it.
|
||||
* **Nothing is mailed to somebody who has not agreed.** A signup is pending until
|
||||
the person follows the link in the confirmation mail, and a pending signup
|
||||
nobody confirms is deleted after 30 days.
|
||||
|
||||
## Where to start
|
||||
|
||||
1. Install it and set up your mail: [SETUP.md](SETUP.md).
|
||||
2. Make a list, put the signup block on a page, write an issue, send yourself a
|
||||
test, send it.
|
||||
3. Developers building on it: [CONTRACT.md](CONTRACT.md) lists every hook,
|
||||
option, table and constant, and what is deliberately not promised.
|
||||
|
||||
## Documentation
|
||||
|
||||
| File | For |
|
||||
| --- | --- |
|
||||
| [SETUP.md](SETUP.md) | Installing it, the few settings that need a decision, and checking that it works. |
|
||||
| [CONTRACT.md](CONTRACT.md) | Developers: what it offers other plugins and themes, what it stores, and what it asks of them. |
|
||||
|
||||
Requires WordPress 5.9+ and PHP 7.4+. WooCommerce is not required; with it
|
||||
active, an issue can carry live product cards.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Setting up Nolimit News
|
||||
|
||||
<!-- nlnws:version 0.2.1 -->
|
||||
|
||||
## Installing it
|
||||
|
||||
| Needs | Version |
|
||||
| --- | --- |
|
||||
| WordPress | 5.9 or newer |
|
||||
| PHP | 7.4 or newer |
|
||||
| WooCommerce | not needed |
|
||||
|
||||
1. Download `nolimit-news.zip` from the
|
||||
[latest release](https://gitea.nolimit.dk/kw/nolimit-news-dist/releases/latest).
|
||||
2. **Plugins → Add New → Upload Plugin**, then activate.
|
||||
3. Open **Newsletters** in the admin menu.
|
||||
|
||||
Later versions arrive in **Dashboard → Updates** like any other plugin: it
|
||||
checks its own release feed and is never looked up on wordpress.org. The first
|
||||
time you open its screen it offers to switch automatic updates on for it; that
|
||||
is WordPress' own setting, in WordPress' own place, and you can say no.
|
||||
|
||||
## Making mail actually leave
|
||||
|
||||
This plugin hands every message to WordPress (`wp_mail()`), so mail leaves
|
||||
however the rest of your site's mail leaves. On most hosting that is the
|
||||
server's own mail function, which is often slow, rate-limited or marked as spam.
|
||||
For a newsletter, set up a real sender first: an SMTP account or a mail service,
|
||||
through any SMTP plugin, or Nolimit Send. Without that, a test send may arrive
|
||||
in a spam folder or not at all, and a large send may be cut off by your host's
|
||||
hourly limit. The screen tells you when mail fails: if the first five messages
|
||||
in a row are refused, the send stops by itself rather than working through your
|
||||
whole list.
|
||||
|
||||
Sending runs from WordPress' background scheduler (WP-Cron), one batch a minute.
|
||||
On a quiet site that only runs when somebody visits, so a scheduled time means
|
||||
"not before", never "exactly at". If WP-Cron is switched off on your site, the
|
||||
Send screen says so, and your server has to call `wp-cron.php` itself.
|
||||
|
||||
## The first ten minutes
|
||||
|
||||
1. **Templates → How every issue looks.** Set the brand colour, the address of
|
||||
a public logo image, and your postal address. The address is printed at the
|
||||
foot of every mail: marketing mail is expected to say who sent it and where
|
||||
they are. Pick the default template.
|
||||
2. **Lists → New list.** Give it a name and a line about what people are signing
|
||||
up for. Leave "Offer this list on signup forms" ticked for lists the public
|
||||
may join; untick it for one you only fill yourself.
|
||||
3. **Put the form on a page.** Add the *Newsletter signup* block, or the
|
||||
shortcode `[nolimit_news_signup]`. With no list named, it offers every public
|
||||
list.
|
||||
4. **Write an issue.** On the Send tab, **Write a new issue**. It is the block
|
||||
editor with only the blocks that survive an email program: paragraphs,
|
||||
headings, lists, quotes, images, separators, columns, a button and a product
|
||||
card. Open the *Add to the issue* panel and drag a product in, or click it.
|
||||
5. **Preview, then send a test** to your own address. Look at it on a phone.
|
||||
6. **Send now**, or **Send not before** a time. Pick the lists; a person on
|
||||
several of them is mailed once.
|
||||
|
||||
When the last message has gone, the issue is published, which puts it in the
|
||||
archive at `/newsletters/` on your site. The *Newsletter archive* block lists
|
||||
the latest issues on any page.
|
||||
|
||||
## Checking that it works
|
||||
|
||||
* **A signup arrives as pending**, and you receive the confirmation mail. Follow
|
||||
its link: a page with a button appears (a mail scanner that opens the link
|
||||
cannot confirm anyone by accident), and pressing it makes the person
|
||||
*subscribed*.
|
||||
* **The unsubscribe link in a test send goes nowhere**, by design. In a real send
|
||||
it takes one click, and the person is not mailed again.
|
||||
* **Mailed N of N** on the issue's Send panel counts what was handed to your mail
|
||||
system. It cannot know what the receiving servers did with it.
|
||||
|
||||
## Leaving Mailchimp
|
||||
|
||||
**Subscribers → Import** reads the CSV files Mailchimp lets you export. Do the
|
||||
three separately:
|
||||
|
||||
* **Subscribed** people join as subscribed. You tick a box to say they agreed to
|
||||
receive your newsletter.
|
||||
* **Unsubscribed** people are recorded as unsubscribed, so no later import can
|
||||
bring them back.
|
||||
* **Cleaned** addresses are suppressed and mailed nothing.
|
||||
|
||||
Only the address and the name are read, and the file itself is not kept. A large
|
||||
file is handled in steps; the screen says which row to carry on from. Imported
|
||||
people stay visibly *not confirmed* until they follow a link, and **Ask the
|
||||
imported people** mails them in batches of 50 to do exactly that. They stay
|
||||
subscribed whether or not they answer.
|
||||
|
||||
## Opens, privacy and the law
|
||||
|
||||
Counting opens is **off** until you switch it on under *How every issue looks*.
|
||||
Switching it on adds one invisible image to each mail and records, in your own
|
||||
database, that this person's copy was opened. It is an estimate: some mail
|
||||
programs load images for the reader and others never do. Switching it off stops
|
||||
recording at once and removes the image from later mail. Nothing is ever sent to
|
||||
anyone else.
|
||||
|
||||
Settings → Privacy → Policy Guide gets a paragraph that matches your choice.
|
||||
Personal data exports and erasure requests from WordPress cover the lists: an
|
||||
export includes the dates the person asked and confirmed, and an erasure deletes
|
||||
the address and its consent records.
|
||||
|
||||
You are responsible for having consent to mail your list. The plugin helps by
|
||||
keeping the proof: *confirmed* means the person followed their own link, and an
|
||||
imported person who has not is shown as one you are asserting rather than one
|
||||
you can show.
|
||||
|
||||
## Deleting it
|
||||
|
||||
Deactivating keeps your lists, subscribers and issues. **Deleting** the plugin
|
||||
removes its tables, its settings and every issue, because the subscriber lists
|
||||
hold personal data with no other owner. Export what you want first
|
||||
(**Subscribers → Export these as CSV**).
|
||||
|
||||
## If something is wrong
|
||||
|
||||
| You see | It means |
|
||||
| --- | --- |
|
||||
| A template "was found and not used" | It broke a rule (a script, a link tag, a missing unsubscribe token). The message says which; fix that and save again. |
|
||||
| *Stopped* with "The mail system said:" | Your mail setup refused the first messages. Fix it, then **Carry on where it stopped**; nobody is mailed twice. |
|
||||
| `/newsletters/` shows a 404 | Open Settings → Permalinks and press Save once. |
|
||||
| Scheduled issue did not go at the time | WP-Cron fires on a visit. Visit the site, or have the server call `wp-cron.php` every minute. |
|
||||
Reference in New Issue
Block a user