Extra Chill Newsletter is a comprehensive network-wide newsletter system providing centralized Sendy integration for the Extra Chill Platform. Operating as a network-activated plugin, it delivers subscription forms, list management, and campaign tracking across all 11 active sites while maintaining specialized functionality on newsletter.extrachill.com (Blog ID 9).
Key Purpose: Bridge WordPress multisite network to Sendy email marketing platform, providing unified subscription workflows while allowing per-site customization through filters and hooks.
Version: 0.2.6
Status: Production active (v0.2.6 includes email template overhaul, REST API migration, and theme integration fix)
Network-Activated: Yes
Text Domain: extrachill-newsletter
- Sendy Integration: Centralized API key management and subscription routing to Sendy lists
- Subscription Forms: Display contextual subscription forms across all sites (homepage, navigation, content, archive)
- Newsletter Management: Custom post type for newsletter content on newsletter.extrachill.com only
- Admin Interface: Settings page for Sendy configuration and list ID mapping
- Cross-Site Coordination: Network-wide subscription handling with site-specific form contexts
Network-Wide with Site-Specific Features:
- Core subscription functionality available on all sites (forms, API, hooks)
- Newsletter post type and admin UI only loaded on newsletter.extrachill.com
- Settings stored in network options accessible by all sites
- Form rendering delegated to theme via hooks, plugins provide display logic
Loading Sequence:
- Core files load immediately (assets, sendy-api, email-template)
- Network-wide hook integrations load (forms, homepage, post-meta, sidebar, breadcrumbs)
- Newsletter-site-only features load on plugins_loaded priority 20 (after extrachill-multisite at priority 10)
extrachill-newsletter/
├── extrachill-newsletter.php # Main plugin file (233 lines)
├── inc/
│ └── core/
│ ├── assets.php # Asset enqueuing (CSS/JS)
│ ├── sendy-api.php # Sendy API integration and subscription function
│ ├── newsletter-post-type.php # Custom newsletter post type (newsletter site only)
│ ├── newsletter-settings.php # Admin settings page (newsletter site only)
│ ├── templates/
│ │ ├── generic-form.php # Universal subscription form template
│ │ ├── email-template.php # Email HTML template generation
│ │ └── recent-newsletters.php # Newsletter list display
│ └── hooks/
│ ├── forms.php # Form display hooks (archive, single post)
│ ├── homepage.php # Homepage integration hooks
│ ├── post-meta.php # Post meta display hooks
│ ├── sidebar.php # Sidebar widget hooks
│ └── breadcrumbs.php # Breadcrumb navigation hooks
├── assets/
│ ├── css/ # Stylesheet files
│ └── js/ # JavaScript files (form submission, etc.)
├── docs/
│ └── CHANGELOG.md # Version history
└── .buildignore # Build exclusion patterns
Key Functions:
is_newsletter_site()- Check if current site is newsletter.extrachill.com (Blog ID 9)extrachill_get_newsletter_context_presets()- Returns preset configurations for form contexts (homepage, navigation, content, archive)extrachill_render_newsletter_form( $context )- Main action handler rendering newsletter forms with context-specific stylingget_newsletter_integrations()- Retrieve all registered subscription integrations (filter-based)newsletter_register_default_integrations()- Register default integration contexts
Integration Contexts Registered:
- navigation - Newsletter form in site navigation menu
- homepage - Hero section subscription form on homepage
- archive - Newsletter archive page subscription
- content - Post-content newsletter signup (below post body)
- contact - Contact form newsletter subscription checkbox (integrated via
extrachill-contact)
Hooks Defined:
extrachill_render_newsletter_form- Action to render form for contextnewsletter_form_integrations- Filter to register/modify integrationsextrachill_newsletter_form_args- Filter to customize form arguments per contextextrachill_navigation_before_social_links- Action for navigation form display
Network Options (stored via update_site_option()):
$settings = get_site_option( 'extrachill_newsletter_settings', array() );Stored Configuration Fields:
sendy_api_key- Sendy API key for authenticationsendy_url- Sendy installation URL (default:https://mail.extrachill.com/sendy)from_name- Campaign sender name (default: "Extra Chill")from_email- Campaign from address (default:newsletter@extrachill.com)reply_to- Reply-to address (default:chubes@extrachill.com)brand_id- Sendy brand ID (default: "1"){context}_list_id- Sendy list ID per integration context (homepage_list_id, navigation_list_id, etc.)
Function: get_sendy_config() - Returns configuration array
Configuration Structure:
array(
'api_key' => string (Sendy API key),
'sendy_url' => string (Sendy base URL),
'from_name' => string (Campaign sender),
'from_email' => string (From address),
'reply_to' => string (Reply-to address),
'brand_id' => string (Sendy brand ID)
)Function: extrachill_multisite_subscribe( $email, $context )
Parameters:
$email- Email address to subscribe (validated viais_email())$context- Integration context (homepage, navigation, content, archive, contact)
Validation Steps:
- Verify integration context exists in registered integrations
- Check list ID is configured for context
- Validate email format
- Verify Sendy API key is available
Subscription Flow:
- Retrieve integration config and list ID from network options
- Build subscription request with email, list ID, and API key
- POST to Sendy subscribe endpoint:
{sendy_url}/subscribe - Return status array with success flag and message
Return Value:
array(
'success' => bool, // true if subscribed, false if error
'message' => string // User-facing message
)Error Handling:
- Logs integration errors to WordPress error log
- Returns user-friendly error messages (not technical details)
- Gracefully handles Sendy unavailability or network errors
- Validates email format before API call
Sendy API Details:
- Endpoint: POST
{sendy_url}/subscribe - Content-Type: application/x-www-form-urlencoded
- Required Fields: email, list, api_key
- Optional Fields: boolean (set to "true" for double opt-in)
- Timeout: 30 seconds
List ID Configuration: Set via Settings page on newsletter.extrachill.com
- Each integration context requires its own Sendy list ID
- List IDs configured once, reused across all sites
- Empty list IDs disable subscription for that context
- Configuration accessible to all plugins via network options
Cross-Site Subscriber Handling:
- Single Sendy list per context receives subscribers from all network sites
- Subscriber email address is unique identifier (no user ID sent to Sendy)
- No subscriber tracking by site origin (all aggregated in Sendy)
- Optional: Use Sendy custom fields for site source tracking
Context Presets (defined in extrachill_get_newsletter_context_presets()):
-
homepage
- Wrapper class:
home-newsletter-signup newsletter-grid-section - Heading: "Subscribe" (h3)
- Description: "Stories and insights from the underground."
- Layout: section (full-width)
- Shows archive link: No
- Use case: Homepage hero/featured section
- Wrapper class:
-
navigation
- Wrapper class:
menu-newsletter - Heading: None
- Layout: inline (minimal width)
- Shows archive link: Yes
- Use case: Site navigation/footer area
- Wrapper class:
-
content
- Wrapper class:
newsletter-content-section - Heading: "Stay Connected with Extra Chill" (h3)
- Description: "Get stories, reflections, and music industry insights delivered to your inbox."
- Layout: section (full-width)
- Shows archive link: Yes
- Use case: After post content
- Wrapper class:
-
archive
- Wrapper class:
newsletter-subscription-form - Heading: "Subscribe to Our Newsletter" (h2)
- Description: "Get independent music journalism with personality delivered to your inbox."
- Layout: section (full-width)
- Shows archive link: No
- Use case: Newsletter archive page (newsletter site only)
- Wrapper class:
Function: extrachill_render_newsletter_form( $context )
Rendering Process:
- Retrieve preset configuration for context
- Apply
extrachill_newsletter_form_argsfilter for customization - Enqueue form CSS/JavaScript via
wp_enqueue_style()/wp_enqueue_script() - Include generic form template with preset arguments
Template: inc/core/templates/forms/generic-form.php
Form Fields:
- Email input (required, validated on submit)
- Subscribe button (handled by
assets/js/newsletter.jsvia REST:POST /wp-json/extrachill/v1/newsletter/subscribe) - Optional: Archive link (if
show_archive_link= true)
REST Submission:
- Endpoint:
POST /wp-json/extrachill/v1/newsletter/subscribe - Header:
X-WP-Nonce: newsletterParams.restNonce - JSON body:
{ "email": "user@example.com", "context": "homepage" } - Delegates to
extrachill_multisite_subscribe() - Returns JSON
{ "message": "..." }on success orWP_Erroron failure
Navigation Form (All Sites):
- Displayed via hook:
extrachill_navigation_before_social_links - Rendered directly by newsletter plugin
- Uses 'navigation' context preset
Other Contexts (Per-Site):
- Forms triggered by theme/plugin hooks
- Theme determines when to display form
- Newsletter plugin provides rendering function via action hook
Hook Pattern:
// Theme calls this hook when ready to display form
do_action( 'extrachill_render_newsletter_form', 'context_name' );
// Newsletter plugin handles the action
add_action( 'extrachill_render_newsletter_form', 'extrachill_render_newsletter_form' );newsletter_form_integrations - Register custom integration contexts
- Type: Filter
- Default Integrations: navigation, homepage, archive, content, contact
- Parameters:
$integrations(array keyed by context slug) - Return: Array of integration definitions
- Structure Per Integration:
array( 'label' => string, // Human-readable name 'description' => string, // Help text 'list_id_key' => string // Settings option key for Sendy list ID )
- Example:
add_filter( 'newsletter_form_integrations', function( $integrations ) { $integrations['my_context'] = array( 'label' => 'My Custom Form', 'description' => 'Form for my custom context', 'list_id_key' => 'my_context_list_id' ); return $integrations; });
extrachill_newsletter_form_args - Customize form rendering per context
- Type: Filter
- Parameters:
$args(preset array),$context(context slug) - Return: Modified arguments array
- Customizable Properties: wrapper_class, heading, heading_level, description, layout, placeholder, button_text, show_archive_link
- Example:
add_filter( 'extrachill_newsletter_form_args', function( $args, $context ) { if ( $context === 'homepage' ) { $args['heading'] = 'Join Our Mailing List'; $args['button_text'] = 'Sign Up'; } return $args; }, 10, 2);
extrachill_render_newsletter_form - Render form for specific context
- Type: Action
- Parameters:
$context(string) - Handler:
extrachill_render_newsletter_form()function - Usage: Called by theme/plugins when form display needed
- Example:
do_action( 'extrachill_render_newsletter_form', 'homepage' );
extrachill_navigation_before_social_links - Hook point for navigation additions
- Type: Action (called by theme)
- Usage: Newsletter plugin hooks here to add navigation form
- Parameters: None
- Example:
add_action( 'extrachill_navigation_before_social_links', function() { do_action( 'extrachill_render_newsletter_form', 'navigation' ); });
extrachill_archive_below_description - After archive page description (newsletter hook)
- Type: Action
- Usage: Archive page form display
- Handler:
extrachill_newsletter_archive_form()
extrachill_after_post_content - After single post content (newsletter hook)
- Type: Action
- Usage: Content form display
- Handler:
extrachill_newsletter_after_post_content()
newsletter_homepage_hero - Homepage hero section display (newsletter site only)
- Type: Action
- Usage: Newsletter site homepage form
- Handler:
extrachill_newsletter_homepage_hero_form()
Email Template Overhaul:
- Modernized HTML email templates
- Improved mobile responsiveness
- Updated branding and styling
REST API Migration:
- Subscription forms now use REST API exclusively
- Endpoint:
POST /wp-json/extrachill/v1/newsletter/subscribe - Campaign push via REST:
POST /wp-json/extrachill/v1/newsletter/campaign/push
Theme Integration Fix:
-
Added theme integration to register newsletter post type with single-post style system
-
Single newsletter posts now display theme single-post styles correctly
-
Function: newsletter_single_post_style_types() in extrachill-newsletter.php
-
Filter: extrachill_single_post_style_post_types
The newsletter plugin's frontend UI calls REST endpoints that are registered in the network-activated extrachill-api plugin.
Endpoint: POST /wp-json/extrachill/v1/newsletter/subscribe
Permission: Public (permission_callback is __return_true)
Body (JSON):
{
"email": "user@example.com",
"context": "homepage"
}Response (Success):
{
"message": "..."
}Endpoint: POST /wp-json/extrachill/v1/newsletter/campaign/push
Permission: current_user_can( 'edit_posts' )
Body (JSON):
{
"post_id": 123
}Response (Success):
{
"message": "Successfully pushed to Sendy!",
"campaign_id": "..."
}Status: Newsletter site only (newsletter.extrachill.com, Blog ID 9)
Post Type Slug: newsletter
Features:
- Custom admin interface
- Title, content, featured image
- Publish date tracking
- Author attribution
- Status workflow (draft, pending, published)
URL Structure: /newsletter/{post-name}/
Post Meta:
- Sendy campaign ID (if campaign created)
- Email template HTML (auto-generated from post content)
- Campaign status (draft, scheduled, sent)
Newsletter Settings Page:
- Located: Newsletter → Settings (admin menu on newsletter site)
- Access: Network administrators only
- Configuration: Sendy API details, list IDs per integration, email sender info
Not Used For:
- Campaign creation (campaigns created directly in Sendy UI)
- Subscriber management (handled by Sendy)
- Email sending (Sendy handles all email delivery)
Option Key: extrachill_newsletter_settings
Stored As: Serialized PHP array
Fields:
array(
'sendy_api_key' => 'api_key_here',
'sendy_url' => 'https://mail.extrachill.com/sendy',
'from_name' => 'Extra Chill',
'from_email' => 'newsletter@extrachill.com',
'reply_to' => 'chubes@extrachill.com',
'brand_id' => '1',
'homepage_list_id' => 'list_id_abc123',
'navigation_list_id' => 'list_id_def456',
'archive_list_id' => 'list_id_ghi789',
'content_list_id' => 'list_id_jkl012',
'contact_list_id' => 'list_id_mno345'
)Access Pattern:
// Get all settings
$settings = get_site_option( 'extrachill_newsletter_settings', array() );
// Update settings
update_site_option( 'extrachill_newsletter_settings', $settings );The newsletter plugin does not maintain subscriber data in WordPress. All subscriber information stored in Sendy:
- Email addresses
- Subscription status
- Campaign history
- List assignments
Subscription Forms Available On:
- All 11 network sites
- Forms access network options for configuration
- Email submissions routed to Sendy via central function
Settings Management:
- Centralized on newsletter.extrachill.com
- Accessible only to network administrators
- Applied to all sites automatically
Blog Context Handling:
- Navigation form displays on all sites (priority: direct call)
- Other contexts handled by per-site themes/plugins
- No site-specific subscriber lists (all to Sendy)
Subscription Flow:
- Any site user submits email via form
- Form context identified (homepage, content, etc.)
extrachill_multisite_subscribe()called with email and context- Function queries network options for Sendy list ID
- Subscription routed to appropriate Sendy list
No Blog Switching Required:
- Network options accessible from any site context
- No per-site data stored (simplifies multi-site)
Plugin: extrachill-contact
Pattern:
- Contact form displays optional newsletter checkbox
- If checked, email submitted to newsletter system
- Uses 'contact' integration context
- Sendy list ID configured separately from other forms
Hook Points:
- Filter:
newsletter_form_integrations(contact form registers itself) - Action: Called via contact form submission handler
Plugin: extrachill-blog
Pattern:
- Blog homepage displays newsletter signup form
- Form uses 'homepage' context preset
- Below-post forms use 'content' context preset
- Theme calls
do_action( 'extrachill_render_newsletter_form', 'context' )
Plugin: extrachill-artist-platform
Pattern:
- Artist signup may include newsletter subscription
- Uses 'navigation' or custom context
- Artist profile pages may display newsletter form
Plugin: extrachill-community
Pattern:
- Member registration may link to newsletter signup
- Community forum sidebar displays newsletter form
Purpose: Registers newsletter post type with theme's single post style system to ensure single-post.css is loaded for newsletter posts.
File: extrachill-newsletter.php
Filter: extrachill_single_post_style_post_types
Function: newsletter_single_post_style_types() - Adds 'newsletter' to the array of post types that get single-post styles.
Why: Single newsletter posts were not displaying theme single-post styles because the plugin didn't register with the theme's filter, unlike other plugins (e.g., news-wire).
Files: Located in /assets/ directory
Loading Pattern:
- Enqueued only on pages with subscription forms
- Enqueued from
inc/core/assets.php - Uses
filemtime()for cache busting - Frontend CSS: form styling and layout
- Frontend JS: form submission via AJAX, Turnstile integration
Conditional Loading: Forms trigger asset enqueue via action hook
Steps to add custom form context:
- Register integration via filter:
add_filter( 'newsletter_form_integrations', function( $integrations ) {
$integrations['custom_context'] = array(
'label' => 'Custom Form',
'description' => 'Description of form',
'list_id_key' => 'custom_context_list_id'
);
return $integrations;
});-
Configure Sendy list ID in Newsletter Settings page
-
Display form where needed:
do_action( 'extrachill_render_newsletter_form', 'custom_context' );- Customize form via filter (if needed):
add_filter( 'extrachill_newsletter_form_args', function( $args, $context ) {
if ( $context === 'custom_context' ) {
$args['heading'] = 'Custom Heading';
}
return $args;
}, 10, 2);Development Setup:
- Configure Sendy API key in network settings (test environment)
- Create test Sendy lists for each integration
- Configure list IDs in newsletter settings
- Test form submission via AJAX
- Verify subscriber appears in Sendy list
Testing Without Sendy:
- Mock
extrachill_multisite_subscribe()for testing - Verify form renders correctly
- Check AJAX submission and response handling
Build System: Use homeboy build extrachill-newsletter for production builds
Build Steps:
- Clean previous builds
- Install production dependencies:
composer install --no-dev - Copy essential files to a temporary
/build/extrachill-newsletter/directory - Exclude: vendor/, docs/, tests/, .git/, composer.lock, CLAUDE.md
- Validate plugin file exists and loads
- Create
/build/extrachill-newsletter.zip(final output) - Remove the temporary
/build/extrachill-newsletter/directory - Restore development dependencies:
composer install
Production Deployment:
Production deployments and remote operations run through Homeboy (homeboy/ in this repo). After building build/extrachill-newsletter.zip, Homeboy handles upload/activation steps per environment.
Credential Management:
- Sendy API key stored in network options (database)
- Alternative: Store in wp-config.php if required
- Never commit credentials to repository
- Use environment variables in deployment if needed
Email Validation:
- All email inputs validated via WordPress
is_email() - Sanitized via
sanitize_email()before processing - Database entries escaped via prepared statements
Form Submission:
- Nonce verification on all AJAX submissions
- Nonce action: based on form context
- Nonce expires after 12 hours (WordPress default)
Cloudflare Turnstile:
- Network-wide configuration (managed by extrachill-multisite)
- Site key: Client-side widget rendering
- Secret key: Server-side token verification
- Server-side verification before Sendy API call
- Prevents bot subscriptions
Graceful Degradation:
- Forms work without Turnstile if not configured
- Turnstile optional (not required)
- No failures if Turnstile service unavailable
Subscriber Privacy:
- Email only data point sent to Sendy
- No user ID, IP address, or tracking data sent
- Subscriber data stored only in Sendy (not WordPress)
- Complies with privacy requirements (no WordPress DB)
API Key Security:
- Stored in network options (database protected)
- Never logged or exposed in error messages
- Sent only to Sendy API via HTTPS
- Sanitized before storage
Do NOT:
- Store subscriber emails in WordPress database (use Sendy only)
- Hardcode Sendy credentials in code (use network options)
- Use unencrypted HTTP for Sendy API calls (use HTTPS)
- Fallback to empty lists if list ID not configured (fail gracefully with error message)
- Create multiple subscribe functions for different contexts (use filters)
- Send unnecessary user data to Sendy (email address only)
Process:
- Verify feature works with Sendy API
- Check for cross-site implications
- Propose hook names and filter parameters
- Consider how feature affects form display
- Test with multiple integration contexts
- Document integration patterns for other plugins
- Uses network options for network-wide data
- Sanitizes all user input
- Verifies nonces on AJAX submissions
- Includes error logging for debugging
- Uses wp_remote_post() for API calls
- Gracefully handles Sendy API errors
- Updates apply via hooks, not direct plugin calls
- No hardcoded Sendy credentials
- WordPress: 5.0+ (multisite network)
- PHP: 7.4+
- Sendy: Email marketing instance with API configured
- Cloudflare Turnstile: Bot prevention (via extrachill-multisite)
- extrachill-multisite: Blog ID helpers (fallback if unavailable)
- WooCommerce: E-commerce platform (separate site)
- bbPress: Community forums (separate site)
- Additional plugins: Works standalone
Component Documentation:
- api-reference.md - REST API endpoint details
- integrations.md - Integration system and context guide
- extrachill-multisite CLAUDE.md - Blog ID management, Turnstile integration
- extrachill-contact CLAUDE.md - Contact form newsletter integration
- Root CLAUDE.md - Platform-wide architectural patterns
External Resources: