Architecture Overview
WP Stories follows WordPress coding standards and MVC pattern:
┌─────────────────────────────────────────────┐
│ WordPress Core │
├─────────────────────────────────────────────┤
│ WP Stories Plugin │
├──────────┬───────────┬──────────┬──────────┤
│ Admin │ Public │ Includes │ Widgets │
├──────────┼───────────┼──────────┼──────────┤
│ CSS │ JS │ Views │ APIs │
└──────────┴───────────┴──────────┴──────────┘
Design Patterns
- Singleton: Main plugin class
- Factory: Story creation
- Observer: Hook system
- MVC: Separation of concerns
Plugin Structure
wp-stories/
├── admin/ # Admin functionality
│ ├── class-wp-stories-admin.php
│ ├── css/ # Admin styles
│ ├── js/ # Admin scripts
│ ├── inc/ # Settings tabs
│ └── wbcom/ # License & settings
├── includes/ # Core functionality
│ ├── class-wp-stories.php # Main plugin class
│ ├── class-wp-stories-activator.php
│ ├── class-wp-stories-deactivator.php
│ ├── class-wp-stories-i18n.php
│ ├── class-wp-stories-loader.php
│ ├── wp-stories-functions.php
│ └── codestar-framework/ # Settings framework
├── public/ # Frontend functionality
│ ├── class-wp-stories-public.php
│ ├── class-wp-stories-submit-user-stories.php
│ ├── css/ # Frontend styles
│ ├── js/ # Frontend scripts
│ ├── widgets/ # Widget classes
│ └── partials/ # View templates
├── languages/ # Translations
├── edd-license/ # License management
└── wp-stories.php # Main plugin file
Core Classes
Main Plugin Class
class Wp_Stories {
protected $loader; // Hook loader
protected $wp_stories; // Plugin slug
protected $version; // Plugin version
public function __construct() {
$this->load_dependencies();
$this->set_locale();
$this->define_admin_hooks();
$this->define_public_hooks();
}
public function run() {
$this->loader->run();
}
}
Admin Class
class Wp_Stories_Admin {
private $wp_stories;
private $version;
// Register custom post type
public function wp_stories_add_custom_post_type() {
register_post_type('wb-story', $args);
register_post_type('wb-story-box', $args);
}
// Enqueue admin scripts
public function enqueue_styles() {
wp_enqueue_style($this->wp_stories, ...);
}
}
Public Class
class Wp_Stories_Public {
// Handle frontend display
public function wp_stories_shortcode($atts) {
// Process shortcode attributes
// Return HTML output
}
// AJAX handlers
public function wp_stories_load_user_stories() {
// Load more stories via AJAX
}
}
Database Schema
Custom Tables
WP Stories uses WordPress post meta for storage:
Post Type: wb-story
-- Stored in wp_posts table
post_type = 'wb-story'
post_status = 'publish'|'draft'|'private'
-- Meta data in wp_postmeta
meta_key: 'wb_story_items' -- Serialized story data
meta_key: 'wb_story_visibility' -- Privacy settings
meta_key: 'wb_story_views' -- View count
meta_key: 'wb_story_expires' -- Expiration timestamp
Post Type: wb-story-box
-- Story collections
post_type = 'wb-story-box'
-- Meta data
meta_key: 'wb-story-box-metabox' -- Box configuration
meta_key: 'wb_story_ids' -- Associated story IDs
User Meta
// Story preferences
get_user_meta($user_id, 'wp_stories_viewed', true);
get_user_meta($user_id, 'wp_stories_settings', true);
Hooks & Filters
Action Hooks
Story Creation
// Before story creation
do_action('wp_stories_before_create', $story_data, $user_id);
// After story creation
do_action('wp_stories_after_create', $story_id, $story_data);
// Story deletion
do_action('wp_stories_before_delete', $story_id);
do_action('wp_stories_after_delete', $story_id);
Display Hooks
// Before stories display
do_action('wp_stories_before_display', $stories);
// After stories display
do_action('wp_stories_after_display', $stories);
// Single story view
do_action('wp_stories_view', $story_id, $viewer_id);
Integration Hooks
// BuddyPress integration
do_action('wp_stories_bp_activity_posted', $activity_id, $story_id);
// PeepSo integration
do_action('wp_stories_peepso_activity_posted', $activity_id, $story_id);
Filter Hooks
Content Filters
// Filter story data before save
$story_data = apply_filters('wp_stories_before_save', $story_data);
// Filter story output
$html = apply_filters('wp_stories_output', $html, $story_id);
// Filter allowed file types
$types = apply_filters('wp_stories_allowed_types', array('jpg', 'png', 'mp4'));
Permission Filters
// Can user create story
$can_create = apply_filters('wp_stories_can_create', true, $user_id);
// Can user view story
$can_view = apply_filters('wp_stories_can_view', true, $story_id, $user_id);
// Can user delete story
$can_delete = apply_filters('wp_stories_can_delete', false, $story_id, $user_id);
Display Filters
// Filter story circle HTML
$circle_html = apply_filters('wp_stories_circle_html', $html, $story);
// Filter story viewer HTML
$viewer_html = apply_filters('wp_stories_viewer_html', $html, $story);
// Filter shortcode attributes
$atts = apply_filters('wp_stories_shortcode_atts', $atts);
PHP API
Core Functions
Story Management
/**
* Get stories
* @param int $box_id Story box ID
* @param bool $author Get author stories
* @param array $query_args WP_Query arguments
* @return array|WP_Error
*/
function get_wp_stories($box_id, $author = false, $query_args = array()) {
// Returns array of story objects
}
/**
* Create a new story
* @param array $data Story data
* @return int|WP_Error Story ID or error
*/
function wp_stories_create_story($data) {
$story_id = wp_insert_post(array(
'post_type' => 'wb-story',
'post_title' => $data['title'],
'post_status' => 'publish'
));
if ($story_id) {
update_post_meta($story_id, 'wb_story_items', $data['items']);
}
return $story_id;
}
/**
* Delete a story
* @param int $story_id
* @return bool
*/
function wp_stories_delete_story($story_id) {
return wp_delete_post($story_id, true);
}
User Functions
/**
* Get user avatar
* @param int $user_id
* @param int $size Avatar size
* @return string Avatar URL
*/
function get_wp_stories_user_avatar($user_id, $size = 100) {
// Returns avatar URL with integration support
}
/**
* Get user display name
* @param int $user_id
* @return string
*/
function get_wp_stories_user_name($user_id) {
// Returns user display name
}
/**
* Check if user can create stories
* @param int $user_id
* @return bool
*/
function wp_stories_user_can_create($user_id = null) {
if (!$user_id) {
$user_id = get_current_user_id();
}
$allowed_roles = get_option('wp_stories_allowed_roles', array());
$user = get_userdata($user_id);
return !empty(array_intersect($allowed_roles, $user->roles));
}
Display Functions
/**
* Display stories
* @param array $args Display arguments
* @return string HTML output
*/
function wp_stories_display($args = array()) {
$defaults = array(
'style' => 'circle',
'limit' => 10,
'columns' => 5
);
$args = wp_parse_args($args, $defaults);
// Generate and return HTML
}
/**
* Get story viewer HTML
* @param int $story_id
* @return string
*/
function wp_stories_get_viewer($story_id) {
// Returns viewer modal HTML
}
JavaScript API
Story Editor Bridge
// Global editor instance
window.WPStoriesEditorBridge = {
/**
* Open image editor
* @param {string} imageUrl - Image URL or base64
* @param {object} options - Editor options
*/
open: function(imageUrl, options) {
options = {
onSave: function(dataURL) {},
onCancel: function() {},
theme: 'light',
...options
};
// Initialize Vue editor
},
/**
* Close editor
*/
close: function() {
// Close modal
}
};
Story Viewer API
// Story viewer instance
window.WPStoriesViewer = {
/**
* Initialize viewer
* @param {string} selector - Container selector
* @param {object} options - Viewer options
*/
init: function(selector, options) {
// Initialize Zuck.js
},
/**
* Open specific story
* @param {string} storyId
*/
open: function(storyId) {
// Open story in viewer
},
/**
* Navigate stories
*/
next: function() {},
previous: function() {},
pause: function() {},
play: function() {}
};
jQuery Extensions
// jQuery plugin for story creation
$.fn.wpStoriesCreator = function(options) {
return this.each(function() {
// Initialize FilePond uploader
// Attach editor bridge
// Handle submissions
});
};
// Usage
$('#story-creator').wpStoriesCreator({
maxFiles: 10,
maxSize: '10MB',
onUpload: function(files) {},
onEdit: function(file) {},
onSubmit: function(data) {}
});
AJAX Handlers
// Load more stories
function wpStoriesLoadMore(page, callback) {
$.ajax({
url: wp_stories_ajax.ajax_url,
type: 'POST',
data: {
action: 'wp_stories_load_more',
page: page,
nonce: wp_stories_ajax.nonce
},
success: callback
});
}
// Submit story
function wpStoriesSubmit(data, callback) {
$.ajax({
url: wp_stories_ajax.ajax_url,
type: 'POST',
data: {
action: 'wp_stories_submit',
story_data: data,
nonce: wp_stories_ajax.nonce
},
success: callback
});
}
REST API
Endpoints
Get Stories
GET /wp-json/wp-stories/v1/stories
Parameters:
box_id– Story box IDuser_id– Filter by userlimit– Number of storiespage– Pagination
Response:
{
"stories": [
{
"id": 123,
"title": "My Story",
"author": 1,
"items": [...],
"created": "2024-01-01T00:00:00"
}
],
"total": 50,
"pages": 5
}
Create Story
POST /wp-json/wp-stories/v1/stories
Body:
{
"title": "New Story",
"items": [
{
"type": "image",
"src": "https://...",
"duration": 5
}
],
"visibility": "public"
}
Delete Story
DELETE /wp-json/wp-stories/v1/stories/{id}
Authentication
// Register REST routes
add_action('rest_api_init', function() {
register_rest_route('wp-stories/v1', '/stories', array(
'methods' => 'GET',
'callback' => 'wp_stories_rest_get_stories',
'permission_callback' => 'wp_stories_rest_permission'
));
});
// Permission callback
function wp_stories_rest_permission() {
return current_user_can('read');
}
Custom Post Types
Story Post Type
register_post_type('wb-story', array(
'labels' => array(
'name' => __('Stories', 'wp-stories'),
'singular_name' => __('Story', 'wp-stories')
),
'public' => true,
'has_archive' => true,
'supports' => array('title', 'editor', 'thumbnail', 'author'),
'menu_icon' => 'dashicons-format-image',
'rewrite' => array('slug' => 'stories'),
'capability_type' => 'post',
'map_meta_cap' => true
));
Story Box Post Type
register_post_type('wb-story-box', array(
'labels' => array(
'name' => __('Story Boxes', 'wp-stories'),
'singular_name' => __('Story Box', 'wp-stories')
),
'public' => false,
'show_ui' => true,
'supports' => array('title'),
'menu_icon' => 'dashicons-grid-view'
));
Templating System
Template Hierarchy
1. Theme: /wp-stories/single-story.php
2. Plugin: /public/partials/single-story.php
3. Default: WordPress single.php
Override Templates
Create in your theme:
/your-theme/
/wp-stories/
single-story.php # Single story
archive-stories.php # Stories archive
story-circle.php # Circle template
story-viewer.php # Viewer modal
Template Tags
// In template files
if (function_exists('wp_stories_display')) {
wp_stories_display(array(
'style' => 'circle',
'limit' => 10
));
}
// Get story data
$story = get_wp_story($story_id);
$items = get_wp_story_items($story_id);
// Display story viewer
wp_stories_viewer($story_id);
Custom Templates
// Register custom template
add_filter('wp_stories_templates', function($templates) {
$templates['custom'] = array(
'label' => 'Custom Style',
'template' => 'path/to/template.php'
);
return $templates;
});
Image Editor Integration
Vue Editor API
// Initialize editor
const editor = new WPStoriesVueEditor({
container: '#editor-container',
image: 'path/to/image.jpg',
options: {
theme: 'light',
locale: 'en',
cssMaxWidth: 700,
cssMaxHeight: 500
}
});
// Event handlers
editor.on('save', (dataURL) => {
// Handle saved image
});
editor.on('cancel', () => {
// Handle cancellation
});
// Methods
editor.loadImage(url);
editor.applyFilter('grayscale');
editor.addText('Hello World');
editor.destroy();
FilePond Integration
// Configure FilePond with editor
FilePond.registerPlugin(
FilePondPluginImageEdit,
FilePondPluginImagePreview,
FilePondPluginFileValidateType
);
const pond = FilePond.create(inputElement, {
imageEditEditor: {
open: (file, instructions) => {
WPStoriesEditorBridge.open(file, {
onSave: (output) => instructions.confirm(output),
onCancel: () => instructions.cancel()
});
}
}
});
Creating Extensions
Basic Extension Structure
/**
* Plugin Name: WP Stories Extension
* Description: Extends WP Stories functionality
*/
class WP_Stories_Extension {
public function __construct() {
add_action('wp_stories_init', array($this, 'init'));
}
public function init() {
// Add custom functionality
add_filter('wp_stories_allowed_types', array($this, 'add_types'));
add_action('wp_stories_after_create', array($this, 'after_create'), 10, 2);
}
public function add_types($types) {
$types[] = 'webp';
return $types;
}
public function after_create($story_id, $data) {
// Custom processing
}
}
new WP_Stories_Extension();
Adding Custom Filters
// Add Instagram-style filter
add_filter('wp_stories_image_filters', function($filters) {
$filters['instagram'] = array(
'label' => 'Instagram',
'css' => 'filter: contrast(1.1) brightness(1.1)',
'canvas' => array(
'brightness' => 10,
'contrast' => 10
)
);
return $filters;
});
Custom Story Types
// Register custom story type
add_filter('wp_stories_types', function($types) {
$types['product'] = array(
'label' => 'Product Story',
'icon' => 'dashicons-cart',
'fields' => array(
'product_id' => array(
'type' => 'select',
'label' => 'Select Product',
'options' => 'callback:get_products'
)
)
);
return $types;
});
Performance Optimization
Caching
// Object caching
$cache_key = 'wp_stories_' . $user_id;
$stories = wp_cache_get($cache_key);
if (false === $stories) {
$stories = get_wp_stories($box_id);
wp_cache_set($cache_key, $stories, '', 3600);
}
// Transients
$transient_key = 'wp_stories_popular';
$popular = get_transient($transient_key);
if (false === $popular) {
$popular = calculate_popular_stories();
set_transient($transient_key, $popular, DAY_IN_SECONDS);
}
Asset Optimization
// Conditional loading
add_action('wp_enqueue_scripts', function() {
if (is_page() && has_shortcode(get_the_content(), 'wp-stories')) {
wp_enqueue_script('wp-stories');
wp_enqueue_style('wp-stories');
}
});
// Async/defer loading
add_filter('script_loader_tag', function($tag, $handle) {
if ('wp-stories' === $handle) {
return str_replace(' src', ' defer src', $tag);
}
return $tag;
}, 10, 2);
Database Optimization
// Batch operations
function wp_stories_bulk_delete($story_ids) {
global $wpdb;
$placeholders = array_fill(0, count($story_ids), '%d');
$format = implode(', ', $placeholders);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->posts}
WHERE ID IN ($format)
AND post_type = 'wb-story'",
$story_ids
)
);
}
// Indexed queries
add_action('init', function() {
// Add index for better performance
add_post_meta_index('wb_story_expires');
});
Testing
Unit Testing
class WP_Stories_Test extends WP_UnitTestCase {
public function test_story_creation() {
$story_id = wp_stories_create_story(array(
'title' => 'Test Story',
'items' => array()
));
$this->assertIsInt($story_id);
$this->assertGreaterThan(0, $story_id);
}
public function test_story_deletion() {
$story_id = $this->factory->post->create(array(
'post_type' => 'wb-story'
));
$result = wp_stories_delete_story($story_id);
$this->assertTrue($result);
}
}
Integration Testing
// Jest tests
describe('Story Editor', () => {
test('opens with image', () => {
const editor = new WPStoriesEditor();
editor.open('test.jpg');
expect(editor.isOpen).toBe(true);
});
test('saves edited image', (done) => {
const editor = new WPStoriesEditor();
editor.open('test.jpg', {
onSave: (dataURL) => {
expect(dataURL).toContain('data:image');
done();
}
});
editor.save();
});
});
E2E Testing
// Cypress tests
describe('Story Creation Flow', () => {
it('creates a new story', () => {
cy.visit('/wp-admin');
cy.get('#menu-posts-wb-story').click();
cy.get('.page-title-action').click();
cy.get('#title').type('Test Story');
cy.get('#publish').click();
cy.contains('Story published');
});
});
Contributing
Development Setup
# Clone repository
git clone https://github.com/wbcomdesigns/wp-stories.git
# Install dependencies
npm install
composer install
# Build assets
npm run build
# Watch for changes
npm run watch
# Run tests
npm test
phpunit
Coding Standards
Follow WordPress Coding Standards:
// PHP
function wp_stories_example_function( $param1, $param2 ) {
if ( ! empty( $param1 ) ) {
return sanitize_text_field( $param1 );
}
return $param2;
}
// JavaScript
function wpStoriesExampleFunction( param1, param2 ) {
if ( param1 ) {
return param1.trim();
}
return param2;
}
Code Review Checklist
- Follows WordPress coding standards
- Includes inline documentation
- Has unit tests
- Passes all existing tests
- Updates documentation
- Maintains backward compatibility
- Includes security considerations
- Performance optimized
API Reference
Global Functions
// Core functions
get_wp_stories($box_id, $author, $query_args, $return_ids, $story_args)
get_wp_story_items($items, $box_id, $author, $story_id)
get_wp_stories_user_avatar($user_id, $size)
get_wp_stories_user_name($user_id)
// Display functions
wp_stories_display($args)
wp_stories_shortcode($atts)
wp_stories_viewer($story_id)
// CRUD operations
wp_stories_create_story($data)
wp_stories_update_story($story_id, $data)
wp_stories_delete_story($story_id)
wp_stories_get_story($story_id)
// User functions
wp_stories_user_can_create($user_id)
wp_stories_user_can_view($story_id, $user_id)
wp_stories_user_can_delete($story_id, $user_id)
wp_stories_get_user_stories($user_id)
Classes
// Main classes
Wp_Stories // Core plugin class
Wp_Stories_Admin // Admin functionality
Wp_Stories_Public // Frontend functionality
Wp_Stories_Activator // Activation hooks
Wp_Stories_Deactivator // Deactivation hooks
Wp_Stories_i18n // Internationalization
Wp_Stories_Loader // Hook loader
// Widget classes
WP_Stories_Activity_Feed_Widget // Activity feed widget
WP_Stories_User_Public_Stories_Widget // User stories widget
WP_Stories_User_Single_Stories_Widget // Single story widget
// Submit handler
Wp_Stories_Submit_User_Stories // User submission handler
JavaScript Objects
// Global objects
WPStoriesEditorBridge // Image editor interface
WPStoriesViewer // Story viewer
WPStoriesUploader // File uploader
WPStoriesNotifications // Notification system
// jQuery plugins
$.fn.wpStoriesCreator // Story creator
$.fn.wpStoriesVueEditor // Vue editor wrapper
$.fn.wpStoriesViewer // Viewer initializer
Resources
Documentation
Tools
