Views: 79
Welcome back to the Versana WordPress Block Theme Development series! Over the past 18 episodes, we’ve built the foundation of our block theme. Now we’re entering Phase 3: Professional Architecture & Design System.
But before we dive into code, we need to address a critical architectural decision that separates amateur themes from professional, best-selling ones:
How should theme.json and a Theme Options page work together?
This isn’t just a technical question—it’s about building a theme that’s:
- Future-proof and aligned with WordPress’s vision
- User-friendly for both beginners and developers
- Competitive with top-selling themes like Kadence and GeneratePress
- Extendable through child themes and plugins
In this episode, we’ll build the correct architecture from the ground up.
Understanding the WordPress Block Theme Ecosystem
WordPress’s Vision: Site Editor First
WordPress is moving toward Full Site Editing (FSE) where:
- theme.json defines the design system structure
- Site Editor is the primary user interface
- Visual customization replaces code editing
Top themes embrace this vision while adding value on top.
The Professional Architecture
Here’s how professional block themes actually work:
┌─────────────────────────────────────────┐
│ THEME OPTIONS PAGE │
│ (Advanced Features & Settings) │
├─────────────────────────────────────────┤
│ ✓ Load Google Fonts │
│ ✓ Performance optimization │
│ ✓ Feature toggles │
│ ✓ Third-party integrations │
│ ✓ Custom scripts │
│ ✓ Developer options │
└─────────────┬───────────────────────────┘
│ Enhances & feeds data
↓
┌─────────────────────────────────────────┐
│ theme.json │
│ (Design System Structure) │
├─────────────────────────────────────────┤
│ ✓ Color palette │
│ ✓ Typography scale │
│ ✓ Spacing system │
│ ✓ Layout settings │
└─────────────┬───────────────────────────┘
│ Powers
↓
┌─────────────────────────────────────────┐
│ SITE EDITOR │
│ (Primary User Interface) │
├─────────────────────────────────────────┤
│ ✓ Visual color selection │
│ ✓ Typography customization │
│ ✓ Block-level styling │
│ ✓ Template editing │
└─────────────────────────────────────────┘
Clear Separation of Concerns
theme.json + Site Editor handle:
- ✅ Design choices (colors, fonts, spacing)
- ✅ Block settings
- ✅ Per-page customization
- ✅ Visual, drag-and-drop interface
- ✅ PRIMARY interface for end users
Theme Options Page handles:
- ✅ Features theme.json CAN’T do
- ✅ Advanced technical settings
- ✅ Performance optimization
- ✅ Third-party integrations
- ✅ ADVANCED interface for power users
What We’ll Build Today
In this episode, we’ll create:
- Enhanced theme.json – Professional design system
- Smart Theme Options Page – Only features theme.json can’t handle:
- Google Fonts Integration (loading fonts)
- Performance Settings (optimization)
- Feature Toggles (enable/disable features)
- Integrations (analytics, custom scripts)
- Advanced Options (custom CSS, developer tools)
Part 1: Building a Professional theme.json
Let’s start by enhancing our theme.json with a complete, professional design system.
Updated theme.json
Replace your current theme.json with this professional version:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"customTemplates": [
{
"name": "blocks-demo",
"title": "Blocks Demo",
"postTypes": ["page"]
},
{
"name": "blog",
"title": "Custom Blog",
"postTypes": ["page"]
}
],
"settings": {
"appearanceTools": true,
"useRootPaddingAwareAlignments": true,
"color": {
"custom": true,
"customDuotone": true,
"customGradient": true,
"defaultGradients": false,
"defaultPalette": false,
"palette": [
{
"slug": "primary",
"color": "#1A73E8",
"name": "Primary"
},
{
"slug": "secondary",
"color": "#E91E63",
"name": "Secondary"
},
{
"slug": "tertiary",
"color": "#9C27B0",
"name": "Tertiary"
},
{
"slug": "neutral-100",
"color": "#FFFFFF",
"name": "Neutral 100"
},
{
"slug": "neutral-200",
"color": "#F5F5F5",
"name": "Neutral 200"
},
{
"slug": "neutral-300",
"color": "#E0E0E0",
"name": "Neutral 300"
},
{
"slug": "neutral-700",
"color": "#424242",
"name": "Neutral 700"
},
{
"slug": "neutral-900",
"color": "#111111",
"name": "Neutral 900"
},
{
"slug": "success",
"color": "#4CAF50",
"name": "Success"
},
{
"slug": "warning",
"color": "#FF9800",
"name": "Warning"
},
{
"slug": "error",
"color": "#F44336",
"name": "Error"
},
{
"slug": "info",
"color": "#2196F3",
"name": "Info"
}
],
"gradients": [
{
"slug": "primary-gradient",
"gradient": "linear-gradient(135deg, #1A73E8 0%, #9C27B0 100%)",
"name": "Primary Gradient"
},
{
"slug": "warm-gradient",
"gradient": "linear-gradient(135deg, #E91E63 0%, #FF9800 100%)",
"name": "Warm Gradient"
}
]
},
"spacing": {
"padding": true,
"margin": true,
"units": ["px", "em", "rem", "%", "vh", "vw"],
"spacingScale": {
"steps": 0
},
"spacingSizes": [
{
"slug": "xs",
"size": "0.5rem",
"name": "XSmall"
},
{
"slug": "sm",
"size": "1rem",
"name": "Small"
},
{
"slug": "md",
"size": "1.5rem",
"name": "Medium"
},
{
"slug": "lg",
"size": "2rem",
"name": "Large"
},
{
"slug": "xl",
"size": "3rem",
"name": "XLarge"
},
{
"slug": "2xl",
"size": "4rem",
"name": "2XLarge"
}
]
},
"typography": {
"customFontSize": true,
"fontStyle": true,
"fontWeight": true,
"letterSpacing": true,
"textDecoration": true,
"textTransform": true,
"dropCap": true,
"fluid": true,
"fontSizes": [
{
"slug": "xs",
"size": "0.75rem",
"name": "XSmall",
"fluid": false
},
{
"slug": "sm",
"size": "0.875rem",
"name": "Small",
"fluid": false
},
{
"slug": "base",
"size": "1rem",
"name": "Base",
"fluid": false
},
{
"slug": "md",
"size": "1.125rem",
"name": "Medium",
"fluid": {
"min": "1rem",
"max": "1.125rem"
}
},
{
"slug": "lg",
"size": "1.25rem",
"name": "Large",
"fluid": {
"min": "1.125rem",
"max": "1.25rem"
}
},
{
"slug": "xl",
"size": "1.5rem",
"name": "XLarge",
"fluid": {
"min": "1.25rem",
"max": "1.5rem"
}
},
{
"slug": "2xl",
"size": "2rem",
"name": "2XLarge",
"fluid": {
"min": "1.5rem",
"max": "2rem"
}
},
{
"slug": "3xl",
"size": "2.5rem",
"name": "3XLarge",
"fluid": {
"min": "2rem",
"max": "2.5rem"
}
},
{
"slug": "4xl",
"size": "3rem",
"name": "4XLarge",
"fluid": {
"min": "2.5rem",
"max": "3rem"
}
}
],
"fontFamilies": [
{
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif",
"slug": "system-sans",
"name": "System Sans-serif"
},
{
"fontFamily": "Georgia, 'Times New Roman', Times, serif",
"slug": "system-serif",
"name": "System Serif"
},
{
"fontFamily": "'Courier New', Courier, monospace",
"slug": "system-mono",
"name": "System Monospace"
}
]
},
"layout": {
"contentSize": "800px",
"wideSize": "1200px"
},
"border": {
"color": true,
"radius": true,
"style": true,
"width": true
},
"shadow": {
"defaultPresets": false,
"presets": [
{
"slug": "sm",
"shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"name": "Small"
},
{
"slug": "md",
"shadow": "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
"name": "Medium"
},
{
"slug": "lg",
"shadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
"name": "Large"
}
]
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--neutral-100)",
"text": "var(--wp--preset--color--neutral-900)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--system-sans)",
"fontSize": "var(--wp--preset--font-size--base)",
"lineHeight": "1.6"
},
"spacing": {
"padding": {
"top": "0",
"right": "var(--wp--preset--spacing--md)",
"bottom": "0",
"left": "var(--wp--preset--spacing--md)"
}
},
"elements": {
"link": {
"color": {
"text": "var(--wp--preset--color--primary)"
},
":hover": {
"color": {
"text": "var(--wp--preset--color--secondary)"
}
}
},
"h1": {
"typography": {
"fontSize": "var(--wp--preset--font-size--4-xl)",
"lineHeight": "1.2",
"fontWeight": "700"
},
"spacing": {
"margin": {
"top": "0",
"bottom": "var(--wp--preset--spacing--md)"
}
}
},
"h2": {
"typography": {
"fontSize": "var(--wp--preset--font-size--3-xl)",
"lineHeight": "1.3",
"fontWeight": "700"
},
"spacing": {
"margin": {
"top": "var(--wp--preset--spacing--lg)",
"bottom": "var(--wp--preset--spacing--sm)"
}
}
},
"h3": {
"typography": {
"fontSize": "var(--wp--preset--font-size--2-xl)",
"lineHeight": "1.4",
"fontWeight": "600"
}
},
"h4": {
"typography": {
"fontSize": "var(--wp--preset--font-size--xl)",
"lineHeight": "1.4",
"fontWeight": "600"
}
},
"h5": {
"typography": {
"fontSize": "var(--wp--preset--font-size--lg)",
"fontWeight": "600"
}
},
"h6": {
"typography": {
"fontSize": "var(--wp--preset--font-size--md)",
"fontWeight": "600"
}
},
"button": {
"border": {
"radius": "4px"
},
"color": {
"background": "var(--wp--preset--color--primary)",
"text": "var(--wp--preset--color--neutral-100)"
},
"spacing": {
"padding": {
"top": "0.75rem",
"right": "1.5rem",
"bottom": "0.75rem",
"left": "1.5rem"
}
},
"typography": {
"fontWeight": "600"
},
":hover": {
"color": {
"background": "var(--wp--preset--color--secondary)"
}
}
}
},
"blocks": {
"core/site-title": {
"typography": {
"fontSize": "var(--wp--preset--font-size--2-xl)",
"fontWeight": "700"
}
},
"core/navigation": {
"typography": {
"fontSize": "var(--wp--preset--font-size--base)"
}
}
}
}
}
Why This theme.json is Professional
1. Complete Design System
- 12 carefully chosen colors (brand + neutrals + semantic)
- 9 font sizes with fluid typography
- 6 spacing sizes following a scale
- Shadow presets for depth
2. Accessibility
- High contrast neutral colors
- Proper color naming
- Semantic color meanings
3. Flexibility
- Multiple font families for different use cases
- Comprehensive spacing options
- Border and shadow utilities
4. Performance
- Disables default WordPress palettes (reduces CSS)
- Uses CSS custom properties efficiently
- Optimized for modern browsers
5. User-Friendly
- Clear, descriptive names
- Logical organization
- Ready for Site Editor
Part 2: Theme Options System Architecture
Now let’s build the Theme Options page that handles what theme.json CAN’T do.
File Structure
versana/
├── functions.php
├── inc/
│ ├── theme-options/
│ │ ├── options-init.php (Initialization)
│ │ ├── options-defaults.php (Default values)
│ │ ├── options-sanitize.php (Security)
│ │ ├── options-page.php (Admin interface)
│ │ ├── options-google-fonts.php (Font loading)
│ │ └── options-output.php (Frontend output)
├── assets/
│ ├── css/
│ │ └── admin.css
│ └── js/
│ └── admin.js
Step 1: Create Default Options
Create: inc/theme-options/options-defaults.php
<?php
/**
* Versana Default Theme Options
*
* Defines defaults for ADVANCED features only.
* Design choices (colors, typography) are in theme.json.
*
* @package Versana
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Get default theme options
*
* These are ADVANCED settings that theme.json cannot handle.
*
* @return array Default options array
*/
function versana_get_default_options() {
$defaults = array(
// Google Fonts Tab
'google_fonts_enabled' => false,
'heading_font_google' => '',
'body_font_google' => '',
'preload_fonts' => true,
'font_display_swap' => true,
// Performance Tab
'lazy_load_images' => true,
'preload_critical_fonts' => true,
'disable_emojis' => false,
'disable_embeds' => false,
'remove_query_strings' => false,
// Features Tab
'enable_breadcrumbs' => false,
'enable_reading_time' => false,
'enable_share_buttons' => false,
'enable_toc' => false,
'enable_sticky_header' => false,
// Integrations Tab
'google_analytics_id' => '',
'facebook_pixel_id' => '',
'google_tag_manager_id' => '',
'header_scripts' => '',
'footer_scripts' => '',
// Advanced Tab
'custom_css' => '',
'enable_developer_mode' => false,
'disable_gutenberg_css' => false,
);
/**
* Filter default theme options
*
* Allows child themes to modify defaults.
*
* @param array $defaults Default options
*/
return apply_filters( 'versana_default_options', $defaults );
}
/**
* Get a single default option value
*
* @param string $key Option key
* @return mixed Default value or null
*/
function versana_get_default_option( $key ) {
$defaults = versana_get_default_options();
return isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
}
Step 2: Create Initialization File
Create: inc/theme-options/options-init.php
<?php
/**
* Versana Theme Options Initialization
*
* Core functions for theme options system.
*
* @package Versana
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Get theme option value
*
* Main function to retrieve option values throughout the theme.
*
* @param string $key Option key
* @param mixed $default Default value if option doesn't exist
* @return mixed Option value
*/
function versana_get_option( $key, $default = null ) {
$options = get_option( 'versana_theme_options', array() );
if ( isset( $options[ $key ] ) ) {
return $options[ $key ];
}
return $default !== null ? $default : versana_get_default_option( $key );
}
/**
* Get all theme options
*
* @return array Complete options array
*/
function versana_get_all_options() {
$saved_options = get_option( 'versana_theme_options', array() );
$defaults = versana_get_default_options();
return wp_parse_args( $saved_options, $defaults );
}
/**
* Update a single theme option
*
* @param string $key Option key
* @param mixed $value New value
* @return bool True if successful
*/
function versana_update_option( $key, $value ) {
$options = get_option( 'versana_theme_options', array() );
$options[ $key ] = $value;
return update_option( 'versana_theme_options', $options );
}
/**
* Register theme settings with WordPress
*/
function versana_register_theme_settings() {
register_setting(
'versana_options',
'versana_theme_options',
array(
'type' => 'array',
'sanitize_callback' => 'versana_sanitize_options',
'default' => versana_get_default_options(),
)
);
}
add_action( 'admin_init', 'versana_register_theme_settings' );
/**
* Enqueue admin assets
*
* @param string $hook Current admin page
*/
function versana_enqueue_admin_assets( $hook ) {
if ( 'appearance_page_versana-options' !== $hook ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_style(
'versana-admin',
get_template_directory_uri() . '/assets/css/admin.css',
array(),
'1.0.0'
);
wp_enqueue_script(
'versana-admin',
get_template_directory_uri() . '/assets/js/admin.js',
array( 'jquery', 'wp-color-picker' ),
'1.0.0',
true
);
}
add_action( 'admin_enqueue_scripts', 'versana_enqueue_admin_assets' );
Step 3: Create Sanitization File
Create: inc/theme-options/options-sanitize.php
<?php
/**
* Versana Theme Options Sanitization
*
* SECURITY CRITICAL: All user input must be sanitized.
*
* @package Versana
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Sanitize all theme options
*
* @param array $input Raw input from form
* @return array Sanitized options
*/
function versana_sanitize_options( $input ) {
$sanitized = array();
// Boolean fields (checkboxes)
$boolean_fields = array(
'google_fonts_enabled',
'preload_fonts',
'font_display_swap',
'lazy_load_images',
'preload_critical_fonts',
'disable_emojis',
'disable_embeds',
'remove_query_strings',
'enable_breadcrumbs',
'enable_reading_time',
'enable_share_buttons',
'enable_toc',
'enable_sticky_header',
'enable_developer_mode',
'disable_gutenberg_css',
);
foreach ( $boolean_fields as $field ) {
$sanitized[ $field ] = isset( $input[ $field ] ) ? (bool) $input[ $field ] : false;
}
// Text fields
$text_fields = array(
'heading_font_google',
'body_font_google',
'google_analytics_id',
'facebook_pixel_id',
'google_tag_manager_id',
);
foreach ( $text_fields as $field ) {
if ( isset( $input[ $field ] ) ) {
$sanitized[ $field ] = sanitize_text_field( $input[ $field ] );
}
}
// Custom CSS
if ( isset( $input['custom_css'] ) ) {
$sanitized['custom_css'] = wp_strip_all_tags( $input['custom_css'] );
}
// Scripts (only for administrators)
if ( current_user_can( 'unfiltered_html' ) ) {
if ( isset( $input['header_scripts'] ) ) {
$sanitized['header_scripts'] = $input['header_scripts'];
}
if ( isset( $input['footer_scripts'] ) ) {
$sanitized['footer_scripts'] = $input['footer_scripts'];
}
}
/**
* Filter sanitized options
*
* @param array $sanitized Sanitized options
* @param array $input Raw input
*/
return apply_filters( 'versana_sanitize_options', $sanitized, $input );
}
/**
* Validate Google Analytics ID format
*
* @param string $id Analytics ID
* @return bool True if valid
*/
function versana_validate_analytics_id( $id ) {
return preg_match( '/^(UA|G)-[0-9]+-[0-9]+$/', $id ) || preg_match( '/^G-[A-Z0-9]+$/', $id );
}
Step 4: Create Admin Page
Create: inc/theme-options/options-page.php
<?php
/**
* Versana Theme Options Admin Page
*
* Renders the admin interface for ADVANCED settings only.
* Design customization happens in Site Editor.
*
* @package Versana
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add theme options page to admin menu
*/
function versana_add_theme_options_page() {
add_theme_page(
__( 'Versana Options', 'versana' ),
__( 'Theme Options', 'versana' ),
'edit_theme_options',
'versana-options',
'versana_render_options_page',
2
);
}
add_action( 'admin_menu', 'versana_add_theme_options_page' );
/**
* Render the main options page
*/
function versana_render_options_page() {
if ( ! current_user_can( 'edit_theme_options' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.', 'versana' ) );
}
$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'fonts';
$valid_tabs = array( 'fonts', 'performance', 'features', 'integrations', 'advanced' );
if ( ! in_array( $active_tab, $valid_tabs, true ) ) {
$active_tab = 'fonts';
}
?>
<div class="wrap versana-options-wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<div class="versana-options-header">
<p class="description">
<?php esc_html_e( 'Configure advanced features and settings. For design customization (colors, typography, spacing), use the Site Editor under Appearance → Editor.', 'versana' ); ?>
</p>
<a href="<?php echo esc_url( admin_url( 'site-editor.php' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Open Site Editor', 'versana' ); ?>
</a>
</div>
<?php settings_errors(); ?>
<h2 class="nav-tab-wrapper">
<a href="?page=versana-options&tab=fonts"
class="nav-tab <?php echo $active_tab === 'fonts' ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-editor-textcolor"></span>
<?php esc_html_e( 'Google Fonts', 'versana' ); ?>
</a>
<a href="?page=versana-options&tab=performance"
class="nav-tab <?php echo $active_tab === 'performance' ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-performance"></span>
<?php esc_html_e( 'Performance', 'versana' ); ?>
</a>
<a href="?page=versana-options&tab=features"
class="nav-tab <?php echo $active_tab === 'features' ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-admin-plugins"></span>
<?php esc_html_e( 'Features', 'versana' ); ?>
</a>
<a href="?page=versana-options&tab=integrations"
class="nav-tab <?php echo $active_tab === 'integrations' ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-admin-links"></span>
<?php esc_html_e( 'Integrations', 'versana' ); ?>
</a>
<a href="?page=versana-options&tab=advanced"
class="nav-tab <?php echo $active_tab === 'advanced' ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-admin-generic"></span>
<?php esc_html_e( 'Advanced', 'versana' ); ?>
</a>
</h2>
<form method="post" action="options.php">
<?php
settings_fields( 'versana_options' );
switch ( $active_tab ) {
case 'performance':
versana_render_performance_tab();
break;
case 'features':
versana_render_features_tab();
break;
case 'integrations':
versana_render_integrations_tab();
break;
case 'advanced':
versana_render_advanced_tab();
break;
case 'fonts':
default:
versana_render_fonts_tab();
break;
}
submit_button();
?>
</form>
</div>
<?php
}
/**
* Render Google Fonts tab
*/
function versana_render_fonts_tab() {
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Google Fonts Integration', 'versana' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Load custom fonts from Google Fonts. Font selection will be added in the next episode.', 'versana' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<?php esc_html_e( 'Enable Google Fonts', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[google_fonts_enabled]"
value="1"
<?php checked( versana_get_option( 'google_fonts_enabled' ), true ); ?> />
<?php esc_html_e( 'Load fonts from Google Fonts', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Enable this to use Google Fonts instead of system fonts.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="heading_font_google">
<?php esc_html_e( 'Heading Font', 'versana' ); ?>
</label>
</th>
<td>
<input type="text"
id="heading_font_google"
name="versana_theme_options[heading_font_google]"
value="<?php echo esc_attr( versana_get_option( 'heading_font_google' ) ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., Inter', 'versana' ); ?>" />
<p class="description">
<?php esc_html_e( 'Google Font name for headings. We\'ll add a font picker in Episode 20.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="body_font_google">
<?php esc_html_e( 'Body Font', 'versana' ); ?>
</label>
</th>
<td>
<input type="text"
id="body_font_google"
name="versana_theme_options[body_font_google]"
value="<?php echo esc_attr( versana_get_option( 'body_font_google' ) ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., Roboto', 'versana' ); ?>" />
<p class="description">
<?php esc_html_e( 'Google Font name for body text.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Font Display', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[font_display_swap]"
value="1"
<?php checked( versana_get_option( 'font_display_swap' ), true ); ?> />
<?php esc_html_e( 'Use font-display: swap', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Prevents invisible text while fonts load. Recommended for performance.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Preload Fonts', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[preload_fonts]"
value="1"
<?php checked( versana_get_option( 'preload_fonts' ), true ); ?> />
<?php esc_html_e( 'Preload critical font files', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Improves performance by loading fonts earlier. Recommended.', 'versana' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
</div>
<?php
}
/**
* Render Performance tab
*/
function versana_render_performance_tab() {
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Performance Optimization', 'versana' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Optimize your site for speed and performance.', 'versana' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<?php esc_html_e( 'Image Optimization', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[lazy_load_images]"
value="1"
<?php checked( versana_get_option( 'lazy_load_images' ), true ); ?> />
<?php esc_html_e( 'Enable lazy loading for images', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Images load only when they\'re about to enter the viewport.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Font Loading', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[preload_critical_fonts]"
value="1"
<?php checked( versana_get_option( 'preload_critical_fonts' ), true ); ?> />
<?php esc_html_e( 'Preload critical fonts', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Loads essential fonts earlier for faster text rendering.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Disable Emojis', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[disable_emojis]"
value="1"
<?php checked( versana_get_option( 'disable_emojis' ), true ); ?> />
<?php esc_html_e( 'Remove WordPress emoji scripts', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Reduces HTTP requests. Modern browsers support emojis natively.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Disable Embeds', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[disable_embeds]"
value="1"
<?php checked( versana_get_option( 'disable_embeds' ), true ); ?> />
<?php esc_html_e( 'Remove WordPress embed scripts', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Disables oEmbed discovery. Only disable if you don\'t use embeds.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Query Strings', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[remove_query_strings]"
value="1"
<?php checked( versana_get_option( 'remove_query_strings' ), true ); ?> />
<?php esc_html_e( 'Remove query strings from static resources', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Some caching systems cache better without query strings.', 'versana' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
</div>
<?php
}
/**
* Render Features tab
*/
function versana_render_features_tab() {
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Theme Features', 'versana' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Enable or disable theme features. These will be built in future episodes.', 'versana' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<?php esc_html_e( 'Breadcrumbs', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_breadcrumbs]"
value="1"
<?php checked( versana_get_option( 'enable_breadcrumbs' ), true ); ?> />
<?php esc_html_e( 'Show breadcrumb navigation', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Displays breadcrumb trail for better navigation and SEO.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Reading Time', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_reading_time]"
value="1"
<?php checked( versana_get_option( 'enable_reading_time' ), true ); ?> />
<?php esc_html_e( 'Show estimated reading time', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Displays estimated reading time for posts and pages.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Share Buttons', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_share_buttons]"
value="1"
<?php checked( versana_get_option( 'enable_share_buttons' ), true ); ?> />
<?php esc_html_e( 'Show social share buttons', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Adds social sharing buttons to posts.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Table of Contents', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_toc]"
value="1"
<?php checked( versana_get_option( 'enable_toc' ), true ); ?> />
<?php esc_html_e( 'Auto-generate table of contents', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Automatically creates TOC from headings in long posts.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Sticky Header', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_sticky_header]"
value="1"
<?php checked( versana_get_option( 'enable_sticky_header' ), true ); ?> />
<?php esc_html_e( 'Make header sticky on scroll', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Header stays visible when scrolling down the page.', 'versana' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
</div>
<?php
}
/**
* Render Integrations tab
*/
function versana_render_integrations_tab() {
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Third-Party Integrations', 'versana' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Connect your site with third-party services.', 'versana' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<label for="google_analytics_id">
<?php esc_html_e( 'Google Analytics', 'versana' ); ?>
</label>
</th>
<td>
<input type="text"
id="google_analytics_id"
name="versana_theme_options[google_analytics_id]"
value="<?php echo esc_attr( versana_get_option( 'google_analytics_id' ) ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'G-XXXXXXXXXX or UA-XXXXXX-X', 'versana' ); ?>" />
<p class="description">
<?php esc_html_e( 'Enter your Google Analytics Measurement ID or Tracking ID.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="google_tag_manager_id">
<?php esc_html_e( 'Google Tag Manager', 'versana' ); ?>
</label>
</th>
<td>
<input type="text"
id="google_tag_manager_id"
name="versana_theme_options[google_tag_manager_id]"
value="<?php echo esc_attr( versana_get_option( 'google_tag_manager_id' ) ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'GTM-XXXXXX', 'versana' ); ?>" />
<p class="description">
<?php esc_html_e( 'Enter your Google Tag Manager container ID.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="facebook_pixel_id">
<?php esc_html_e( 'Facebook Pixel', 'versana' ); ?>
</label>
</th>
<td>
<input type="text"
id="facebook_pixel_id"
name="versana_theme_options[facebook_pixel_id]"
value="<?php echo esc_attr( versana_get_option( 'facebook_pixel_id' ) ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'XXXXXXXXXXXXXXXX', 'versana' ); ?>" />
<p class="description">
<?php esc_html_e( 'Enter your Facebook Pixel ID for conversion tracking.', 'versana' ); ?>
</p>
</td>
</tr>
<?php if ( current_user_can( 'unfiltered_html' ) ) : ?>
<tr>
<th scope="row">
<label for="header_scripts">
<?php esc_html_e( 'Header Scripts', 'versana' ); ?>
</label>
</th>
<td>
<textarea id="header_scripts"
name="versana_theme_options[header_scripts]"
rows="5"
class="large-text code"><?php echo esc_textarea( versana_get_option( 'header_scripts' ) ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Scripts added here will be inserted into the <head> section. Include <script> tags.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="footer_scripts">
<?php esc_html_e( 'Footer Scripts', 'versana' ); ?>
</label>
</th>
<td>
<textarea id="footer_scripts"
name="versana_theme_options[footer_scripts]"
rows="5"
class="large-text code"><?php echo esc_textarea( versana_get_option( 'footer_scripts' ) ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Scripts added here will be inserted before </body>. Include <script> tags.', 'versana' ); ?>
</p>
</td>
</tr>
<?php else : ?>
<tr>
<th scope="row">
<?php esc_html_e( 'Custom Scripts', 'versana' ); ?>
</th>
<td>
<p class="description">
<?php esc_html_e( 'Custom script fields are only available to administrators for security reasons.', 'versana' ); ?>
</p>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
/**
* Render Advanced tab
*/
function versana_render_advanced_tab() {
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Advanced Options', 'versana' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Advanced settings for developers and power users.', 'versana' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<label for="custom_css">
<?php esc_html_e( 'Custom CSS', 'versana' ); ?>
</label>
</th>
<td>
<textarea id="custom_css"
name="versana_theme_options[custom_css]"
rows="10"
class="large-text code"><?php echo esc_textarea( versana_get_option( 'custom_css' ) ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Add custom CSS that will be applied to your site. Do not include <style> tags.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Developer Mode', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_developer_mode]"
value="1"
<?php checked( versana_get_option( 'enable_developer_mode' ), true ); ?> />
<?php esc_html_e( 'Enable developer mode', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Shows additional debugging information. Only enable during development.', 'versana' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Gutenberg CSS', 'versana' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[disable_gutenberg_css]"
value="1"
<?php checked( versana_get_option( 'disable_gutenberg_css' ), true ); ?> />
<?php esc_html_e( 'Disable default Gutenberg block CSS', 'versana' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'For advanced users who want complete control over block styles.', 'versana' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
<div class="versana-reset-section">
<h3><?php esc_html_e( 'Reset Options', 'versana' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Reset all theme options to their default values. This action cannot be undone.', 'versana' ); ?>
</p>
<button type="button" class="button button-secondary versana-reset-options">
<?php esc_html_e( 'Reset to Defaults', 'versana' ); ?>
</button>
</div>
</div>
<?php
}
Step 5: Create Frontend Output
Create: `inc/theme-options/options-output.php`
<?php
/**
* Versana Theme Options Frontend Output
*
* Outputs theme option values to the frontend.
*
* @package Versana
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Output custom CSS to head
*/
function versana_output_custom_css() {
$custom_css = versana_get_option( 'custom_css' );
if ( empty( $custom_css ) ) {
return;
}
?>
<style id="versana-custom-css" type="text/css">
<?php echo wp_strip_all_tags( $custom_css ); ?>
</style>
<?php
}
add_action( 'wp_head', 'versana_output_custom_css', 99 );
/**
* Output Google Analytics tracking code
*/
function versana_output_google_analytics() {
$analytics_id = versana_get_option( 'google_analytics_id' );
if ( empty( $analytics_id ) || ! versana_validate_analytics_id( $analytics_id ) ) {
return;
}
// Google Analytics 4 (G-XXXXXXXXXX)
if ( strpos( $analytics_id, 'G-' ) === 0 ) {
?>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo esc_attr( $analytics_id ); ?>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<?php echo esc_js( $analytics_id ); ?>');
</script>
<?php
}
// Universal Analytics (UA-XXXXXXX-X)
else {
?>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/analytics.js"></script>
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', '<?php echo esc_js( $analytics_id ); ?>', 'auto');
ga('send', 'pageview');
</script>
<?php
}
}
add_action( 'wp_head', 'versana_output_google_analytics', 10 );
/**
* Output Google Tag Manager head code
*/
function versana_output_gtm_head() {
$gtm_id = versana_get_option( 'google_tag_manager_id' );
if ( empty( $gtm_id ) ) {
return;
}
?>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<?php echo esc_js( $gtm_id ); ?>');</script>
<!-- End Google Tag Manager -->
<?php
}
add_action( 'wp_head', 'versana_output_gtm_head', 5 );
/**
* Output Google Tag Manager body code
*/
function versana_output_gtm_body() {
$gtm_id = versana_get_option( 'google_tag_manager_id' );
if ( empty( $gtm_id ) ) {
return;
}
?>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo esc_attr( $gtm_id ); ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<?php
}
add_action( 'wp_body_open', 'versana_output_gtm_body' );
/**
* Output Facebook Pixel code
*/
function versana_output_facebook_pixel() {
$pixel_id = versana_get_option( 'facebook_pixel_id' );
if ( empty( $pixel_id ) ) {
return;
}
?>
<!-- Facebook Pixel Code -->
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '<?php echo esc_js( $pixel_id ); ?>');
fbq('track', 'PageView');
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=<?php echo esc_attr( $pixel_id ); ?>&ev=PageView&noscript=1"
/></noscript>
<!-- End Facebook Pixel Code -->
<?php
}
add_action( 'wp_head', 'versana_output_facebook_pixel', 10 );
/**
* Output header scripts
*/
function versana_output_header_scripts() {
$header_scripts = versana_get_option( 'header_scripts' );
if ( empty( $header_scripts ) ) {
return;
}
echo $header_scripts;
}
add_action( 'wp_head', 'versana_output_header_scripts', 100 );
/**
* Output footer scripts
*/
function versana_output_footer_scripts() {
$footer_scripts = versana_get_option( 'footer_scripts' );
if ( empty( $footer_scripts ) ) {
return;
}
echo $footer_scripts;
}
add_action( 'wp_footer', 'versana_output_footer_scripts', 100 );
/**
* Disable emojis if option is enabled
*/
function versana_disable_emojis() {
if ( ! versana_get_option( 'disable_emojis' ) ) {
return;
}
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
}
add_action( 'init', 'versana_disable_emojis' );
/**
* Disable embeds if option is enabled
*/
function versana_disable_embeds() {
if ( ! versana_get_option( 'disable_embeds' ) ) {
return;
}
global $wp;
$wp->public_query_vars = array_diff( $wp->public_query_vars, array( 'embed' ) );
remove_action( 'rest_api_init', 'wp_oembed_register_route' );
remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 );
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
}
add_action( 'init', 'versana_disable_embeds', 9999 );
Step 6: Create Admin Assets
Create: assets/css/admin.css
/**
* Versana Admin Styles
*
* @package Versana
* @since 1.0.0
*/
/* Main Wrapper */
.versana-options-wrap {
max-width: 1200px;
margin: 20px 0;
}
/* Header Section */
.versana-options-header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px 0 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #1A73E8;
}
.versana-options-header .description {
margin: 0;
flex: 1;
}
.versana-options-header .button-primary {
margin-left: 20px;
}
/* Tab Navigation */
.versana-options-wrap .nav-tab-wrapper {
margin: 20px 0 30px 0;
border-bottom: 1px solid #ccd0d4;
}
.versana-options-wrap .nav-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
}
.versana-options-wrap .nav-tab .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
}
/* Tab Content */
.versana-tab-content {
background: #fff;
padding: 20px;
border: 1px solid #c3c4c7;
border-radius: 4px;
margin-bottom: 20px;
}
.versana-tab-content h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 2px solid #1A73E8;
}
.versana-tab-content > .description {
font-size: 14px;
margin: 10px 0 20px;
padding: 12px;
background: #e7f5ff;
border-left: 4px solid #2196F3;
border-radius: 4px;
}
/* Form Table */
.versana-options-wrap .form-table {
margin-top: 20px;
}
.versana-options-wrap .form-table th {
width: 250px;
padding: 20px 10px 20px 0;
vertical-align: top;
font-weight: 600;
}
.versana-options-wrap .form-table td {
padding: 15px 10px;
}
.versana-options-wrap .form-table label {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.versana-options-wrap .form-table input[type="checkbox"] {
margin: 0;
}
/* Field Descriptions */
.versana-options-wrap .description {
font-style: italic;
color: #646970;
margin-top: 8px;
display: block;
line-height: 1.5;
}
/* Input Fields */
.versana-options-wrap input[type="text"],
.versana-options-wrap input[type="number"] {
padding: 8px 12px;
border-radius: 4px;
}
.versana-options-wrap .regular-text {
width: 400px;
max-width: 100%;
}
/* Code Editor */
textarea.code {
font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
background: #f6f7f7;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 12px;
}
/* Submit Button */
.versana-options-wrap .submit {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #dcdcde;
}
/* Reset Section */
.versana-reset-section {
margin-top: 40px;
padding: 20px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
}
.versana-reset-section h3 {
margin-top: 0;
color: #856404;
}
.versana-reset-section .description {
color: #856404;
}
.versana-reset-options {
margin-top: 10px;
}
/* Settings Saved Message */
.versana-options-wrap .updated {
margin: 20px 0;
}
/* Color Picker */
.wp-picker-container {
display: inline-block;
}
/* Responsive */
@media screen and (max-width: 782px) {
.versana-options-header {
flex-direction: column;
align-items: flex-start;
}
.versana-options-header .button-primary {
margin-left: 0;
margin-top: 15px;
}
.versana-options-wrap .form-table th,
.versana-options-wrap .form-table td {
display: block;
width: 100%;
padding: 10px 0;
}
.versana-options-wrap .form-table th {
padding-bottom: 5px;
}
.versana-options-wrap .regular-text {
width: 100%;
}
}
Create: assets/js/admin.js
/**
* Versana Admin JavaScript
*
* @package Versana
* @since 1.0.0
*/
(function($) {
'use strict';
$(document).ready(function() {
/**
* Initialize WordPress Color Pickers
*/
if (typeof $.fn.wpColorPicker !== 'undefined') {
$('.versana-color-picker').wpColorPicker();
}
/**
* Reset to Defaults
*/
$('.versana-reset-options').on('click', function(e) {
e.preventDefault();
if (!confirm('Are you sure you want to reset all theme options to their default values? This action cannot be undone.')) {
return;
}
// Submit form with reset flag
var form = $(this).closest('form');
$('<input>').attr({
type: 'hidden',
name: 'versana_reset_options',
value: '1'
}).appendTo(form);
form.submit();
});
/**
* Validation for analytics IDs
*/
$('#google_analytics_id').on('blur', function() {
var value = $(this).val().trim();
if (value && !value.match(/^(UA|G)-[0-9]+-[0-9]+$/) && !value.match(/^G-[A-Z0-9]+$/)) {
alert('Please enter a valid Google Analytics ID (format: G-XXXXXXXXXX or UA-XXXXXX-X)');
}
});
/**
* Tab switching with localStorage
*/
$('.nav-tab').on('click', function() {
var tab = $(this).attr('href').split('tab=')[1];
if (tab) {
localStorage.setItem('versana_active_tab', tab);
}
});
// Restore active tab on page load
var savedTab = localStorage.getItem('versana_active_tab');
if (savedTab) {
var tabUrl = window.location.href.split('&tab=')[0] + '&tab=' + savedTab;
if (window.location.href.indexOf('tab=') === -1) {
// Don't auto-navigate on first load
localStorage.removeItem('versana_active_tab');
}
}
});
})(jQuery);
Step 7: Update functions.php
Update your functions.php to load all the files:
<?php
/**
* Versana Theme Functions
*
* @package Versana
* @since 1.0.0
*/
// Theme setup
add_action( 'init', 'versana_register_pattern_categories' );
function versana_register_pattern_categories() {
register_block_pattern_category(
'versana-sections',
array( 'label' => __( 'Versana Sections', 'versana' ) )
);
}
/**
* Load theme options system
*
* Files are loaded in specific order for dependencies.
* Only admin page loads in admin area (conditional loading).
*/
$theme_options_path = get_template_directory() . '/inc/theme-options/';
// Load in order of dependency
if ( file_exists( $theme_options_path . 'options-defaults.php' ) ) {
require_once $theme_options_path . 'options-defaults.php';
}
if ( file_exists( $theme_options_path . 'options-sanitize.php' ) ) {
require_once $theme_options_path . 'options-sanitize.php';
}
if ( file_exists( $theme_options_path . 'options-init.php' ) ) {
require_once $theme_options_path . 'options-init.php';
}
// Admin page (conditional - only in admin)
if ( is_admin() && file_exists( $theme_options_path . 'options-page.php' ) ) {
require_once $theme_options_path . 'options-page.php';
}
// Frontend output
if ( file_exists( $theme_options_path . 'options-output.php' ) ) {
require_once $theme_options_path . 'options-output.php';
}
// Google Fonts integration (will be built in Episode 20)
if ( file_exists( $theme_options_path . 'options-google-fonts.php' ) ) {
require_once $theme_options_path . 'options-google-fonts.php';
}
Understanding the Professional Architecture
Let’s break down why this approach is superior:
1. Clear Separation of Concerns
theme.json handles:
{
"settings": {
"color": {
"palette": [...] // Users choose from these
},
"typography": {
"fontSizes": [...] // Users choose from these
}
}
}
Theme Options handles:
// Loading Google Fonts (theme.json can't do this)
'google_fonts_enabled' => true,
'heading_font_google' => 'Inter',
// Performance (theme.json can't do this)
'lazy_load_images' => true,
'preload_fonts' => true,
// Integrations (theme.json can't do this)
'google_analytics_id' => 'G-XXXXXXXXXX',
Result: No duplication, no confusion, clear purposes.
2. Future-Proof Design
This architecture aligns with WordPress’s long-term vision:
- Site Editor is the PRIMARY customization interface
- theme.json is the design system structure
- Theme options provide ADVANCED features
As WordPress evolves, this architecture will adapt easily.
3. User Experience
For Beginners:
- Use Site Editor for colors, fonts, spacing
- Visual, drag-and-drop interface
- No code required
For Advanced Users:
- Use Theme Options for features and integrations
- Technical settings in one place
- Developer-friendly options
4. Professional Credibility
This is how top themes work:
- Kadence: Site Editor for design, theme options for features
- GeneratePress: Same approach
- Blocksy: Same approach
It’s the industry standard for a reason.
Testing Your Implementation
Step 1: Verify File Structure
Ensure all files are in place:
versana/
├── theme.json (updated)
├── functions.php (updated)
├── inc/theme-options/
│ ├── options-init.php ✓
│ ├── options-defaults.php ✓
│ ├── options-sanitize.php ✓
│ ├── options-page.php ✓
│ └── options-output.php ✓
└── assets/
├── css/admin.css ✓
└── js/admin.js ✓
Step 2: Access Theme Options
- Go to Appearance → Theme Options
- You should see 5 tabs with icons
- Header shows link to Site Editor
Step 3: Test Each Tab
Google Fonts Tab:
- Enable Google Fonts
- Enter font names (e.g., “Inter”, “Roboto”)
- Save changes
Performance Tab:
- Enable lazy loading
- Disable emojis
- Save and check frontend source code
Features Tab:
- Enable breadcrumbs (placeholder for now)
- Save changes
Integrations Tab:
- Enter Google Analytics ID
- Save and view page source
- You should see the tracking code
Advanced Tab:
- Add custom CSS
- Enable developer mode
- Save and verify output
Step 4: Verify Site Editor Integration
- Go to Appearance → Editor
- Click “Styles” → “Colors”
- You should see your professional color palette
- Click “Typography”
- You should see the complete font size scale
Step 5: Test Frontend Output
View your site’s source code and verify:
- Custom CSS appears in
<head> - Analytics tracking code is present
- No emoji scripts if disabled
- Clean, optimized code
What We’ve Accomplished
- Professional theme.json with complete design system
- Smart Theme Options page for advanced features only
- Clear separation of design vs. features
- Future-proof architecture aligned with WordPress vision
- 5 organized tabs for different settings
- Complete integrations (Analytics, GTM, Facebook Pixel)
- Performance optimizations built-in
- Security-first approach with proper sanitization
- Extensibility through hooks and filters
- Professional code structure following WordPress standards
Key Architectural Principles
1. WordPress’s Vision First
We embrace Full Site Editing:
- theme.json for design system
- Site Editor for customization
- Theme options for enhancements
2. Don’t Duplicate Functionality
- Colors → theme.json (not theme options)
- Typography → theme.json (not theme options)
- Spacing → theme.json (not theme options)
- Features → Theme options (not theme.json)
3. User Experience Hierarchy
Beginner Users → Site Editor (visual, intuitive)
↓
Power Users → Theme Options (technical, advanced)
↓
Developers → Code/Hooks (maximum control)
4. Extensibility Layer
Every level can be extended:
// Child theme can modify defaults
add_filter( 'versana_default_options', 'child_modify_defaults' );
// Plugin can add custom tabs
add_action( 'versana_after_options_tabs', 'plugin_add_tab' );
// Developer can override output
add_filter( 'versana_custom_css_output', 'custom_css_function' );
What’s Coming in Episode 20
Now that we have the correct architecture, we’ll enhance it with:
- Google Fonts Selector
- Browse 900+ Google Fonts
- Live preview
- Font pairing suggestions
- Weight and style selection
- Advanced Typography System
- Font loading optimization
- Subset selection
- Preload configuration
- Display swap settings
- Performance Dashboard
- PageSpeed insights integration
- Optimization recommendations
- Before/after comparisons
- Import/Export Settings
- Backup theme options
- Share configurations
- Quick setup for new sites
Why This Approach Wins
Compared to Other Approaches
❌ Bad: Duplicate theme.json in Theme Options
- Confusing for users
- Conflicts between systems
- Not future-proof
❌ Bad: Ignore Site Editor Entirely
- Miss WordPress’s vision
- Poor user experience
- Not competitive
✅ Good: Our Approach
- Embrace WordPress’s vision
- Clear separation of concerns
- Best of both worlds
- Industry standard
Real-World Benefits
For Theme Developers:
- Easier maintenance
- Clear code organization
- Future-proof architecture
For Users:
- Intuitive design customization (Site Editor)
- Powerful advanced features (Theme Options)
- No confusion about where to go
For Child Themes:
- Clear extension points
- Can enhance either layer
- Won’t break with updates
Best Practices We’re Following
1. Security
// Always check capabilities
if ( ! current_user_can( 'edit_theme_options' ) ) {
wp_die();
}
// Always sanitize
$sanitized = sanitize_text_field( $input );
// Always escape
echo esc_html( $value );
// Limit powerful features
if ( current_user_can( 'unfiltered_html' ) ) {
// Only then allow scripts
}
2. Performance
// Conditional loading
if ( is_admin() ) {
require_once 'options-page.php';
}
// Conditional asset loading
if ( 'appearance_page_versana-options' !== $hook ) {
return;
}
// Efficient database queries
$options = get_option( 'versana_theme_options', array() );
3. Extensibility
// Provide filters
return apply_filters( 'versana_default_options', $defaults );
// Provide actions
do_action( 'versana_before_options_save' );
// Clear naming
function versana_get_option( $key ) { }
4. User Experience
- Clear descriptions for every option
- Logical grouping in tabs
- Visual feedback
- Link to Site Editor
- Icon-enhanced navigation
Conclusion
We’ve built the professional foundation for Versana theme that:
✅ Aligns with WordPress’s vision for block themes
✅ Separates design from features clearly
✅ Provides flexibility for all user types
✅ Follows industry standards from top themes
✅ Is future-proof and maintainable
✅ Extensible through hooks and filters
This is the architecture that will make Versana competitive with best-selling themes while staying true to WordPress’s Full Site Editing vision.
In Episode 20, we’ll add the Google Fonts integration with a beautiful font selector, live preview, and optimization features that will make Versana stand out!
Resources
- WordPress theme.json Documentation
- Full Site Editing Guide
- WordPress Settings API
- Block Theme Best Practices
Have questions about the architecture? Drop them in the comments!