$npx -y skills add mralaminahamed/wp-dev-skills --skill wp-email-templatesUse when adding or refactoring transactional emails in a WordPress plugin — extracting inline HTML strings into reusable templates sharing a branded base shell, implementing the render_template helper (ob_start / include / ob_get_clean), passing variables safely with extract(EXTR
| 1 | # Reusable HTML Email Templates (WordPress plugin) |
| 2 | |
| 3 | > **Model note:** Mechanical refactoring — extract strings, create template files, wire `wp_mail()`. `haiku` handles end-to-end. |
| 4 | |
| 5 | Move email bodies out of inline PHP strings into `templates/emails/`, where every message reuses one branded shell. Adding a new email = adding one content file. |
| 6 | |
| 7 | ## When to use |
| 8 | |
| 9 | - "Move email content to templates", "branded/HTML emails", "reuse an email template". |
| 10 | - Any plugin building messages inline with `wp_mail()` heredocs. |
| 11 | |
| 12 | **Not for:** REST API webhooks, push notifications, or Slack integrations. Transactional email in themes — this skill targets plugin-delivered mail only. |
| 13 | |
| 14 | ## Structure |
| 15 | |
| 16 | ``` |
| 17 | templates/emails/ |
| 18 | base.php shared shell: header, body slot ($content), footer |
| 19 | <name>.php per-email content (greeting, copy, CTA button) |
| 20 | ``` |
| 21 | |
| 22 | - **base.php** — table-based, inline-CSS responsive shell (email clients ignore `<style>`/external CSS). Receives `$subject`, `$preheader`, `$content`, `$site_name`, `$site_url`. Echoes `$content` raw (`// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped` — content templates escape their own values). |
| 23 | - **content templates** — escape every variable (`esc_html`, `esc_url`); wrap copy in `__()`/`esc_html__()` with the text domain; use `_n()` for plurals. Guard each `$var = $var ?? default;` so the file is robust if rendered standalone. |
| 24 | |
| 25 | ## Render helpers (in your Helpers class) |
| 26 | |
| 27 | ```php |
| 28 | public static function render_template( string $path, array $vars = [] ): string { |
| 29 | if ( ! is_readable( $path ) ) return ''; |
| 30 | // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- controlled template vars |
| 31 | extract( $vars, EXTR_SKIP ); |
| 32 | ob_start(); |
| 33 | include $path; |
| 34 | return (string) ob_get_clean(); |
| 35 | } |
| 36 | |
| 37 | public static function render_email( string $template, array $vars = [] ): string { |
| 38 | $dir = (string) plugin_config( 'templates' ) . 'emails/'; // your assets/templates path helper |
| 39 | $vars['content'] = self::render_template( $dir . $template . '.php', $vars ); |
| 40 | $vars['site_name'] = $vars['site_name'] ?? get_bloginfo( 'name' ); |
| 41 | $vars['site_url'] = $vars['site_url'] ?? home_url( '/' ); |
| 42 | return self::render_template( $dir . 'base.php', $vars ); // always wrap in the shell |
| 43 | } |
| 44 | ``` |
| 45 | |
| 46 | A missing content template yields an empty body but the shell still renders — callers always get a valid document. |
| 47 | |
| 48 | ## Sending |
| 49 | |
| 50 | ```php |
| 51 | $body = Helpers::render_email( 'welcome', [ 'subject' => $subject, 'user_name' => $u->display_name, ... ] ); |
| 52 | $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; // REQUIRED for HTML |
| 53 | return wp_mail( $to, $subject, $body, $headers ); |
| 54 | ``` |
| 55 | |
| 56 | Drive any TTL/expiry copy from the same constant the code uses (e.g. a token transient TTL) so email text can't drift from behavior. |
| 57 | |
| 58 | ## Gotchas |
| 59 | |
| 60 | - Entities in translated strings: `©` double-escapes through `esc_html__` → use the literal `©`. |
| 61 | - Don't assign WordPress globals in templates (`$year` etc. trip `WordPress.WP.GlobalVariablesOverride`) — inline `gmdate('Y')` instead. |
| 62 | |
| 63 | ## Testing |
| 64 | |
| 65 | WP's test suite captures `wp_mail` via MockPHPMailer: |
| 66 | |
| 67 | ```php |
| 68 | reset_phpmailer_instance(); |
| 69 | // ... trigger the send ... |
| 70 | $sent = tests_retrieve_phpmailer_instance()->get_sent(); |
| 71 | $this->assertStringContainsString( 'text/html', $sent->header ); |
| 72 | $this->assertStringContainsString( '<!DOCTYPE html', $sent->body ); // base shell used |
| 73 | $this->assertStringContainsString( $expected_cta, $sent->body ); |
| 74 | ``` |
| 75 | |
| 76 | Force a send failure with `add_filter( 'pre_wp_mail', '__return_false' )` to test the error branch. |
| 77 | |
| 78 | ## References |
| 79 | |
| 80 | - `references/base.php` — the full responsive HTML base shell (header/body/footer, inline CSS), copy into `templates/emails/base.php`. |
| 81 | - `references/content-exam |