How to Build a WordPress Plugin from Scratch: Part 1 – Plugin Skeleton
Welcome to the first part of our series on building a custom WordPress plugin from scratch. We’re going to walk through the process step-by-step using a real-world example: a simple but scalable photo gallery plugin. In this part, we’ll create the foundation — the plugin skeleton — with all the essential files and folders set up correctly.
Prefer watching over reading? You can check out the full tutorial on YouTube in the player below. Or if you’re more of an old-school type who likes a proper walkthrough in text — just scroll down. Everything’s laid out step by step, like back in the good days of real how-to blogs.
Table of Contents
- Why Start with a Skeleton?
- Directory Structure
- The Main Plugin File
- Core Module Placeholders
- Assets: CSS and JavaScript
- What’s Next
- FAQ
1. Why Start with a Skeleton?
Building a plugin without structure is like building a house without a blueprint. A clean skeleton:
- Gives you a predictable layout
- Makes it easier to separate logic (assets, templates, PHP classes)
- Scales better when features grow
Even for small projects, a well-structured foundation saves time and reduces bugs later on.
2. Directory Structure
Here’s the initial file and folder structure for our plugin oz-photo-gallery:
| Path | Description |
|---|---|
oz-photo-gallery/ | Main plugin folder |
assets/css/gallery.css | Compiled styles for frontend gallery |
assets/js/gallery.js | Vanilla JS file for interactivity |
includes/ | All PHP logic and classes |
templates/gallery-template.php | Gallery HTML template |
uninstall.php | Cleanup logic on plugin uninstall |
readme.txt | Plugin description for wp.org |
3. The Main Plugin File
The entry point for any plugin is its main PHP file. Ours is called oz-photo-gallery.php. Here’s the boilerplate:
<?php
/**
* Plugin Name: OZ Photo Gallery
* Plugin URI: https://ozwebexpert.com/
* Description: Lightweight custom photo gallery plugin with albums and photo pages. Built for Australian photographers and web creators.
* Version: 1.0.0
* Author: Grigory Frolov
* Author URI: https://ozwebexpert.com/
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: oz-photo-gallery
* Domain Path: /languages
*/
defined('ABSPATH') || exit;
// Define plugin path and URL constants
define('OZPG_PATH', plugin_dir_path(__FILE__));
define('OZPG_URL', plugin_dir_url(__FILE__));
// Load helpers and classes
require_once OZPG_PATH . 'includes/helpers.php';
require_once OZPG_PATH . 'includes/class-ozpg-post-types.php';
require_once OZPG_PATH . 'includes/class-ozpg-assets.php';
// Register plugin activation hook (flush rewrite rules)
register_activation_hook(__FILE__, function () {
if (function_exists('flush_rewrite_rules')) {
flush_rewrite_rules();
}
});
// Initialize after all plugins are loaded
add_action('plugins_loaded', function () {
if (class_exists('OZPG_Post_Types')) {
OZPG_Post_Types::init();
}
if (class_exists('OZPG_Assets')) {
OZPG_Assets::init();
}
});
This file loads the necessary components and ensures everything runs via hooks.
4. Core Module Placeholders
We’ll be splitting the logic into individual classes. Each file goes into the includes/ folder:
- Post Types:
class-ozpg-post-types.php– for Albums and Photos - Assets:
class-ozpg-assets.php– for enqueuing CSS and JS - Helpers: helpers.php – for additional logic
includes/class-ozpg-post-types.php
<?php
defined('ABSPATH') || exit;
// Register custom post types: Album and Photo
class OZPG_Post_Types {
public static function init() {
add_action('init', [self::class, 'register_post_types']);
}
public static function register_post_types() {
//future function
}
}
includes/class-ozpg-assets.php
<?php
defined('ABSPATH') || exit;
// Enqueues CSS and JS for the gallery plugin
class OZPG_Assets {
public static function init() {
add_action('wp_enqueue_scripts', [self::class, 'enqueue_public_assets']);
}
public static function enqueue_public_assets() {
$plugin_url = plugin_dir_url(__DIR__); // points to /oz-photo-gallery/
$plugin_path = plugin_dir_path(__DIR__);
wp_enqueue_style(
'ozpg-gallery-style',
$plugin_url . 'assets/css/gallery.css',
[],
file_exists($plugin_path . 'gallery.css') ? filemtime($plugin_path . 'gallery.css') : false
);
wp_enqueue_script(
'ozpg-gallery-script',
$plugin_url . 'assets/js/gallery.js',
[],
file_exists($plugin_path . 'gallery.js') ? filemtime($plugin_path . 'gallery.js') : false,
true
);
}
}
includes/helpers.php
<?php
defined('ABSPATH') || exit;
/**
* Returns the base slug for the gallery.
*/
function oz_get_gallery_slug() {
return 'gallery';
}
5. Assets: CSS and JavaScript
We’re keeping things lean — no jQuery, no frameworks. Just a bit of CSS Grid and plain JavaScript:
assets/css/gallery.css
.ozpg-gallery {
display: grid;
gap: 16px;
}
assets/js/gallery.js
// OZ Photo Gallery — Vanilla JS interactivity
document.addEventListener('DOMContentLoaded', function () {
// Placeholder: Add gallery interactions here
console.log('OZ Photo Gallery script loaded.');
});
Both files are registered using wp_enqueue_style and wp_enqueue_script inside our asset handler class.
templates/gallery-template.php
<?php
// Gallery template output
// Can be overridden via theme if needed
defined('ABSPATH') || exit;
echo '<div class="ozpg-gallery-template">';
echo '<p>This is the default gallery template.</p>';
echo '</div>';
uninstall.php
<?php
// Clean up custom post types and options if needed
defined('WP_UNINSTALL_PLUGIN') || exit;
// Example: delete_option('ozpg_settings');
readme.txt
=== OZ Photo Gallery ===
Contributors: ozwebexpert
Tags: photo, gallery, albums, images
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
A lightweight, developer-friendly photo gallery plugin made for the Australian creative community.
6. What’s Next?
Now that we’ve got a clean structure, we can safely move forward with the next parts:
- Register custom post types: albums and photos
- Build the front-end template for galleries
- Add photo upload logic and thumbnail previews
In Part 2, we’ll focus on creating our custom post types with meaningful labels and admin UI integration.