This guide will walk you through creating a working DVP CMS plugin from scratch.
What is a Plugin?
A DVP CMS plugin is a Python class that:
- Inherits from
Pluginbase class - Implements one or more hook protocols
- Can modify content, perform actions, or add functionality
- Is registered with the plugin registry
- Can be enabled/disabled independently
Plugins let you extend DVP CMS without modifying core code.
Plugin Architecture
The Three Components
Every plugin has three parts:
- Plugin Class (
plugin.py) - Your code - Metadata (
plugin.json) - Plugin information - Package (
__init__.py) - Python package file
my-plugin/
├── plugin.py # Your plugin code
├── plugin.json # Metadata
└── __init__.py # Package initialization
Hook Types
DVP CMS has two types of hooks:
Filter Hooks - Modify and return data:
def dvp_before_content_create(self, content, metadata=None):
content['modified'] = True
return content # MUST return modified content
Action Hooks - Perform actions without returning:
def dvp_content_created(self, content_id, content, metadata=None):
print(f"Content {content_id} was created!")
# No return needed
Step 1: Project Setup
Create Directory Structure
mkdir -p my-first-plugin
cd my-first-plugin
Required Files
You'll create three files:
plugin.py- Main plugin codeplugin.json- Metadata__init__.py- Package init
Step 2: Create Plugin Files
2.1 Create plugin.json
This file describes your plugin:
{
"name": "my-first-plugin",
"display_name": "My First Plugin",
"version": "0.1.0",
"description": "My first DVP CMS plugin that adds a timestamp to content",
"author": "Your Name",
"author_email": "you@example.com",
"license": "AGPL-3.0",
"keywords": ["example", "tutorial", "timestamp"],
"category": "content",
"requires_dvp_version": ">=0.1.0",
"python_requires": ">=3.9",
"dependencies": [],
"hooks": [
"dvp_before_content_create"
],
"entry_point": "my_first_plugin:MyFirstPlugin"
}
Key fields:
name: Unique identifier (lowercase, hyphens)hooks: List of hooks your plugin implementsentry_point: How to import your plugin
2.2 Create __init__.py
This makes your directory a Python package:
"""My First Plugin for DVP CMS"""
try:
from .plugin import MyFirstPlugin
except ImportError:
from plugin import MyFirstPlugin
__all__ = ["MyFirstPlugin"]
__version__ = "0.1.0"
Step 3: Implement Your Plugin
3.1 Create plugin.py
Here's the full plugin:
"""
My First Plugin for DVP CMS
Adds a timestamp to all content before creation.
"""
from typing import Any, Dict, Optional
from datetime import datetime
from dvp_cms.plugins import Plugin
from dvp_cms.plugins.hookspec import ContentLifecycleHooks
class MyFirstPlugin(Plugin, ContentLifecycleHooks):
"""
Add timestamps to content.
This plugin demonstrates the basics of DVP CMS plugin development.
Example:
>>> from dvp_cms.plugins import PluginRegistry
>>> from my_first_plugin import MyFirstPlugin
>>>
>>> registry = PluginRegistry()
>>> plugin = MyFirstPlugin()
>>> registry.register(plugin)
>>> registry.enable("my-first-plugin")
"""
# Plugin metadata (must match plugin.json)
name = "my-first-plugin"
version = "0.1.0"
description = "Add timestamps to content"
author = "Your Name"
dependencies = []
def __init__(self):
"""Initialize the plugin."""
super().__init__()
self.content_count = 0
def initialize(self) -> None:
"""Called when plugin is enabled."""
super().initialize()
self.logger.info(f"My First Plugin v{self.version} initialized!")
def shutdown(self) -> None:
"""Called when plugin is disabled."""
self.logger.info(
f"My First Plugin shutting down. "
f"Processed {self.content_count} pieces of content."
)
super().shutdown()
def dvp_before_content_create(
self,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Add timestamp before content is created.
This is a FILTER hook - we modify content and return it.
Args:
content: Content data being created
metadata: Optional metadata
Returns:
Modified content with timestamp added
"""
# Add timestamp
content['created_at_plugin'] = datetime.now().isoformat()
content['processed_by'] = self.name
# Increment counter
self.content_count += 1
# Log what we did
self.logger.debug(
f"Added timestamp to: {content.get('title', 'Untitled')}"
)
# IMPORTANT: Always return the modified content
return content
3.2 Understanding the Code
Line-by-Line Breakdown:
class MyFirstPlugin(Plugin, ContentLifecycleHooks):
- Inherit from
Plugin(base class) - Inherit from
ContentLifecycleHooks(hook protocol)
name = "my-first-plugin"
version = "0.1.0"
- Required metadata fields
- Must match
plugin.json
def __init__(self):
super().__init__()
self.content_count = 0
- Initialize plugin
- Call
super().__init__()first! - Set up any instance variables
def dvp_before_content_create(self, content, metadata=None):
content['created_at_plugin'] = datetime.now().isoformat()
return content
- Hook implementation
- Modify content
- Must return content (it's a filter hook!)
Step 4: Test Your Plugin
4.1 Create Test File
Create test_my_first_plugin.py:
"""Tests for My First Plugin"""
import pytest
from my_first_plugin import MyFirstPlugin
def test_plugin_metadata():
"""Test plugin has correct metadata."""
plugin = MyFirstPlugin()
assert plugin.name == "my-first-plugin"
assert plugin.version == "0.1.0"
assert plugin.content_count == 0
def test_adds_timestamp():
"""Test plugin adds timestamp to content."""
plugin = MyFirstPlugin()
# Create test content
content = {
'title': 'Test Article',
'body': 'This is a test.'
}
# Run the hook
result = plugin.dvp_before_content_create(content)
# Check timestamp was added
assert 'created_at_plugin' in result
assert 'processed_by' in result
assert result['processed_by'] == 'my-first-plugin'
# Check counter incremented
assert plugin.content_count == 1
def test_preserves_existing_fields():
"""Test plugin doesn't delete existing fields."""
plugin = MyFirstPlugin()
content = {
'title': 'Test',
'body': 'Content',
'custom_field': 'value'
}
result = plugin.dvp_before_content_create(content)
# Original fields preserved
assert result['title'] == 'Test'
assert result['custom_field'] == 'value'
# New field added
assert 'created_at_plugin' in result
4.2 Run Tests
pytest test_my_first_plugin.py -v
Expected output:
test_my_first_plugin.py::test_plugin_metadata PASSED
test_my_first_plugin.py::test_adds_timestamp PASSED
test_my_first_plugin.py::test_preserves_existing_fields PASSED
3 passed in 0.05s
Step 5: Register and Enable
5.1 Use Your Plugin
Create example_usage.py:
"""Example of using My First Plugin"""
from dvp_cms.plugins import PluginRegistry, call_hook_chain
from my_first_plugin import MyFirstPlugin
import asyncio
async def main():
# Create registry
registry = PluginRegistry()
# Create and register plugin
plugin = MyFirstPlugin()
registry.register(plugin)
# Enable plugin
registry.enable("my-first-plugin")
print("✓ Plugin registered and enabled")
# Create some content
content = {
'title': 'My First Article',
'body': 'This is my first article using my plugin!'
}
print(f"\nBefore plugin: {content}")
# Get plugins for hook
plugins = registry.get_plugins_for_hook("dvp_before_content_create")
# Run hook chain
modified_content = await call_hook_chain(
plugins,
'dvp_before_content_create',
content
)
print(f"\nAfter plugin: {modified_content}")
print(f"\n✓ Timestamp added: {modified_content['created_at_plugin']}")
# Disable plugin
registry.disable("my-first-plugin")
print("\n✓ Plugin disabled")
if __name__ == "__main__":
asyncio.run(main())
5.2 Run Your Example
python example_usage.py
Congratulations!
You've created your first DVP CMS plugin!
What You Learned
- Plugin structure (3 required files)
- How to inherit from Plugin base class
- How to implement hooks
- Filter vs Action hooks
- Plugin lifecycle (initialize/shutdown)
- Testing your plugin
- Registering and enabling plugins
Next Steps
1. Explore More Hooks
Try other hooks from ContentLifecycleHooks:
def dvp_content_created(self, content_id, content, metadata=None):
"""Called AFTER content is created."""
print(f"Content {content_id} was created!")
def dvp_before_content_update(self, content_id, old_content, new_content):
"""Called BEFORE content is updated."""
new_content['updated_at'] = datetime.now().isoformat()
return new_content
2. Add Configuration
Make your plugin configurable:
def __init__(self, timestamp_format: str = "%Y-%m-%d %H:%M:%S"):
super().__init__()
self.timestamp_format = timestamp_format
def dvp_before_content_create(self, content, metadata=None):
content['created_at_plugin'] = datetime.now().strftime(
self.timestamp_format
)
return content
3. Study Example Plugins
DVP CMS includes example plugins demonstrating various patterns:
- hello-world - Simplest example
- markdown-transformer - Content transformation
- seo-optimizer - SEO automation
- content-audit-plugin - Production patterns
Check your DVP CMS installation's examples directory for reference implementations.
4. Read More Guides
- Plugin Best Practices - Production standards
- Testing Plugins - Testing with pytest
- Plugin Deployment - Publishing your plugin
5. Build Something Useful
Ideas for your next plugin:
- Word Counter - Count words and estimate reading time
- Tag Extractor - Extract hashtags from content
- Spell Checker - Validate spelling before creation
- Image Optimizer - Compress images automatically
- Social Sharer - Auto-post to social media
Common Mistakes to Avoid
Mistake 1: Forgetting to Return Content
def dvp_before_content_create(self, content, metadata=None):
content['field'] = 'value'
# Forgot to return!
def dvp_before_content_create(self, content, metadata=None):
content['field'] = 'value'
return content # ✓
Mistake 2: Not Calling super().__init__()
def __init__(self):
self.my_var = 123
# Forgot super().__init__()!
def __init__(self):
super().__init__() # ✓
self.my_var = 123
Mistake 3: Using Wrong Hook Signature
def dvp_before_content_update(self, content):
return content
def dvp_before_content_update(self, content_id, old_content, new_content):
return new_content # ✓
Tip: Always check hookspec.py for correct signatures!
Troubleshooting
Plugin Not Loading?
- Check
plugin.jsonis valid JSON - Verify
namematches inplugin.jsonandplugin.py - Ensure
__init__.pyimports correctly
Tests Failing?
- Run
pytest -vto see detailed errors - Check you're returning content from filter hooks
- Verify you're not modifying original content object
Hook Not Running?
- Verify hook name is correct (check
hookspec.py) - Ensure plugin is enabled:
registry.enable("plugin-name") - Check hook signature matches protocol
Resources
- Example Plugins: Check your DVP CMS installation's examples directory for reference implementations
- Hook Reference: See
hookspec.pyin your DVP CMS installation for full hook documentation - Plugin Documentation: Visit the DVP CMS documentation for more guides