CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pelican

Static site generator supporting Markdown and reStructuredText

Pending
Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

Signal-based plugin architecture that enables extensibility through custom generators, content processors, output handlers, and theme enhancements. The plugin system provides hooks throughout the site generation pipeline.

Capabilities

Plugin Signals

Core signals that plugins can connect to for extending Pelican functionality.

from pelican.plugins import signals

# Run-level lifecycle signals
signals.initialized                    # Fired after Pelican initialization
signals.get_generators                # Request additional generators
signals.all_generators_finalized     # Fired after all generators complete
signals.get_writer                   # Request custom writer
signals.finalized                    # Fired after site generation completion

# Reader-level signals
signals.readers_init                  # After readers initialization

# Generator-level signals
signals.generator_init               # Generic generator initialization
signals.article_generator_init       # Before article generator initialization
signals.article_generator_pretaxonomy # Before article taxonomy processing
signals.article_generator_finalized  # After article generator completion
signals.article_generator_write_article # Before writing individual articles
signals.article_writer_finalized    # After article writer completion

signals.page_generator_init          # Before page generator initialization
signals.page_generator_finalized    # After page generator completion
signals.page_generator_write_page    # Before writing individual pages
signals.page_writer_finalized       # After page writer completion

signals.static_generator_init        # Before static generator initialization
signals.static_generator_finalized  # After static generator completion

# Content-level signals
signals.article_generator_preread    # Before reading article content
signals.article_generator_context    # After article context generation
signals.page_generator_preread       # Before reading page content
signals.page_generator_context       # After page context generation
signals.static_generator_preread     # Before reading static content
signals.static_generator_context     # After static context generation

signals.content_object_init          # After content object initialization

# Writer signals
signals.content_written              # After content file is written
signals.feed_generated               # After feed generation
signals.feed_written                 # After feed file is written

Plugin Utilities

Helper functions for plugin development and management.

def load_plugins(settings: dict) -> list:
    """
    Load enabled plugins from settings.
    
    Parameters:
    - settings (dict): Site settings dictionary
    
    Returns:
    list: List of loaded plugin modules
    """

def get_plugin_name(plugin) -> str:
    """
    Get display name for a plugin.
    
    Parameters:
    - plugin: Plugin module or object
    
    Returns:
    str: Plugin display name
    """

def plugin_enabled(plugin_name: str, settings: dict) -> bool:
    """
    Check if a specific plugin is enabled.
    
    Parameters:
    - plugin_name (str): Name of plugin to check
    - settings (dict): Site settings dictionary
    
    Returns:
    bool: True if plugin is enabled
    """

def list_plugins() -> None:
    """List all available plugins in plugin paths."""

Plugin Development

Basic Plugin Structure

# my_plugin.py - Basic plugin example
from pelican import signals

def initialize_plugin(pelican):
    """Initialize plugin with Pelican instance."""
    print(f"Plugin initialized for site: {pelican.settings['SITENAME']}")

def modify_articles(generator):
    """Modify articles after generation."""
    for article in generator.articles:
        # Add custom processing
        article.custom_field = "Added by plugin"

def register():
    """Register plugin signals."""
    signals.initialized.connect(initialize_plugin)
    signals.article_generator_finalized.connect(modify_articles)

Content Processing Plugin

# content_processor.py - Content modification plugin
from pelican import signals
import re

def process_content(content):
    """Process content object after initialization."""
    if hasattr(content, 'content'):
        # Add custom processing to content HTML
        content.content = enhance_content(content.content)
        
        # Add metadata
        content.word_count = len(content.content.split())
        content.reading_time = max(1, content.word_count // 200)

def enhance_content(html_content):
    """Enhance HTML content with custom features."""
    # Add responsive image classes
    html_content = re.sub(
        r'<img([^>]+)>',
        r'<img\1 class="responsive-image">',
        html_content
    )
    
    # Wrap tables for responsive design
    html_content = re.sub(
        r'<table([^>]*)>',
        r'<div class="table-wrapper"><table\1>',
        html_content
    )
    html_content = re.sub(
        r'</table>',
        r'</table></div>',
        html_content
    )
    
    return html_content

def register():
    """Register plugin signals."""
    signals.content_object_init.connect(process_content)

Custom Generator Plugin

# custom_generator.py - Custom content generator
from pelican import signals
from pelican.generators import Generator
import json
from pathlib import Path

class DataGenerator(Generator):
    """Generator for JSON data files."""
    
    def generate_context(self):
        """Generate context from JSON data files."""
        data_path = Path(self.path) / 'data'
        if not data_path.exists():
            return
        
        data_files = {}
        for json_file in data_path.glob('*.json'):
            with open(json_file) as f:
                data_files[json_file.stem] = json.load(f)
        
        self.context['data_files'] = data_files
    
    def generate_output(self, writer):
        """Generate data-driven pages."""
        if 'data_files' not in self.context:
            return
        
        template = self.get_template('data_page.html')
        
        for name, data in self.context['data_files'].items():
            writer.write_file(
                f'data/{name}.html',
                template,
                self.context,
                data=data,
                page_name=name
            )

def add_generator(pelican):
    """Add custom generator to Pelican."""
    return DataGenerator

def register():
    """Register plugin."""
    signals.get_generators.connect(add_generator)

Template Enhancement Plugin

# template_helpers.py - Template helper functions
from pelican import signals
from jinja2 import Markup
import markdown

def markdown_filter(text):
    """Jinja2 filter to render Markdown in templates."""
    md = markdown.Markdown(extensions=['extra', 'codehilite'])
    return Markup(md.convert(text))

def reading_time_filter(content):
    """Calculate reading time for content."""
    word_count = len(content.split())
    return max(1, word_count // 200)

def related_posts_filter(article, articles, count=5):
    """Find related posts based on tags."""
    if not hasattr(article, 'tags') or not article.tags:
        return []
    
    article_tags = set(tag.name for tag in article.tags)
    related = []
    
    for other in articles:
        if other == article:
            continue
        if hasattr(other, 'tags') and other.tags:
            other_tags = set(tag.name for tag in other.tags)
            common_tags = len(article_tags & other_tags)
            if common_tags > 0:
                related.append((other, common_tags))
    
    # Sort by number of common tags and return top N
    related.sort(key=lambda x: x[1], reverse=True)
    return [post for post, _ in related[:count]]

def add_template_filters(pelican):
    """Add custom template filters."""
    pelican.env.filters.update({
        'markdown': markdown_filter,
        'reading_time': reading_time_filter,
        'related_posts': related_posts_filter,
    })

def register():
    """Register plugin."""
    signals.initialized.connect(add_template_filters)

Plugin Configuration

Enabling Plugins

# pelicanconf.py - Plugin configuration
PLUGINS = [
    'my_plugin',
    'content_processor', 
    'custom_generator',
    'template_helpers',
    'sitemap',
    'related_posts',
]

# Plugin search paths
PLUGIN_PATHS = [
    'plugins',
    '/usr/local/lib/pelican/plugins',
]

# Plugin-specific settings
PLUGIN_SETTINGS = {
    'my_plugin': {
        'option1': 'value1',
        'option2': True,
    },
    'sitemap': {
        'format': 'xml',
        'priorities': {
            'articles': 0.8,
            'pages': 0.5,
            'indexes': 1.0,
        }
    }
}

Plugin Installation

# Install plugin from Git repository
cd plugins/
git clone https://github.com/author/pelican-plugin.git

# Install via pip (if packaged)
pip install pelican-plugin-name

# Manual installation
mkdir -p plugins/my_plugin/
cp my_plugin.py plugins/my_plugin/__init__.py

Advanced Plugin Examples

SEO Enhancement Plugin

# seo_plugin.py - SEO optimization plugin
from pelican import signals
from pelican.contents import Article, Page
import re
from urllib.parse import urljoin

def add_seo_metadata(content):
    """Add SEO metadata to content."""
    if isinstance(content, (Article, Page)):
        # Generate meta description from summary or content
        if hasattr(content, 'summary') and content.summary:
            content.meta_description = clean_text(content.summary)[:160]
        else:
            # Extract first paragraph from content
            first_para = extract_first_paragraph(content.content)
            content.meta_description = clean_text(first_para)[:160]
        
        # Generate Open Graph metadata
        content.og_title = content.title
        content.og_description = content.meta_description
        content.og_type = 'article' if isinstance(content, Article) else 'website'
        
        # Add canonical URL
        if hasattr(content, 'url'):
            content.canonical_url = urljoin(content.settings['SITEURL'], content.url)

def clean_text(html_text):
    """Remove HTML tags and clean text."""
    clean = re.sub(r'<[^>]+>', '', html_text)
    clean = re.sub(r'\s+', ' ', clean).strip()
    return clean

def extract_first_paragraph(html_content):
    """Extract first paragraph from HTML content."""
    match = re.search(r'<p[^>]*>(.*?)</p>', html_content, re.DOTALL)
    return match.group(1) if match else html_content[:200]

def generate_sitemap(generator):
    """Generate XML sitemap."""
    if not hasattr(generator, 'articles'):
        return
    
    sitemap_entries = []
    
    # Add articles
    for article in generator.articles:
        sitemap_entries.append({
            'url': urljoin(generator.settings['SITEURL'], article.url),
            'lastmod': article.date.strftime('%Y-%m-%d'),
            'priority': '0.8'
        })
    
    # Add pages
    if hasattr(generator, 'pages'):
        for page in generator.pages:
            sitemap_entries.append({
                'url': urljoin(generator.settings['SITEURL'], page.url),
                'lastmod': page.date.strftime('%Y-%m-%d') if hasattr(page, 'date') else '',
                'priority': '0.5'
            })
    
    # Generate sitemap XML
    sitemap_xml = generate_sitemap_xml(sitemap_entries)
    
    # Write sitemap file
    output_path = generator.output_path
    with open(f"{output_path}/sitemap.xml", 'w') as f:
        f.write(sitemap_xml)

def generate_sitemap_xml(entries):
    """Generate XML sitemap content."""
    xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
    xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
    
    for entry in entries:
        xml += '  <url>\n'
        xml += f'    <loc>{entry["url"]}</loc>\n'
        if entry['lastmod']:
            xml += f'    <lastmod>{entry["lastmod"]}</lastmod>\n'
        xml += f'    <priority>{entry["priority"]}</priority>\n'
        xml += '  </url>\n'
    
    xml += '</urlset>\n'
    return xml

def register():
    """Register SEO plugin."""
    signals.content_object_init.connect(add_seo_metadata)
    signals.all_generators_finalized.connect(generate_sitemap)

Analytics Plugin

# analytics_plugin.py - Site analytics plugin
from pelican import signals
import json
from collections import defaultdict, Counter
from datetime import datetime

class SiteAnalytics:
    """Site analytics collector."""
    
    def __init__(self):
        self.reset_stats()
    
    def reset_stats(self):
        """Reset analytics statistics."""
        self.stats = {
            'total_articles': 0,
            'total_pages': 0,
            'categories': Counter(),
            'tags': Counter(),
            'authors': Counter(),
            'monthly_posts': defaultdict(int),
            'yearly_posts': defaultdict(int),
            'total_words': 0,
            'avg_reading_time': 0,
        }
    
    def analyze_articles(self, generator):
        """Analyze article statistics."""
        if not hasattr(generator, 'articles'):
            return
        
        articles = generator.articles
        self.stats['total_articles'] = len(articles)
        
        total_words = 0
        
        for article in articles:
            # Category stats
            if hasattr(article, 'category'):
                self.stats['categories'][article.category.name] += 1
            
            # Tag stats
            if hasattr(article, 'tags'):
                for tag in article.tags:
                    self.stats['tags'][tag.name] += 1
            
            # Author stats
            if hasattr(article, 'author'):
                self.stats['authors'][article.author.name] += 1
            
            # Date stats
            if hasattr(article, 'date'):
                month_key = article.date.strftime('%Y-%m')
                year_key = str(article.date.year)
                self.stats['monthly_posts'][month_key] += 1
                self.stats['yearly_posts'][year_key] += 1
            
            # Word count
            if hasattr(article, 'content'):
                word_count = len(article.content.split())
                total_words += word_count
        
        self.stats['total_words'] = total_words
        if articles:
            self.stats['avg_reading_time'] = (total_words // len(articles)) // 200
    
    def analyze_pages(self, generator):
        """Analyze page statistics."""
        if hasattr(generator, 'pages'):
            self.stats['total_pages'] = len(generator.pages)
    
    def generate_report(self, pelican):
        """Generate analytics report."""
        output_path = pelican.output_path
        
        # Convert Counter objects to regular dicts for JSON serialization
        report_data = dict(self.stats)
        report_data['categories'] = dict(report_data['categories'])
        report_data['tags'] = dict(report_data['tags'])
        report_data['authors'] = dict(report_data['authors'])
        report_data['monthly_posts'] = dict(report_data['monthly_posts'])
        report_data['yearly_posts'] = dict(report_data['yearly_posts'])
        report_data['generated_at'] = datetime.now().isoformat()
        
        # Write JSON report
        with open(f"{output_path}/analytics.json", 'w') as f:
            json.dump(report_data, f, indent=2)
        
        # Write human-readable report
        with open(f"{output_path}/analytics.txt", 'w') as f:
            f.write("Site Analytics Report\n")
            f.write("=" * 20 + "\n\n")
            f.write(f"Total Articles: {report_data['total_articles']}\n")
            f.write(f"Total Pages: {report_data['total_pages']}\n")
            f.write(f"Total Words: {report_data['total_words']:,}\n")
            f.write(f"Average Reading Time: {report_data['avg_reading_time']} minutes\n\n")
            
            f.write("Top Categories:\n")
            for cat, count in Counter(report_data['categories']).most_common(10):
                f.write(f"  {cat}: {count}\n")
            
            f.write("\nTop Tags:\n")
            for tag, count in Counter(report_data['tags']).most_common(10):
                f.write(f"  {tag}: {count}\n")

# Global analytics instance
analytics = SiteAnalytics()

def collect_article_stats(generator):
    """Collect article statistics."""
    analytics.analyze_articles(generator)

def collect_page_stats(generator):
    """Collect page statistics."""
    analytics.analyze_pages(generator)

def generate_analytics_report(pelican):
    """Generate final analytics report."""
    analytics.generate_report(pelican)

def register():
    """Register analytics plugin."""
    signals.article_generator_finalized.connect(collect_article_stats)
    signals.page_generator_finalized.connect(collect_page_stats)
    signals.finalized.connect(generate_analytics_report)

Plugin Testing

# test_my_plugin.py - Plugin testing example
import unittest
from unittest.mock import Mock, MagicMock
from pelican.contents import Article
from pelican.settings import DEFAULT_CONFIG
import my_plugin

class TestMyPlugin(unittest.TestCase):
    """Test cases for my_plugin."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.settings = DEFAULT_CONFIG.copy()
        self.pelican_mock = Mock()
        self.pelican_mock.settings = self.settings
    
    def test_initialize_plugin(self):
        """Test plugin initialization."""
        my_plugin.initialize_plugin(self.pelican_mock)
        # Add assertions
    
    def test_modify_articles(self):
        """Test article modification."""
        # Create mock generator with articles
        generator_mock = Mock()
        article_mock = Mock(spec=Article)
        generator_mock.articles = [article_mock]
        
        # Call plugin function
        my_plugin.modify_articles(generator_mock)
        
        # Assert modifications
        self.assertTrue(hasattr(article_mock, 'custom_field'))
        self.assertEqual(article_mock.custom_field, "Added by plugin")

if __name__ == '__main__':
    unittest.main()

Install with Tessl CLI

npx tessl i tessl/pypi-pelican

docs

cli-tools.md

content-generation.md

content-management.md

content-reading.md

index.md

main-application.md

plugin-system.md

settings-configuration.md

utilities.md

tile.json