Network-activated WordPress plugin providing the network administration foundation for the Extra Chill Platform multisite network.
- Name: Extra Chill Network
- Version: 1.4.5
- Text Domain:
extrachill-network - Author: Chris Huber
- Author URI: https://chubes.net
- License: GPL v2 or later
- Network: true (network-activated)
- Requires at least: 5.0
- Tested up to: 6.4
- Requires PHP: 7.4
Extra Chill Network is the network foundation plugin for the Extra Chill Platform, providing centralized infrastructure for all 10 active WordPress multisite sites. It manages network-wide configuration, authentication, security, site discovery, and cross-site linking patterns while remaining lightweight and performant across the entire network.
Core Purpose: Single source of truth for blog ID management, Cloudflare Turnstile integration, network admin menu structure, and cross-site coordination patterns (including ec_get_artist_profile_by_slug).
Production Status: Active network foundation plugin
Architecture: Procedural WordPress pattern with network-wide functionality
Scope: Network administration infrastructure for all 10 active sites
Build System: Use homeboy build extrachill-network for production builds
Single Plugin, Network Scope: Activated once at network level, serves all sites via Network: true header
Network Options Storage: Uses get_site_option() for network-wide configuration (not per-site)
Cross-Site Access: Uses switch_to_blog() / restore_current_blog() for operations requiring blog-specific context
Key Principle: Plugin operates at network level, loaded once, providing shared infrastructure to all sites
extrachill-network/
├── extrachill-network.php # Main plugin file
├── inc/
│ ├── core/
│ │ ├── blog-ids.php # Blog ID constants and helper functions
│ │ ├── extrachill-turnstile.php # Turnstile integration and validation
│ │ ├── legacy-path-redirects.php # Legacy URL redirects
│ │ ├── oauth-helpers.php # OAuth helper functions for Google OAuth
│ │ └── object-cache-config.php # Object cache configuration
│ ├── cross-site-links/ # Cross-site linking system
│ │ ├── canonical-authority.php # Canonical URL resolution for taxonomies
│ │ ├── cross-site-links.php # Loader + mapping/labels + hook registration
│ │ ├── entity-links.php # User profile + artist profile resolution
│ │ ├── renderers.php # Button renderers for theme hooks
│ │ └── taxonomy-links.php # Taxonomy archive linking
│ ├── theme/ # Theme integration hooks
│ │ ├── 404-content.php # Custom 404 page content
│ │ ├── admin-menu.php # Admin menu customizations
│ │ ├── dns-prefetch.php # DNS prefetch hints
│ │ ├── filter-bar.php # Filter bar artist dropdown for music categories
│ │ ├── footer-links.php # EC-specific footer bottom menu links
│ │ ├── footer-main-menu.php # Footer main menu items
│ │ ├── network-dropdown.php # Network site dropdown
│ │ └── site-title.php # Site title customizations
│ └── assets.php # Asset enqueuing (404 styles, taxonomy badges)
├── assets/
│ └── css/
│ ├── 404.css # 404 page styles
│ └── taxonomy-badges.css # Music-specific taxonomy badge colors
├── admin/
│ ├── network-menu.php # Network admin menu structure
│ ├── network-security-settings.php # Security settings page (Turnstile)
│ ├── network-oauth-settings.php # OAuth provider settings page (Google OAuth)
│ ├── network-payments-settings.php # Payment provider settings page (Stripe)
│ └── network-shipping-settings.php # Shipping provider settings page
├── docs/
│ └── CHANGELOG.md # Version history
└── .buildignore # Build exclusion patterns
Plugins_loaded Hook: Plugin initializes at priority 10 via plugins_loaded action
Network-Only Validation: Activation hook checks is_multisite(), deactivates if not network
Admin-Only Features: Network menu and security settings only load in network admin via is_network_admin()
Conditional Loading:
add_action( 'plugins_loaded', 'extrachill_network_init' );
function extrachill_network_init() {
// Always load blog IDs and Turnstile
require_once 'inc/core/blog-ids.php';
require_once 'inc/core/extrachill-turnstile.php';
// Only in network admin
if ( is_admin() && is_network_admin() ) {
require_once 'admin/network-menu.php';
require_once 'admin/network-security-settings.php';
}
}Purpose: Centralized, performance-optimized blog ID system for all multisite sites
Constants Defined (in inc/core/blog-ids.php):
EC_BLOG_ID_MAIN= 1 (extrachill.com)EC_BLOG_ID_COMMUNITY= 2 (community.extrachill.com)EC_BLOG_ID_SHOP= 3 (shop.extrachill.com)EC_BLOG_ID_ARTIST= 4 (artist.extrachill.com + extrachill.link)EC_BLOG_ID_EVENTS= 7 (events.extrachill.com)EC_BLOG_ID_NEWSLETTER= 9 (newsletter.extrachill.com)EC_BLOG_ID_DOCS= 10 (docs.extrachill.com)EC_BLOG_ID_WIRE= 11 (wire.extrachill.com)EC_BLOG_ID_STUDIO= 12 (studio.extrachill.com)
Note: Blog IDs 5–6 are unused (historical artifacts; chat.extrachill.com was archived). Blog ID 8 (stream.extrachill.com, EC_BLOG_ID_STREAM) was decommissioned in April 2026 — the constant, slug, and domain entry have all been removed from the code.
ec_get_blog_ids() - Returns associative array of all blog IDs
Returns:
array(
'main' => 1,
'community' => 2,
'shop' => 3,
'artist' => 4,
'events' => 7,
'newsletter' => 9,
'docs' => 10,
'wire' => 11,
'studio' => 12
)ec_get_blog_id( $key ) - Get blog ID by logical slug
Parameters: $key (string) - Logical site key (e.g., 'artist', 'newsletter')
Returns: int|null - Blog ID or null if unknown
Usage:
$blog_id = ec_get_blog_id( 'newsletter' ); // Returns 9
$blog_id = ec_get_blog_id( 'unknown' ); // Returns nullec_get_domain_map() - Returns mapping of domains to blog IDs
Returns:
array(
'extrachill.com' => 1,
'community.extrachill.com' => 2,
'shop.extrachill.com' => 3,
'artist.extrachill.com' => 4,
'events.extrachill.com' => 7,
'newsletter.extrachill.com' => 9,
'docs.extrachill.com' => 10,
'wire.extrachill.com' => 11,
'studio.extrachill.com' => 12,
'extrachill.link' => 4, // Domain mapping for artist link pages
'www.extrachill.link' => 4
)ec_get_blog_slug_by_id( $blog_id ) - Reverse lookup: slug by blog ID
Parameters: $blog_id (int) - Numeric blog ID
Returns: string|null - Slug (e.g., 'artist') or null if unknown
Usage:
$slug = ec_get_blog_slug_by_id( 4 ); // Returns 'artist'ec_get_site_url( $key ) - Get production site URL by logical slug
Parameters: $key (string) - Logical site key (e.g., 'artist', 'newsletter')
Returns: string|null - Full site URL (e.g., https://artist.extrachill.com) or null if unknown
Overridable: Fires ec_site_url_override filter for dev environment URLs
Usage:
$url = ec_get_site_url( 'newsletter' ); // Returns 'https://newsletter.extrachill.com'Implementation: .github/sunrise.php (executes before WordPress loads)
Mapping: extrachill.link (and www.extrachill.link) → Blog ID 4 (artist.extrachill.com)
URL Preservation:
- Frontend URLs display as
extrachill.link/artist-slug/ - Backend operates on
artist.extrachill.com - WordPress multisite native cookies set for both domains
Blog ID Helper Support: Both domains resolve to Blog ID 4 via ec_get_blog_id( 'artist' )
Purpose: Network-wide bot prevention via Cloudflare Turnstile CAPTCHA service
Location: inc/core/extrachill-turnstile.php
Network Options Storage:
ec_turnstile_site_key- Client-side widget identifierec_turnstile_secret_key- Server-side verification token
ec_get_turnstile_site_key() - Retrieve client-side site key
Returns: string - Site key or empty string if not configured
ec_get_turnstile_secret_key() - Retrieve server-side secret key
Returns: string - Secret key or empty string if not configured
ec_update_turnstile_site_key( $site_key ) - Store client-side site key
Parameters: $site_key (string) - Cloudflare-provided site key
Security: Input sanitized via sanitize_text_field()
ec_update_turnstile_secret_key( $secret_key ) - Store server-side secret key
Parameters: $secret_key (string) - Cloudflare-provided secret key
Security: Input sanitized via sanitize_text_field()
ec_is_turnstile_configured() - Check if Turnstile is properly configured
Returns: bool - true if both site key and secret key exist
Usage:
if ( ec_is_turnstile_configured() ) {
// Render widget and validate responses
}ec_verify_turnstile_response( $response ) - Verify Turnstile token via Cloudflare API
Parameters: $response (string) - Token from frontend widget
Returns: bool - true if verified, false if invalid or error
Verification Process:
- Sanitize response token via
sanitize_text_field() - Retrieve secret key from network options
- POST to Cloudflare verification endpoint with secret and token
- Parse JSON response, verify success flag
- Log detailed errors if verification fails
Error Logging: Comprehensive error logging for debugging:
- Empty response token
- Missing secret key
- HTTP errors from Cloudflare
- JSON decode failures
- Verification failures with error codes
Usage:
if ( ec_verify_turnstile_response( $_POST['cf-turnstile-response'] ) ) {
// Proceed with subscription/registration
} else {
// Reject form submission
}ec_render_turnstile_widget( $args = array() ) - Generate HTML for Turnstile widget
Parameters (optional): $args (array) - Widget options
Customizable Attributes:
data-sitekey- Client-side site key (required)data-size- Widget size: 'normal' (default), 'compact'data-theme- Theme: 'light', 'dark', 'auto' (default)data-appearance- Appearance: 'always' (default), 'interaction-only'class- CSS classes (default: 'cf-turnstile')
Returns: string - HTML div with widget attributes or empty string if not configured
Example:
echo ec_render_turnstile_widget( array(
'data-size' => 'compact',
'data-theme' => 'dark'
) );ec_enqueue_turnstile_script( $handle = 'cloudflare-turnstile' ) - Enqueue Turnstile JavaScript library
Parameters (optional): $handle (string) - Script handle
Script Source: https://challenges.cloudflare.com/turnstile/v0/api.js
Conditional: Only enqueues if Turnstile is configured via ec_is_turnstile_configured()
Usage:
add_action( 'wp_enqueue_scripts', function() {
ec_enqueue_turnstile_script();
});Location: admin/network-menu.php
Access: Network administrators only (checked via current_user_can( 'manage_network' ))
Menu Structure: Organized top-level menu for platform settings
Subpages:
- Security Settings (Turnstile configuration)
- OAuth Settings (Google OAuth configuration)
- Payment Settings (Stripe configuration)
Integration Points: Extends WordPress network admin interface with Extra Chill specific tools
Location: admin/network-security-settings.php
Purpose: Centralized interface for network-wide security configuration
Features:
- Cloudflare Turnstile API key management
- Access control configuration
- Security policy settings
UI Integration: Submenu under network admin menu
Capability Checks: Network administrator capability verification
Location: admin/network-oauth-settings.php
Purpose: Centralized OAuth provider configuration for the multisite network
Features:
- Google OAuth client ID and secret management
- OAuth redirect URI configuration
- Provider enable/disable toggles
Helper Functions (inc/core/oauth-helpers.php):
ec_get_google_client_id()- Retrieve Google OAuth client IDec_get_google_client_secret()- Retrieve Google OAuth client secretec_is_google_oauth_configured()- Check if Google OAuth is properly configured
Integration: Used by extrachill-users plugin for Google sign-in functionality
Location: admin/network-payments-settings.php
Purpose: Centralized payment provider configuration for the multisite network
Features:
- Stripe API key management (publishable and secret keys)
- Stripe Connect configuration
- Payment provider enable/disable toggles
Integration: Used by extrachill-shop plugin for Stripe Connect and payment processing
Purpose: Provide unified cross-site navigation patterns (taxonomy archives, user profiles, artist profiles) across the 10-site network.
Core Modules:
inc/cross-site-links/canonical-authority.php� canonical URL resolution for shared taxonomy archives.inc/cross-site-links/cross-site-links.php� loader + mapping/labels + hook registration.inc/cross-site-links/taxonomy-links.php� taxonomy archive linking (REST-backed counts for main/events/shop/wire).inc/cross-site-links/entity-links.php� user profile linking + artist profile resolution.inc/cross-site-links/renderers.php� button renderers hooked into the theme.
Core Functions:
ec_get_cross_site_term_links( $term, $taxonomy )� Returns links to other sites where the term has published content.ec_get_taxonomy_site_map()� Defines taxonomy � site-key mapping (filterable:ec_taxonomy_site_map).ec_get_site_labels()� Defines site labels used in UI (filterable:ec_site_labels).ec_get_cross_site_user_links( $user_id )� Returns links to community profile, author archive, and artist profiles.ec_get_artist_profile_by_slug( $slug )� Resolves publishedartist_profileCPT by slug on the artist site.ec_render_cross_site_taxonomy_links()(hook:extrachill_archive_below_description) � renders taxonomy link buttons onis_tax().ec_render_cross_site_user_links( $user_id )(hook:extrachill_after_author_bio) � renders user link buttons on author pages.
Canonical Authority Functions (inc/cross-site-links/canonical-authority.php):
ec_get_canonical_authority_url( $term, $taxonomy )� Returns canonical URL for taxonomy archive across sites.ec_get_taxonomy_canonical_config()� Defines which site is canonical for each shared taxonomy.ec_artist_profile_has_image( $slug )� Checks if artist profile has a featured image.
Integration: This system centralizes cross-site navigation so plugins and the theme do not duplicate blog switching and link resolution logic.
Purpose: Provide EC-specific content and styling hooks that keep platform-specific logic in the plugin while allowing the theme to remain generic.
Filter Bar Integration (inc/theme/filter-bar.php):
- Adds artist dropdown filter for music-specific categories (song-meanings, music-history)
- Hook:
extrachill_filter_bar_category_itemsfilter - Conditionally displays only on relevant category archives
- Depends on theme's
extrachill_build_artist_dropdown()function
Footer Links (inc/theme/footer-links.php):
- Provides EC-specific footer bottom menu links (Affiliate Disclosure, Privacy Policy)
- Hook:
extrachill_footer_bottom_menu_itemsfilter - Theme provides empty default; plugin adds EC-specific links
- Uses
ec_get_site_url( 'main' )for consistent URL generation
Taxonomy Badge Colors (assets/css/taxonomy-badges.css):
- Music-specific badge colors for festivals, locations, venues, and artists
- Extends theme's base taxonomy badge styling
- Loaded via
inc/assets.phpafter theme'sextrachill-taxonomy-badgesdependency - Provides branded colors for specific festivals (Bonnaroo, Coachella, etc.) and locations
Other Theme Integrations:
inc/theme/404-content.php- Custom 404 page contentinc/theme/admin-menu.php- Admin menu customizationsinc/theme/dns-prefetch.php- DNS prefetch hints for performanceinc/theme/footer-main-menu.php- Footer main menu itemsinc/theme/network-dropdown.php- Network site dropdowninc/theme/site-title.php- Site title customizations
Direct Function Calls:
// From any plugin on any site
if ( function_exists( 'ec_get_blog_id' ) ) {
$newsletter_blog_id = ec_get_blog_id( 'newsletter' );
// Operates on newsletter.extrachill.com (Blog ID 9)
}Graceful Degradation: Plugins check function existence before using
No Cross-Site Data Pollution: Each plugin receives only network-wide configuration
Pattern: switch_to_blog() / restore_current_blog() with try/finally
Why: WordPress requires explicit blog context switching for per-site data
Example:
$blog_id = ec_get_blog_id( 'newsletter' );
try {
switch_to_blog( $blog_id );
// Operate in newsletter site context
$newsletters = get_posts( array( 'post_type' => 'newsletter' ) );
} finally {
restore_current_blog();
}Responsibility: Plugin initialization and loading orchestration
Activation Hook: Validates multisite installation
Plugins_loaded Action: Triggers conditional loading at priority 10
Load Sequence:
- Blog IDs and constants first (all contexts)
- Turnstile functions second (all contexts)
- Network admin files last (network admin only)
Responsibility: Canonical blog ID definitions and helper functions
Exports:
- 10 constants (EC_BLOG_ID_*)
- 4 functions (ec_get_blog_ids, ec_get_blog_id, ec_get_blog_slug_by_id, ec_get_site_url)
- 1 filter hook (ec_site_url_override)
Single Source of Truth: All blog ID knowledge centralized here
Responsibility: Cloudflare Turnstile API integration and validation
Exports:
- 2 getter functions (site key, secret key)
- 2 setter functions (site key, secret key)
- 1 status function (is_configured)
- 2 validation functions (verify response, render widget)
- 1 asset function (enqueue script)
Network Options Driven: All configuration via network options
Network Admin Only: Features requiring admin access verify is_network_admin() and current_user_can( 'manage_network' )
Capability Checks: All settings pages use WordPress capability system
Input Validation: All user input sanitized via WordPress sanitization functions
Token Verification: Server-side verification prevents spoofing
Secure Storage: API keys stored in network options (database protected)
Error Handling: Comprehensive logging without exposing secrets
Graceful Degradation: Forms work without Turnstile, prevents blocking legitimate users
API Keys: Never hardcoded in source (stored in network options)
Configuration: Managed via network admin UI
Environment Compatibility: Works across dev, staging, and production environments
Steps:
- Create WordPress site at subdomain
- Assign blog ID from available pool
- Add constant to
inc/core/blog-ids.php:if ( ! defined( 'EC_BLOG_ID_NEWSITE' ) ) { define( 'EC_BLOG_ID_NEWSITE', 12 ); }
- Add to
ec_get_blog_ids()function array - Add to
ec_get_domain_map()for domain routing - Activate appropriate plugins on new site
- Configure network options if site-specific settings needed
Steps:
- Check if configured:
ec_is_turnstile_configured() - Enqueue script:
ec_enqueue_turnstile_script() - Render widget:
ec_render_turnstile_widget( $args ) - Verify response:
ec_verify_turnstile_response( $response )
Example:
// In extrachill-users registration form
add_action( 'wp_enqueue_scripts', function() {
if ( ec_is_turnstile_configured() ) {
ec_enqueue_turnstile_script();
}
});
// In form template
if ( ec_is_turnstile_configured() ) {
echo ec_render_turnstile_widget();
}
// In AJAX handler
if ( ! ec_verify_turnstile_response( $_POST['cf-turnstile-response'] ) ) {
return wp_send_json_error( 'Verification failed' );
}Do NOT:
- Hardcode blog IDs in plugin code (use helper functions)
- Store Sendy/Turnstile credentials in source code (use network options)
- Assume blog context in network-level code (use blog context detection)
- Create per-site copies of network configuration (use
get_site_option()) - Bypass capability checks on admin pages
- Log sensitive API keys to error logs
- Fail silently on Turnstile errors (log with context)
- Uses
ec_get_blog_id()instead of hardcoded IDs (where applicable) - Network options used for network-wide data
- Blog switching uses try/finally pattern
- Capability checks on all admin pages
- Input sanitization on all user input
- Error logging includes debugging context
- No API keys in source code
- Functions gracefully handle missing configuration
- WordPress: 5.0+ (multisite network)
- PHP: 7.4+
- Multisite: WordPress must be configured for multisite
- Cloudflare Turnstile: Bot prevention service
- Extra Chill Theme: For full integration experience
Build System: Use homeboy build extrachill-network for production builds
Build Output: /build/extrachill-network.zip file only.
File Exclusions: vendor/, docs/, tests/, .git/, .buildignore, build.sh
Deployment:
- Build ZIP via
./build.sh(creates/build/extrachill-network.zip) - Deploy ZIP via Homeboy (
homeboy deploy ...) or your preferred deploy pipeline - Network activate in network admin
- Configure Turnstile keys if using bot prevention
- Verify blog ID helpers accessible to other plugins
Component Documentation:
- extrachill-newsletter CLAUDE.md - Uses blog IDs and Turnstile
- extrachill-users CLAUDE.md - Uses blog IDs and Turnstile
- Root CLAUDE.md - Platform architecture and hardcoded blog ID reference
Related Files:
.github/sunrise.php- Domain mapping for extrachill.link.github/NETWORK-ARCHITECTURE.md- Network structure documentationcomposer.json- Development dependencies.buildignore- Build exclusions
External Resources: