1 Commits
Author SHA1 Message Date
Nolimit release fa5c939602 Update the public pages to 0.2.1 2026-10-07 08:16:41 +00:00
10 changed files with 542 additions and 3 deletions
+37 -3
View File
@@ -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. ![Nolimit News — writing an issue with columns and shop products](assets/hero.png)
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 an issue](assets/screenshots/sending.png) | **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](assets/screenshots/lists.png) | **Lists.** Public lists the signup form offers and private ones you fill yourself, with pending, subscribed and unsubscribed counted apart. |
| ![Subscribers](assets/screenshots/subscribers.png) | **Subscribers.** Double opt-in, so *confirmed* means the person followed their own link — and an import from Mailchimp that asks people to confirm. |
| ![Templates](assets/screenshots/templates.png) | **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. |
| ![The archive](assets/screenshots/archive.png) | **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.
BIN
View File
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

+322
View File
@@ -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.
+58
View File
@@ -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
View File
@@ -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. |