@lebtiga/sonic-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE.txt +223 -0
  2. package/README.md +61 -0
  3. package/bin/sonic.js +304 -0
  4. package/lib/index.js +20 -0
  5. package/lib/installer.js +156 -0
  6. package/lib/license.js +48 -0
  7. package/package.json +46 -0
  8. package/plugin/.claude-plugin/plugin.json +13 -0
  9. package/plugin/README.md +100 -0
  10. package/plugin/agents/sonic.md +80 -0
  11. package/plugin/commands/sonic-build.md +145 -0
  12. package/plugin/commands/sonic-help.md +71 -0
  13. package/plugin/skills/accessibility-qa/SKILL.md +160 -0
  14. package/plugin/skills/accessibility-qa/templates/accessibility-qa-report-template.md +123 -0
  15. package/plugin/skills/accessibility-qa/templates/wcag-compliance-statement.md +70 -0
  16. package/plugin/skills/aka-wireframe-wp/SKILL.md +149 -0
  17. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/README.md +190 -0
  18. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/footer.php +49 -0
  19. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/functions.php +395 -0
  20. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/header.php +58 -0
  21. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/index.php +39 -0
  22. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-answer.php +62 -0
  23. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-authority-hub.php +122 -0
  24. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-knowledge.php +58 -0
  25. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/style.css +633 -0
  26. package/plugin/skills/aka-wireframe-wp/references/content-generator.md +371 -0
  27. package/plugin/skills/aka-wireframe-wp/references/internal-linker.md +430 -0
  28. package/plugin/skills/aka-wireframe-wp/references/orchestrator.md +269 -0
  29. package/plugin/skills/aka-wireframe-wp/references/prompts-library.md +880 -0
  30. package/plugin/skills/aka-wireframe-wp/references/seo-optimizer.md +433 -0
  31. package/plugin/skills/aka-wireframe-wp/references/strategy-planner.md +317 -0
  32. package/plugin/skills/aka-wireframe-wp/references/wordpress-deployer.md +545 -0
  33. package/plugin/skills/authority-site-builder/SKILL.md +138 -0
  34. package/plugin/skills/brand-philosophy/SKILL.md +77 -0
  35. package/plugin/skills/freepik-spaces/SKILL.md +122 -0
  36. package/plugin/skills/freepik-spaces/docs/automation-guide.md +233 -0
  37. package/plugin/skills/freepik-spaces/docs/research-notes.md +264 -0
  38. package/plugin/skills/freepik-spaces/plans/naseberry-demo-plan.md +320 -0
  39. package/plugin/skills/freepik-spaces/templates/naseberry-demo.json +302 -0
  40. package/plugin/skills/freepik-spaces/templates/saas-demo.json +212 -0
  41. package/plugin/skills/frontend-design/LICENSE.txt +177 -0
  42. package/plugin/skills/frontend-design/SKILL.md +77 -0
  43. package/plugin/skills/programmatic-seo/SKILL.md +236 -0
  44. package/plugin/skills/programmatic-seo/references/playbooks.md +293 -0
  45. package/plugin/skills/seo-qa/SKILL.md +132 -0
  46. package/plugin/skills/seo-qa/templates/schema-localbusiness.json +49 -0
  47. package/plugin/skills/seo-qa/templates/schema-service.json +36 -0
  48. package/plugin/skills/seo-qa/templates/seo-qa-report-template.md +90 -0
  49. package/plugin/skills/visual-identity/SKILL.md +109 -0
  50. package/plugin/skills/visual-identity/templates/style-guide-template.md +108 -0
  51. package/plugin/skills/website-image-gen/SKILL.md +82 -0
  52. package/plugin/skills/website-image-gen/templates/blog-featured.md +56 -0
  53. package/plugin/skills/website-image-gen/templates/hero-service-photo.md +56 -0
  54. package/plugin/skills/wordpress-pro/SKILL.md +105 -0
  55. package/plugin/skills/wordpress-pro/references/gutenberg-blocks.md +870 -0
  56. package/plugin/skills/wordpress-pro/references/hooks-filters.md +845 -0
  57. package/plugin/skills/wordpress-pro/references/performance-security.md +1012 -0
  58. package/plugin/skills/wordpress-pro/references/plugin-architecture.md +1041 -0
  59. package/plugin/skills/wordpress-pro/references/theme-development.md +858 -0
  60. package/plugin/sops/SOP-Sonic 777/authority-site-sop.html +1100 -0
  61. package/plugin/sops/SOP-WORDPRESS-330-PAGE-SITES.md +926 -0
  62. package/scripts/postinstall.js +109 -0
@@ -0,0 +1,1041 @@
1
+ # Plugin Architecture
2
+
3
+ ---
4
+
5
+ ## Plugin Structure
6
+
7
+ ### Minimal Plugin Structure
8
+
9
+ ```
10
+ plugin-name/
11
+ ├── plugin-name.php # Main plugin file with header
12
+ ├── uninstall.php # Cleanup on uninstall
13
+ ├── includes/
14
+ │ ├── class-plugin-name.php
15
+ │ ├── class-activator.php
16
+ │ ├── class-deactivator.php
17
+ │ └── class-loader.php
18
+ ├── admin/
19
+ │ ├── class-admin.php
20
+ │ ├── css/
21
+ │ └── js/
22
+ ├── public/
23
+ │ ├── class-public.php
24
+ │ ├── css/
25
+ │ └── js/
26
+ ├── languages/
27
+ │ └── plugin-name.pot
28
+ └── README.txt
29
+ ```
30
+
31
+ ### Full Plugin Structure (Enterprise)
32
+
33
+ ```
34
+ plugin-name/
35
+ ├── plugin-name.php
36
+ ├── uninstall.php
37
+ ├── composer.json
38
+ ├── phpcs.xml.dist
39
+ ├── phpunit.xml.dist
40
+ ├── includes/
41
+ │ ├── class-plugin.php # Main plugin class
42
+ │ ├── class-activator.php # Activation logic
43
+ │ ├── class-deactivator.php # Deactivation logic
44
+ │ ├── class-loader.php # Hook loader
45
+ │ ├── class-i18n.php # Internationalization
46
+ │ ├── Traits/
47
+ │ │ └── Singleton.php
48
+ │ ├── Interfaces/
49
+ │ │ ├── Registrable.php
50
+ │ │ └── Hookable.php
51
+ │ ├── Services/
52
+ │ │ └── class-api-service.php
53
+ │ └── Repositories/
54
+ │ └── class-data-repository.php
55
+ ├── admin/
56
+ │ ├── class-admin.php
57
+ │ ├── class-settings.php
58
+ │ ├── partials/
59
+ │ │ └── settings-page.php
60
+ │ ├── css/
61
+ │ └── js/
62
+ ├── public/
63
+ │ ├── class-frontend.php
64
+ │ ├── partials/
65
+ │ ├── css/
66
+ │ └── js/
67
+ ├── blocks/
68
+ │ └── custom-block/
69
+ ├── templates/
70
+ │ └── single-custom-type.php
71
+ ├── languages/
72
+ ├── tests/
73
+ │ ├── bootstrap.php
74
+ │ └── unit/
75
+ └── vendor/
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Main Plugin File
81
+
82
+ ### Plugin Header
83
+
84
+ ```php
85
+ <?php
86
+ /**
87
+ * Plugin Name: Plugin Name
88
+ * Plugin URI: https://example.com/plugin-name
89
+ * Description: A brief description of what this plugin does.
90
+ * Version: 1.0.0
91
+ * Requires at least: 6.4
92
+ * Requires PHP: 8.1
93
+ * Author: Author Name
94
+ * Author URI: https://example.com
95
+ * License: GPL v2 or later
96
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
97
+ * Text Domain: plugin-name
98
+ * Domain Path: /languages
99
+ * Update URI: https://example.com/plugin-name
100
+ *
101
+ * @package PluginName
102
+ */
103
+
104
+ declare(strict_types=1);
105
+
106
+ namespace PluginName;
107
+
108
+ // Prevent direct access
109
+ defined('ABSPATH') || exit;
110
+
111
+ // Plugin constants
112
+ define('PLUGIN_NAME_VERSION', '1.0.0');
113
+ define('PLUGIN_NAME_FILE', __FILE__);
114
+ define('PLUGIN_NAME_PATH', plugin_dir_path(__FILE__));
115
+ define('PLUGIN_NAME_URL', plugin_dir_url(__FILE__));
116
+ define('PLUGIN_NAME_BASENAME', plugin_basename(__FILE__));
117
+
118
+ // Autoloader
119
+ if (file_exists(PLUGIN_NAME_PATH . 'vendor/autoload.php')) {
120
+ require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
121
+ }
122
+
123
+ // Manual includes if no autoloader
124
+ require_once PLUGIN_NAME_PATH . 'includes/class-plugin.php';
125
+ require_once PLUGIN_NAME_PATH . 'includes/class-activator.php';
126
+ require_once PLUGIN_NAME_PATH . 'includes/class-deactivator.php';
127
+
128
+ /**
129
+ * Plugin activation hook
130
+ */
131
+ function activate(): void {
132
+ Activator::activate();
133
+ }
134
+ register_activation_hook(__FILE__, __NAMESPACE__ . '\\activate');
135
+
136
+ /**
137
+ * Plugin deactivation hook
138
+ */
139
+ function deactivate(): void {
140
+ Deactivator::deactivate();
141
+ }
142
+ register_deactivation_hook(__FILE__, __NAMESPACE__ . '\\deactivate');
143
+
144
+ /**
145
+ * Initialize the plugin
146
+ */
147
+ function init(): void {
148
+ $plugin = new Plugin();
149
+ $plugin->run();
150
+ }
151
+ add_action('plugins_loaded', __NAMESPACE__ . '\\init');
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Activation & Deactivation
157
+
158
+ ### Activator Class
159
+
160
+ ```php
161
+ <?php
162
+ declare(strict_types=1);
163
+
164
+ namespace PluginName;
165
+
166
+ /**
167
+ * Fired during plugin activation
168
+ */
169
+ class Activator {
170
+
171
+ /**
172
+ * Activation tasks
173
+ */
174
+ public static function activate(): void {
175
+ // Check requirements
176
+ self::check_requirements();
177
+
178
+ // Create database tables
179
+ self::create_tables();
180
+
181
+ // Set default options
182
+ self::set_default_options();
183
+
184
+ // Schedule cron events
185
+ self::schedule_events();
186
+
187
+ // Add capabilities
188
+ self::add_capabilities();
189
+
190
+ // Flush rewrite rules (if registering CPT/taxonomy)
191
+ flush_rewrite_rules();
192
+
193
+ // Set activation flag for welcome notice
194
+ set_transient('plugin_name_activated', true, 30);
195
+ }
196
+
197
+ /**
198
+ * Check system requirements
199
+ */
200
+ private static function check_requirements(): void {
201
+ if (version_compare(PHP_VERSION, '8.1', '<')) {
202
+ deactivate_plugins(PLUGIN_NAME_BASENAME);
203
+ wp_die(
204
+ esc_html__('This plugin requires PHP 8.1 or higher.', 'plugin-name'),
205
+ 'Plugin Activation Error',
206
+ ['back_link' => true]
207
+ );
208
+ }
209
+
210
+ global $wp_version;
211
+ if (version_compare($wp_version, '6.4', '<')) {
212
+ deactivate_plugins(PLUGIN_NAME_BASENAME);
213
+ wp_die(
214
+ esc_html__('This plugin requires WordPress 6.4 or higher.', 'plugin-name'),
215
+ 'Plugin Activation Error',
216
+ ['back_link' => true]
217
+ );
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Create custom database tables
223
+ */
224
+ private static function create_tables(): void {
225
+ global $wpdb;
226
+
227
+ $charset_collate = $wpdb->get_charset_collate();
228
+ $table_name = $wpdb->prefix . 'plugin_name_data';
229
+
230
+ $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
231
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
232
+ user_id bigint(20) unsigned NOT NULL DEFAULT 0,
233
+ data_key varchar(191) NOT NULL,
234
+ data_value longtext,
235
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
236
+ updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
237
+ PRIMARY KEY (id),
238
+ KEY user_id (user_id),
239
+ KEY data_key (data_key)
240
+ ) {$charset_collate};";
241
+
242
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
243
+ dbDelta($sql);
244
+
245
+ // Store DB version
246
+ update_option('plugin_name_db_version', PLUGIN_NAME_VERSION);
247
+ }
248
+
249
+ /**
250
+ * Set default options
251
+ */
252
+ private static function set_default_options(): void {
253
+ $defaults = [
254
+ 'enabled' => true,
255
+ 'api_key' => '',
256
+ 'cache_duration' => 3600,
257
+ 'items_per_page' => 10,
258
+ 'allowed_roles' => ['administrator', 'editor'],
259
+ ];
260
+
261
+ if (get_option('plugin_name_settings') === false) {
262
+ add_option('plugin_name_settings', $defaults);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Schedule cron events
268
+ */
269
+ private static function schedule_events(): void {
270
+ if (!wp_next_scheduled('plugin_name_daily_cleanup')) {
271
+ wp_schedule_event(time(), 'daily', 'plugin_name_daily_cleanup');
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Add custom capabilities
277
+ */
278
+ private static function add_capabilities(): void {
279
+ $admin = get_role('administrator');
280
+ if ($admin) {
281
+ $admin->add_cap('manage_plugin_name');
282
+ $admin->add_cap('edit_plugin_name_data');
283
+ }
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### Deactivator Class
289
+
290
+ ```php
291
+ <?php
292
+ declare(strict_types=1);
293
+
294
+ namespace PluginName;
295
+
296
+ /**
297
+ * Fired during plugin deactivation
298
+ */
299
+ class Deactivator {
300
+
301
+ /**
302
+ * Deactivation tasks
303
+ */
304
+ public static function deactivate(): void {
305
+ // Clear scheduled events
306
+ self::clear_scheduled_events();
307
+
308
+ // Clear transients
309
+ self::clear_transients();
310
+
311
+ // Flush rewrite rules
312
+ flush_rewrite_rules();
313
+
314
+ // Note: Do NOT delete options or tables here
315
+ // That should only happen in uninstall.php
316
+ }
317
+
318
+ /**
319
+ * Clear all scheduled cron events
320
+ */
321
+ private static function clear_scheduled_events(): void {
322
+ $timestamp = wp_next_scheduled('plugin_name_daily_cleanup');
323
+ if ($timestamp) {
324
+ wp_unschedule_event($timestamp, 'plugin_name_daily_cleanup');
325
+ }
326
+
327
+ // Clear all instances of our events
328
+ wp_clear_scheduled_hook('plugin_name_daily_cleanup');
329
+ }
330
+
331
+ /**
332
+ * Clear transients
333
+ */
334
+ private static function clear_transients(): void {
335
+ global $wpdb;
336
+
337
+ // Clear specific transients
338
+ delete_transient('plugin_name_cache');
339
+ delete_transient('plugin_name_activated');
340
+
341
+ // Clear all plugin transients (use with caution)
342
+ $wpdb->query(
343
+ $wpdb->prepare(
344
+ "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
345
+ '_transient_plugin_name_%',
346
+ '_transient_timeout_plugin_name_%'
347
+ )
348
+ );
349
+ }
350
+ }
351
+ ```
352
+
353
+ ### uninstall.php
354
+
355
+ ```php
356
+ <?php
357
+ /**
358
+ * Uninstall script - runs when plugin is deleted
359
+ *
360
+ * @package PluginName
361
+ */
362
+
363
+ // If uninstall not called from WordPress, exit
364
+ if (!defined('WP_UNINSTALL_PLUGIN')) {
365
+ exit;
366
+ }
367
+
368
+ // Check if we should preserve data
369
+ $settings = get_option('plugin_name_settings', []);
370
+ $preserve_data = $settings['preserve_data_on_uninstall'] ?? false;
371
+
372
+ if (!$preserve_data) {
373
+ global $wpdb;
374
+
375
+ // Delete options
376
+ delete_option('plugin_name_settings');
377
+ delete_option('plugin_name_db_version');
378
+
379
+ // Delete user meta
380
+ $wpdb->query("DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'plugin_name_%'");
381
+
382
+ // Delete post meta
383
+ $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_plugin_name_%'");
384
+
385
+ // Delete custom tables
386
+ $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}plugin_name_data");
387
+
388
+ // Delete transients
389
+ $wpdb->query(
390
+ $wpdb->prepare(
391
+ "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
392
+ '_transient_plugin_name_%',
393
+ '_transient_timeout_plugin_name_%'
394
+ )
395
+ );
396
+
397
+ // Remove capabilities
398
+ $roles = ['administrator', 'editor'];
399
+ foreach ($roles as $role_name) {
400
+ $role = get_role($role_name);
401
+ if ($role) {
402
+ $role->remove_cap('manage_plugin_name');
403
+ $role->remove_cap('edit_plugin_name_data');
404
+ }
405
+ }
406
+
407
+ // Clear any cached data
408
+ wp_cache_flush();
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Settings API
415
+
416
+ ### Settings Class
417
+
418
+ ```php
419
+ <?php
420
+ declare(strict_types=1);
421
+
422
+ namespace PluginName\Admin;
423
+
424
+ /**
425
+ * Plugin settings management
426
+ */
427
+ class Settings {
428
+
429
+ private const OPTION_GROUP = 'plugin_name_settings';
430
+ private const OPTION_NAME = 'plugin_name_settings';
431
+ private const PAGE_SLUG = 'plugin-name-settings';
432
+
433
+ /**
434
+ * Initialize settings
435
+ */
436
+ public function init(): void {
437
+ add_action('admin_menu', [$this, 'add_menu_page']);
438
+ add_action('admin_init', [$this, 'register_settings']);
439
+ }
440
+
441
+ /**
442
+ * Add admin menu page
443
+ */
444
+ public function add_menu_page(): void {
445
+ add_options_page(
446
+ __('Plugin Name Settings', 'plugin-name'),
447
+ __('Plugin Name', 'plugin-name'),
448
+ 'manage_options',
449
+ self::PAGE_SLUG,
450
+ [$this, 'render_settings_page']
451
+ );
452
+ }
453
+
454
+ /**
455
+ * Register all settings
456
+ */
457
+ public function register_settings(): void {
458
+ register_setting(
459
+ self::OPTION_GROUP,
460
+ self::OPTION_NAME,
461
+ [
462
+ 'type' => 'array',
463
+ 'sanitize_callback' => [$this, 'sanitize_settings'],
464
+ 'default' => $this->get_defaults(),
465
+ ]
466
+ );
467
+
468
+ // General section
469
+ add_settings_section(
470
+ 'plugin_name_general',
471
+ __('General Settings', 'plugin-name'),
472
+ [$this, 'render_general_section'],
473
+ self::PAGE_SLUG
474
+ );
475
+
476
+ // API section
477
+ add_settings_section(
478
+ 'plugin_name_api',
479
+ __('API Settings', 'plugin-name'),
480
+ [$this, 'render_api_section'],
481
+ self::PAGE_SLUG
482
+ );
483
+
484
+ // Fields
485
+ $this->add_fields();
486
+ }
487
+
488
+ /**
489
+ * Add settings fields
490
+ */
491
+ private function add_fields(): void {
492
+ // Enable field
493
+ add_settings_field(
494
+ 'enabled',
495
+ __('Enable Plugin', 'plugin-name'),
496
+ [$this, 'render_checkbox_field'],
497
+ self::PAGE_SLUG,
498
+ 'plugin_name_general',
499
+ [
500
+ 'label_for' => 'enabled',
501
+ 'description' => __('Enable or disable the plugin functionality.', 'plugin-name'),
502
+ ]
503
+ );
504
+
505
+ // Items per page
506
+ add_settings_field(
507
+ 'items_per_page',
508
+ __('Items Per Page', 'plugin-name'),
509
+ [$this, 'render_number_field'],
510
+ self::PAGE_SLUG,
511
+ 'plugin_name_general',
512
+ [
513
+ 'label_for' => 'items_per_page',
514
+ 'min' => 1,
515
+ 'max' => 100,
516
+ 'description' => __('Number of items to display per page.', 'plugin-name'),
517
+ ]
518
+ );
519
+
520
+ // API Key
521
+ add_settings_field(
522
+ 'api_key',
523
+ __('API Key', 'plugin-name'),
524
+ [$this, 'render_text_field'],
525
+ self::PAGE_SLUG,
526
+ 'plugin_name_api',
527
+ [
528
+ 'label_for' => 'api_key',
529
+ 'type' => 'password',
530
+ 'description' => __('Enter your API key for external service.', 'plugin-name'),
531
+ ]
532
+ );
533
+
534
+ // Cache duration
535
+ add_settings_field(
536
+ 'cache_duration',
537
+ __('Cache Duration', 'plugin-name'),
538
+ [$this, 'render_select_field'],
539
+ self::PAGE_SLUG,
540
+ 'plugin_name_api',
541
+ [
542
+ 'label_for' => 'cache_duration',
543
+ 'options' => [
544
+ '900' => __('15 minutes', 'plugin-name'),
545
+ '1800' => __('30 minutes', 'plugin-name'),
546
+ '3600' => __('1 hour', 'plugin-name'),
547
+ '86400' => __('1 day', 'plugin-name'),
548
+ ],
549
+ 'description' => __('How long to cache API responses.', 'plugin-name'),
550
+ ]
551
+ );
552
+ }
553
+
554
+ /**
555
+ * Get default settings
556
+ */
557
+ private function get_defaults(): array {
558
+ return [
559
+ 'enabled' => true,
560
+ 'api_key' => '',
561
+ 'cache_duration' => 3600,
562
+ 'items_per_page' => 10,
563
+ ];
564
+ }
565
+
566
+ /**
567
+ * Sanitize settings
568
+ */
569
+ public function sanitize_settings(array $input): array {
570
+ $sanitized = [];
571
+
572
+ $sanitized['enabled'] = !empty($input['enabled']);
573
+
574
+ $sanitized['api_key'] = sanitize_text_field($input['api_key'] ?? '');
575
+
576
+ $sanitized['cache_duration'] = absint($input['cache_duration'] ?? 3600);
577
+ if (!in_array($sanitized['cache_duration'], [900, 1800, 3600, 86400], true)) {
578
+ $sanitized['cache_duration'] = 3600;
579
+ }
580
+
581
+ $sanitized['items_per_page'] = absint($input['items_per_page'] ?? 10);
582
+ $sanitized['items_per_page'] = max(1, min(100, $sanitized['items_per_page']));
583
+
584
+ return $sanitized;
585
+ }
586
+
587
+ /**
588
+ * Render settings page
589
+ */
590
+ public function render_settings_page(): void {
591
+ if (!current_user_can('manage_options')) {
592
+ return;
593
+ }
594
+
595
+ // Show success message
596
+ if (isset($_GET['settings-updated'])) {
597
+ add_settings_error(
598
+ self::OPTION_GROUP,
599
+ 'settings_updated',
600
+ __('Settings saved.', 'plugin-name'),
601
+ 'updated'
602
+ );
603
+ }
604
+ ?>
605
+ <div class="wrap">
606
+ <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
607
+
608
+ <?php settings_errors(self::OPTION_GROUP); ?>
609
+
610
+ <form action="options.php" method="post">
611
+ <?php
612
+ settings_fields(self::OPTION_GROUP);
613
+ do_settings_sections(self::PAGE_SLUG);
614
+ submit_button(__('Save Settings', 'plugin-name'));
615
+ ?>
616
+ </form>
617
+ </div>
618
+ <?php
619
+ }
620
+
621
+ /**
622
+ * Render general section description
623
+ */
624
+ public function render_general_section(): void {
625
+ echo '<p>' . esc_html__('Configure general plugin settings.', 'plugin-name') . '</p>';
626
+ }
627
+
628
+ /**
629
+ * Render API section description
630
+ */
631
+ public function render_api_section(): void {
632
+ echo '<p>' . esc_html__('Configure API connection settings.', 'plugin-name') . '</p>';
633
+ }
634
+
635
+ /**
636
+ * Render checkbox field
637
+ */
638
+ public function render_checkbox_field(array $args): void {
639
+ $options = get_option(self::OPTION_NAME, $this->get_defaults());
640
+ $value = $options[$args['label_for']] ?? false;
641
+ ?>
642
+ <input type="checkbox"
643
+ id="<?php echo esc_attr($args['label_for']); ?>"
644
+ name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
645
+ value="1"
646
+ <?php checked($value, true); ?>
647
+ />
648
+ <?php if (!empty($args['description'])): ?>
649
+ <p class="description"><?php echo esc_html($args['description']); ?></p>
650
+ <?php endif;
651
+ }
652
+
653
+ /**
654
+ * Render text field
655
+ */
656
+ public function render_text_field(array $args): void {
657
+ $options = get_option(self::OPTION_NAME, $this->get_defaults());
658
+ $value = $options[$args['label_for']] ?? '';
659
+ $type = $args['type'] ?? 'text';
660
+ ?>
661
+ <input type="<?php echo esc_attr($type); ?>"
662
+ id="<?php echo esc_attr($args['label_for']); ?>"
663
+ name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
664
+ value="<?php echo esc_attr($value); ?>"
665
+ class="regular-text"
666
+ />
667
+ <?php if (!empty($args['description'])): ?>
668
+ <p class="description"><?php echo esc_html($args['description']); ?></p>
669
+ <?php endif;
670
+ }
671
+
672
+ /**
673
+ * Render number field
674
+ */
675
+ public function render_number_field(array $args): void {
676
+ $options = get_option(self::OPTION_NAME, $this->get_defaults());
677
+ $value = $options[$args['label_for']] ?? 0;
678
+ ?>
679
+ <input type="number"
680
+ id="<?php echo esc_attr($args['label_for']); ?>"
681
+ name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
682
+ value="<?php echo esc_attr($value); ?>"
683
+ min="<?php echo esc_attr($args['min'] ?? 0); ?>"
684
+ max="<?php echo esc_attr($args['max'] ?? 100); ?>"
685
+ class="small-text"
686
+ />
687
+ <?php if (!empty($args['description'])): ?>
688
+ <p class="description"><?php echo esc_html($args['description']); ?></p>
689
+ <?php endif;
690
+ }
691
+
692
+ /**
693
+ * Render select field
694
+ */
695
+ public function render_select_field(array $args): void {
696
+ $options = get_option(self::OPTION_NAME, $this->get_defaults());
697
+ $value = $options[$args['label_for']] ?? '';
698
+ ?>
699
+ <select id="<?php echo esc_attr($args['label_for']); ?>"
700
+ name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>">
701
+ <?php foreach ($args['options'] as $key => $label): ?>
702
+ <option value="<?php echo esc_attr($key); ?>" <?php selected($value, $key); ?>>
703
+ <?php echo esc_html($label); ?>
704
+ </option>
705
+ <?php endforeach; ?>
706
+ </select>
707
+ <?php if (!empty($args['description'])): ?>
708
+ <p class="description"><?php echo esc_html($args['description']); ?></p>
709
+ <?php endif;
710
+ }
711
+ }
712
+ ```
713
+
714
+ ---
715
+
716
+ ## Custom Post Types & Taxonomies
717
+
718
+ ### Registering Custom Post Types
719
+
720
+ ```php
721
+ <?php
722
+ declare(strict_types=1);
723
+
724
+ namespace PluginName;
725
+
726
+ /**
727
+ * Register custom post types and taxonomies
728
+ */
729
+ class CustomPostTypes {
730
+
731
+ /**
732
+ * Initialize
733
+ */
734
+ public function init(): void {
735
+ add_action('init', [$this, 'register_post_types']);
736
+ add_action('init', [$this, 'register_taxonomies']);
737
+ }
738
+
739
+ /**
740
+ * Register custom post type
741
+ */
742
+ public function register_post_types(): void {
743
+ $labels = [
744
+ 'name' => _x('Products', 'Post Type General Name', 'plugin-name'),
745
+ 'singular_name' => _x('Product', 'Post Type Singular Name', 'plugin-name'),
746
+ 'menu_name' => __('Products', 'plugin-name'),
747
+ 'name_admin_bar' => __('Product', 'plugin-name'),
748
+ 'archives' => __('Product Archives', 'plugin-name'),
749
+ 'attributes' => __('Product Attributes', 'plugin-name'),
750
+ 'parent_item_colon' => __('Parent Product:', 'plugin-name'),
751
+ 'all_items' => __('All Products', 'plugin-name'),
752
+ 'add_new_item' => __('Add New Product', 'plugin-name'),
753
+ 'add_new' => __('Add New', 'plugin-name'),
754
+ 'new_item' => __('New Product', 'plugin-name'),
755
+ 'edit_item' => __('Edit Product', 'plugin-name'),
756
+ 'update_item' => __('Update Product', 'plugin-name'),
757
+ 'view_item' => __('View Product', 'plugin-name'),
758
+ 'view_items' => __('View Products', 'plugin-name'),
759
+ 'search_items' => __('Search Product', 'plugin-name'),
760
+ 'not_found' => __('Not found', 'plugin-name'),
761
+ 'not_found_in_trash' => __('Not found in Trash', 'plugin-name'),
762
+ 'featured_image' => __('Featured Image', 'plugin-name'),
763
+ 'set_featured_image' => __('Set featured image', 'plugin-name'),
764
+ 'remove_featured_image' => __('Remove featured image', 'plugin-name'),
765
+ 'use_featured_image' => __('Use as featured image', 'plugin-name'),
766
+ 'insert_into_item' => __('Insert into product', 'plugin-name'),
767
+ 'uploaded_to_this_item' => __('Uploaded to this product', 'plugin-name'),
768
+ 'items_list' => __('Products list', 'plugin-name'),
769
+ 'items_list_navigation' => __('Products list navigation', 'plugin-name'),
770
+ 'filter_items_list' => __('Filter products list', 'plugin-name'),
771
+ ];
772
+
773
+ $args = [
774
+ 'label' => __('Product', 'plugin-name'),
775
+ 'description' => __('Product custom post type', 'plugin-name'),
776
+ 'labels' => $labels,
777
+ 'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'revisions'],
778
+ 'taxonomies' => ['product_category', 'product_tag'],
779
+ 'hierarchical' => false,
780
+ 'public' => true,
781
+ 'show_ui' => true,
782
+ 'show_in_menu' => true,
783
+ 'menu_position' => 20,
784
+ 'menu_icon' => 'dashicons-products',
785
+ 'show_in_admin_bar' => true,
786
+ 'show_in_nav_menus' => true,
787
+ 'can_export' => true,
788
+ 'has_archive' => 'products',
789
+ 'exclude_from_search' => false,
790
+ 'publicly_queryable' => true,
791
+ 'capability_type' => 'post',
792
+ 'show_in_rest' => true, // Enable Gutenberg
793
+ 'rest_base' => 'products',
794
+ 'rewrite' => [
795
+ 'slug' => 'product',
796
+ 'with_front' => false,
797
+ ],
798
+ 'template' => [
799
+ ['core/image', ['align' => 'wide']],
800
+ ['core/paragraph', ['placeholder' => 'Product description...']],
801
+ ],
802
+ 'template_lock' => false, // 'all', 'insert', false
803
+ ];
804
+
805
+ register_post_type('product', $args);
806
+ }
807
+
808
+ /**
809
+ * Register custom taxonomies
810
+ */
811
+ public function register_taxonomies(): void {
812
+ // Product Category (hierarchical like categories)
813
+ $category_labels = [
814
+ 'name' => _x('Product Categories', 'taxonomy general name', 'plugin-name'),
815
+ 'singular_name' => _x('Product Category', 'taxonomy singular name', 'plugin-name'),
816
+ 'search_items' => __('Search Product Categories', 'plugin-name'),
817
+ 'all_items' => __('All Product Categories', 'plugin-name'),
818
+ 'parent_item' => __('Parent Product Category', 'plugin-name'),
819
+ 'parent_item_colon' => __('Parent Product Category:', 'plugin-name'),
820
+ 'edit_item' => __('Edit Product Category', 'plugin-name'),
821
+ 'update_item' => __('Update Product Category', 'plugin-name'),
822
+ 'add_new_item' => __('Add New Product Category', 'plugin-name'),
823
+ 'new_item_name' => __('New Product Category Name', 'plugin-name'),
824
+ 'menu_name' => __('Categories', 'plugin-name'),
825
+ ];
826
+
827
+ register_taxonomy('product_category', ['product'], [
828
+ 'hierarchical' => true,
829
+ 'labels' => $category_labels,
830
+ 'show_ui' => true,
831
+ 'show_admin_column' => true,
832
+ 'query_var' => true,
833
+ 'show_in_rest' => true,
834
+ 'rewrite' => ['slug' => 'product-category'],
835
+ ]);
836
+
837
+ // Product Tags (non-hierarchical like tags)
838
+ $tag_labels = [
839
+ 'name' => _x('Product Tags', 'taxonomy general name', 'plugin-name'),
840
+ 'singular_name' => _x('Product Tag', 'taxonomy singular name', 'plugin-name'),
841
+ 'search_items' => __('Search Product Tags', 'plugin-name'),
842
+ 'popular_items' => __('Popular Product Tags', 'plugin-name'),
843
+ 'all_items' => __('All Product Tags', 'plugin-name'),
844
+ 'edit_item' => __('Edit Product Tag', 'plugin-name'),
845
+ 'update_item' => __('Update Product Tag', 'plugin-name'),
846
+ 'add_new_item' => __('Add New Product Tag', 'plugin-name'),
847
+ 'new_item_name' => __('New Product Tag Name', 'plugin-name'),
848
+ 'separate_items_with_commas' => __('Separate tags with commas', 'plugin-name'),
849
+ 'add_or_remove_items' => __('Add or remove tags', 'plugin-name'),
850
+ 'choose_from_most_used' => __('Choose from the most used tags', 'plugin-name'),
851
+ 'not_found' => __('No tags found.', 'plugin-name'),
852
+ 'menu_name' => __('Tags', 'plugin-name'),
853
+ ];
854
+
855
+ register_taxonomy('product_tag', ['product'], [
856
+ 'hierarchical' => false,
857
+ 'labels' => $tag_labels,
858
+ 'show_ui' => true,
859
+ 'show_admin_column' => true,
860
+ 'query_var' => true,
861
+ 'show_in_rest' => true,
862
+ 'rewrite' => ['slug' => 'product-tag'],
863
+ ]);
864
+ }
865
+ }
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Plugin Updates
871
+
872
+ ### Self-Hosted Update Checker
873
+
874
+ ```php
875
+ <?php
876
+ declare(strict_types=1);
877
+
878
+ namespace PluginName;
879
+
880
+ /**
881
+ * Handle plugin updates from custom server
882
+ */
883
+ class UpdateChecker {
884
+
885
+ private string $plugin_slug;
886
+ private string $update_url;
887
+ private string $plugin_file;
888
+
889
+ public function __construct() {
890
+ $this->plugin_slug = 'plugin-name';
891
+ $this->plugin_file = PLUGIN_NAME_BASENAME;
892
+ $this->update_url = 'https://example.com/api/plugin-updates/';
893
+ }
894
+
895
+ /**
896
+ * Initialize update checker
897
+ */
898
+ public function init(): void {
899
+ add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_update']);
900
+ add_filter('plugins_api', [$this, 'plugin_info'], 20, 3);
901
+ add_action('in_plugin_update_message-' . $this->plugin_file, [$this, 'update_message'], 10, 2);
902
+ }
903
+
904
+ /**
905
+ * Check for plugin updates
906
+ */
907
+ public function check_for_update(object $transient): object {
908
+ if (empty($transient->checked)) {
909
+ return $transient;
910
+ }
911
+
912
+ $remote = $this->get_remote_info();
913
+
914
+ if (
915
+ $remote &&
916
+ version_compare(PLUGIN_NAME_VERSION, $remote->version, '<') &&
917
+ version_compare($remote->requires, get_bloginfo('version'), '<=') &&
918
+ version_compare($remote->requires_php, PHP_VERSION, '<=')
919
+ ) {
920
+ $transient->response[$this->plugin_file] = (object) [
921
+ 'slug' => $this->plugin_slug,
922
+ 'plugin' => $this->plugin_file,
923
+ 'new_version' => $remote->version,
924
+ 'url' => $remote->url,
925
+ 'package' => $remote->package,
926
+ 'icons' => (array) ($remote->icons ?? []),
927
+ 'banners' => (array) ($remote->banners ?? []),
928
+ 'tested' => $remote->tested ?? '',
929
+ 'requires' => $remote->requires ?? '',
930
+ ];
931
+ }
932
+
933
+ return $transient;
934
+ }
935
+
936
+ /**
937
+ * Plugin information for update screen
938
+ */
939
+ public function plugin_info(mixed $result, string $action, object $args): mixed {
940
+ if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
941
+ return $result;
942
+ }
943
+
944
+ $remote = $this->get_remote_info();
945
+
946
+ if (!$remote) {
947
+ return $result;
948
+ }
949
+
950
+ return (object) [
951
+ 'name' => $remote->name,
952
+ 'slug' => $this->plugin_slug,
953
+ 'version' => $remote->version,
954
+ 'author' => $remote->author,
955
+ 'author_profile' => $remote->author_profile ?? '',
956
+ 'requires' => $remote->requires,
957
+ 'tested' => $remote->tested,
958
+ 'requires_php' => $remote->requires_php,
959
+ 'sections' => (array) $remote->sections,
960
+ 'download_link' => $remote->package,
961
+ 'banners' => (array) ($remote->banners ?? []),
962
+ 'icons' => (array) ($remote->icons ?? []),
963
+ 'last_updated' => $remote->last_updated ?? '',
964
+ 'homepage' => $remote->url ?? '',
965
+ ];
966
+ }
967
+
968
+ /**
969
+ * Custom update message
970
+ */
971
+ public function update_message(array $plugin_data, object $response): void {
972
+ if (!empty($response->upgrade_notice)) {
973
+ printf(
974
+ '<br /><strong>%s</strong>: %s',
975
+ esc_html__('Upgrade Notice', 'plugin-name'),
976
+ esc_html($response->upgrade_notice)
977
+ );
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Get remote plugin information
983
+ */
984
+ private function get_remote_info(): ?object {
985
+ $transient_key = 'plugin_name_update_info';
986
+ $remote = get_transient($transient_key);
987
+
988
+ if ($remote !== false) {
989
+ return $remote === 'error' ? null : $remote;
990
+ }
991
+
992
+ $response = wp_remote_get($this->update_url . 'info.json', [
993
+ 'timeout' => 10,
994
+ 'headers' => [
995
+ 'Accept' => 'application/json',
996
+ ],
997
+ ]);
998
+
999
+ if (
1000
+ is_wp_error($response) ||
1001
+ wp_remote_retrieve_response_code($response) !== 200 ||
1002
+ empty(wp_remote_retrieve_body($response))
1003
+ ) {
1004
+ set_transient($transient_key, 'error', HOUR_IN_SECONDS);
1005
+ return null;
1006
+ }
1007
+
1008
+ $remote = json_decode(wp_remote_retrieve_body($response));
1009
+ set_transient($transient_key, $remote, 12 * HOUR_IN_SECONDS);
1010
+
1011
+ return $remote;
1012
+ }
1013
+ }
1014
+ ```
1015
+
1016
+ ---
1017
+
1018
+ ## Best Practices
1019
+
1020
+ ### Do
1021
+
1022
+ - Use namespaces to avoid function/class name collisions
1023
+ - Follow WordPress Coding Standards (WPCS)
1024
+ - Include proper plugin headers with all required fields
1025
+ - Implement proper activation/deactivation/uninstall hooks
1026
+ - Use the Settings API for options pages
1027
+ - Register CPTs with `show_in_rest` for Gutenberg support
1028
+ - Cache remote API responses with transients
1029
+ - Provide translation support with text domains
1030
+ - Include a proper uninstall.php for cleanup
1031
+
1032
+ ### Do Not
1033
+
1034
+ - Access the database directly without proper preparation
1035
+ - Store options without sanitization
1036
+ - Skip capability checks in admin functions
1037
+ - Leave orphaned data after uninstall
1038
+ - Hardcode plugin paths (use constants)
1039
+ - Register hooks in constructors (use init methods)
1040
+ - Modify core WordPress tables
1041
+ - Include heavy operations in activation hooks