Build Your First DVP CMS Plugin in 30 Minutes

Beginner 30 minutes Prerequisites: Basic Python knowledge

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:

Plugins let you extend DVP CMS without modifying core code.

Plugin Architecture

The Three Components

Every plugin has three parts:

  1. Plugin Class (plugin.py) - Your code
  2. Metadata (plugin.json) - Plugin information
  3. 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:

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:

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):
name = "my-first-plugin"
version = "0.1.0"
def __init__(self):
    super().__init__()
    self.content_count = 0
def dvp_before_content_create(self, content, metadata=None):
    content['created_at_plugin'] = datetime.now().isoformat()
    return content

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

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:

Check your DVP CMS installation's examples directory for reference implementations.

4. Read More Guides

5. Build Something Useful

Ideas for your next plugin:

Common Mistakes to Avoid

Mistake 1: Forgetting to Return Content

WRONG - No return!
def dvp_before_content_create(self, content, metadata=None):
    content['field'] = 'value'
    # Forgot to return!
CORRECT
def dvp_before_content_create(self, content, metadata=None):
    content['field'] = 'value'
    return content  # ✓

Mistake 2: Not Calling super().__init__()

WRONG
def __init__(self):
    self.my_var = 123
    # Forgot super().__init__()!
CORRECT
def __init__(self):
    super().__init__()  # ✓
    self.my_var = 123

Mistake 3: Using Wrong Hook Signature

WRONG - Missing parameters
def dvp_before_content_update(self, content):
    return content
CORRECT - All required parameters
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?

  1. Check plugin.json is valid JSON
  2. Verify name matches in plugin.json and plugin.py
  3. Ensure __init__.py imports correctly

Tests Failing?

  1. Run pytest -v to see detailed errors
  2. Check you're returning content from filter hooks
  3. Verify you're not modifying original content object

Hook Not Running?

  1. Verify hook name is correct (check hookspec.py)
  2. Ensure plugin is enabled: registry.enable("plugin-name")
  3. Check hook signature matches protocol

Resources