TIAA WordPress Plugin
WordPress plugin in support of tiaa-forum.org
by Lew Grothe and TIAA Forum Admin Platform sub-team. · github.com/tiaa-forum-org/tiaa-wpplugin · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/tiaa-forum-org/tiaa-wpplugin/releases/download/v0.0.19/tiaa-wpplugin.zipOverview
The TIAA WordPress Plugin (from tiaa-forum.org) is designed to provide essential functionality for the TIAA-Forum.org community, integrating key features to enhance user experience, streamline administrative processes, and improve interactions with external the Discourse server.
This plugin consolidates various functions previously handled by Google Apps Scripts and third-party WordPress plugins, aiming for a more maintainable and WordPress-native solution.
Features
1. User Invitation Management
- Replaces the existing Google Apps Script-based invitation system.
- Provides a form-based invite process within WordPress.
- Integrates with Discourse to automate invitation processing.
- Allows invitation to a particular Discourse group to allow for special onboarding or to allow changing the home group for members of a particular primary group
- Detects duplicate email addresses and directs users to the password reset process.
Setting up an Invite Topic ID
Both the Signup tab and each Group Invite tab have an optional Topic ID field. When set, a new member lands on that Discourse topic right after accepting their invite — it's typically used as a "start here" post tailored to that particular invite group.
Why the topic has to be created a specific way, not just any topic ID: Discourse's invite API checks whether the linked topic is visible as if the recipient hadn't joined yet — regardless of how privileged the account sending the invite is. If the topic lives in a category that isn't readable by "Everyone" (for example, one restricted to logged-in members only), every invite send using that Topic ID fails, even though the exact same topic links without any problem when an admin creates the invite manually from inside Discourse's own web UI (confirmed 2026-08-20 — that flow doesn't go through the same check the API does).
Process for creating a topic to use as an invite Topic ID (confirmed working 2026-08-20):
- On Discourse, create a new topic in the
#publiccategory. - Write the post as you'd want a new invite recipient to see it — this is the first thing they'll land on after joining.
- Once the post is finished, open the topic's settings and change it to
to
unlisted. - Note the topic ID from the URL — that's the value to enter in the Topic ID field on the Signup or Group Invite settings tab in the topic_id field.
Creating the topic in #public first, then moving it to unlisted,
keeps the category's read permission set to "Everyone" — satisfying the
invite API's visibility check above — while unlisted hides the topic
from public search, the topic list, and ordinary browsing (unless the user is a member of the staff group). In practice this
means the topic is only reachable by someone who has the direct link (as
the invite itself provides), even though it isn't gated by a logged-in-only
permission the way a normal restricted category would be.
Setting up the invite form's honeypot field
The /invite REST endpoint is intentionally public (anonymous visitors
submit it), which makes it a target for form-scraping spam bots. As one
layer of defense, the plugin checks for a hidden honeypot field named
website on the submitted form — see TiaaHooks::INVITE_HONEYPOT_FIELD's
docblock for the full reasoning. This field has to be added on the
Elementor side; it can't be added by a code change alone.
Do not use Elementor Pro's built-in "Honeypot" field type. Checked
2026-08-27: that field type renders with a hardcoded
style="display:none !important" attribute, with no way to style it
differently. display:none is the textbook honeypot tell — it's exactly
what a honeypot-aware scraper checks for and skips filling, which defeats
the purpose entirely. Use a regular text field instead, hidden by
positioning it off-screen (see step 2 below) rather than hiding it via
display:none — that doesn't carry the same widely-recognized fingerprint.
To set it up, on each Elementor invite form (Signup and every Group Invite form):
- Add a new text field to the form with the exact field ID/name
website(or changeTiaaHooks::INVITE_HONEYPOT_FIELDto match a different name — just keep the two in sync). - In the field's Advanced settings, add a custom CSS class (e.g.
tiaa-hp), then add this rule to the site's custom CSS:.tiaa-hp { position: absolute; left: -9999px; top: -9999px; }Use
position: absoluteoff-screen, notdisplay: none— some bots skip fields hidden that way but still fill anything present in the DOM regardless of CSS visibility. - Leave the field optional/unvalidated in Elementor — a real visitor should never interact with it at all, and its presence with any value is what marks a submission as a bot.
A real visitor never sees or fills this field, so it stays empty on every
genuine submission. A submission that arrives with it populated is
treated as a bot and gets a normal-looking success response with no
Discourse API call made — see the honeypot section of
TiaaHooks::invite_to_discourse() for why the response is disguised
rather than an explicit rejection.
Limitation: this only screens naive, generic scraping bots that
blindly fill every field they find. It does not stop a targeted attacker
who inspects the real request and simply omits the field — the per-email
invite rate limit (TiaaHooks::INVITE_EMAIL_RATE_LIMIT_MAX) is the
defense that still holds against that case.
2. Welcome Message Automation
- Sends personalized Discourse messages to new users.
- Helps onboard members by explaining key features of the forum.
- Ensures engagement and retention by encouraging participation.
3. Site Settings
- Centralized settings page for site-wide configuration (
Admin > TIAA Forum > Site Settings).
| Setting | Purpose |
|---|---|
| Cookie Domain | Shared domain used for cross-subdomain cookies between WordPress and Discourse (e.g. .tiaa-forum.org for production). Can be locked via the TIAA_COOKIE_DOMAIN constant in wp-config.php to prevent accidental changes. |
| Contact Email | Site contact email address. Displayed anywhere via [tiaa_contact_email], rendered as a mailto: link. |
| Funding Level | Reserve status (blue / green / yellow / red) — colors for the "Contribute" button on the front end header. |
| Forum Stats | Member, topic, post, and category counts, plus an as-of date. Updated manually and displayed anywhere (e.g. Home Page) via [tiaa_stat field="..."] shortcodes. Current numbers can be obtained via a Discourse Data Explorer plugin query. |
- Also drives front-end automations that update automatically when a Site Setting changes — no code or template edits needed:
- Funding Level → colors the
.tiaa-contributebutton (Elementor CSS class) via an inline `
- Funding Level → colors the
Releases
1 release. Each count is every asset in that release; expand a row for the breakdown.