Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.
Lessons
A plugin that grows beyond a few functions needs a clear folder structure, a set of path and URL constants built from WordPress helpers, and a unique prefix on every name so it can never collide with another plugin.
gdc-books/
├── gdc-books.php Main file: header, constants, loader
├── uninstall.php Runs when the plugin is deleted
├── readme.txt For WordPress.org
├── includes/ PHP that runs everywhere
│ ├── post-types.php
│ ├── shortcodes.php
│ └── rest-api.php
├── admin/ PHP that only runs in the dashboard
│ ├── settings-page.php
│ └── meta-boxes.php
├── public/ Front-end-only PHP
├── assets/
│ ├── css/
│ └── js/
├── templates/ HTML a theme may override
├── blocks/ Block source code
└── languages/Define these once in the main file. Every other file uses them instead of guessing at paths:
<?php
define( 'GDC_BOOKS_VERSION', '1.0.0' );
define( 'GDC_BOOKS_FILE', __FILE__ );
define( 'GDC_BOOKS_PATH', plugin_dir_path( __FILE__ ) ); // /var/www/.../plugins/gdc-books/
define( 'GDC_BOOKS_URL', plugin_dir_url( __FILE__ ) ); // https://site.com/wp-content/plugins/gdc-books/
define( 'GDC_BOOKS_BASENAME', plugin_basename( __FILE__ ) ); // gdc-books/gdc-books.php
require_once GDC_BOOKS_PATH . 'includes/post-types.php';
require_once GDC_BOOKS_PATH . 'includes/shortcodes.php';
if ( is_admin() ) {
require_once GDC_BOOKS_PATH . 'admin/settings-page.php';
require_once GDC_BOOKS_PATH . 'admin/meta-boxes.php';
}| Helper | Returns | Use for |
|---|---|---|
plugin_dir_path( __FILE__ ) | Server file path with trailing slash | require and reading files |
plugin_dir_url( __FILE__ ) | Public URL with trailing slash | Enqueuing CSS, JS and images |
plugins_url( 'assets/js/app.js', __FILE__ ) | URL of one file | A single asset |
plugin_basename( __FILE__ ) | gdc-books/gdc-books.php | Hooks that need the plugin's identifier |
Watch out: Never hard-code/wp-content/plugins/. Sites can move the plugins folder withWP_PLUGIN_DIR, and many hosts do. The helpers always return the real location.
Your plugin shares one PHP global space with WordPress and every other plugin. A function called register_books() collides with the first other plugin that picked the same name, and PHP stops with a fatal error. Prefix every global name with something unique to your plugin:
| Kind | Prefixed |
|---|---|
| Functions | gdc_books_register_post_type() |
| Constants | GDC_BOOKS_VERSION |
| Option names | gdc_books_settings |
| Post meta keys | _gdc_books_isbn |
| Hook names you create | gdc_books_after_save |
| Script and style handles | gdc-books-admin |
| Classes | Use a namespace: GDC\Books\Plugin (lesson 25) |
Tip: Pick a prefix of at least four characters. Two-letter prefixes likewp_ormy_are exactly the ones everybody else picked too.
The is_admin() check above means the settings page and meta box code is never even read on front-end requests. Visitors get a faster site, and admin-only code has one less way to be reached.
GDC_BOOKS_PATH.GDC_BOOKS_URL once to debug.log with error_log() and check it is a full URL.gdc_books_ prefix.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.