Skip to main content

A comprehensive Wagtail CMS extension providing theming and foundational features.

Project description

Wagtail Feathers

A comprehensive Wagtail CMS extension providing foundational functionality for building sophisticated content management systems.

Python Version Django Version Wagtail Version License

[!CAUTION] This package is a beta version which means it will still have breaking changes and definitely lacks some documentation and tests The package is aimed at new installations

[!IMPORTANT] Expected Release Candidate (RC) in late september 2025, do not use in production

Features

Wagtail Feathers provides a comprehensive set of reusable components and patterns:

๐Ÿ—๏ธ Advanced Page Architecture

  • FeatherBasePage: Foundation for all custom pages with comprehensive SEO capabilities
  • Flexible Content Blocks: Rich StreamField blocks for dynamic content composition
  • Page Relationships: Built-in related page functionality and cross-references

๐Ÿ“‚ Taxonomy & Classification

  • Hierarchical Categories: Tree-structured content classification
  • Flexible Subjects: Tagging and subject matter organization
  • Custom Attributes: Extensible metadata and properties system
  • Admin Integration: Beautiful select widgets and management interface

๐Ÿงญ Dynamic Navigation

  • Multiple Menu Types: Traditional hierarchical and StreamField-based navigation
  • Flexible Menu Items: Support for both page links and external URLs

๐Ÿ” SEO & Metadata

  • Built on Wagtail SEO: Extends Wagtail's built-in seo_title and search_description fields
  • Social Media Ready: Open Graph and Twitter Card support with dedicated image field
  • Structured Data: Automatic JSON-LD generation for articles, organizations, and more
  • Smart Fallbacks: Intelligent content detection for missing SEO fields
  • Search Engine Control: No-index/no-follow directives and robots meta tags
  • Reading Time: Automatic reading time calculation with SEO integration

๐ŸŽจ Theme System

  • Multi-Site Theming: Different themes per site
  • Template Discovery: Automatic theme-based template resolution
  • Asset Management: Theme-specific CSS/JS handling
  • Configuration: Site-wide settings and theme preferences

๐ŸŒ Internationalization (Optional)

  • Locale-Aware Ready: Models ready for i18n when you need it
  • Simple Translation: Use Wagtail's built-in translation features
  • Advanced Workflows: Optional wagtail-localize integration (recommended)

๐Ÿ‘ฅ Authorship & People

  • Author Management: Flexible author attribution system with AuthorType and PageAuthor models
  • People Directory: Staff and contributor profiles with PersonGroup organization
  • Social Integration: Social media links and profiles with SocialMediaSettings

โŒ Error Pages

  • Custom Error Pages: Configurable 404, and 500 error pages (TBC: 400, 403)
  • ErrorPage Model: Editable error pages with custom messages and images

โ“ FAQ System

  • FAQ Management: Structured FAQ system with FAQ and FAQItem models
  • Categorized Content: Organize frequently asked questions by category

๐Ÿ”— Page Relationships

  • Related Content: RelatedPage, RelatedDocument, and RelatedExternalLink models
  • Cross-References: Built-in page relationship functionality
  • Geographic Tagging: PageCountry model for location-based content

Installation

Basic Installation (No i18n)

pip install wagtail-feathers

With Advanced Translation Workflows

pip install wagtail-feathers[localize]

Quick Start

  1. Add to Django Settings:

Basic Setup:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtailmarkdown",
    "django_extensions",
    # ... other apps
]

Theming

# create a folder named "themes" at the 'BASE_DIR' of your project
...
@cached_property
def themes_dir(self) -> Path:
    """Get the themes directory from Django settings."""
    return getattr(settings, "BASE_DIR", Path.cwd()) / "themes"
...

Add all the themes you want, see the demo/themes folder for an example. Adapt the json file accordingly.

With Simple Translation:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtail.contrib.simple_translation",  # Wagtail built-in
    # ... other apps
]

With Advanced Translation:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtail_localize",
    "wagtail_localize.locales",
    # ... other apps
]
  1. Run Migrations:
python manage.py migrate
  1. Create Your First Page:
from wagtail_feathers.models import FeatherBasePage

class MyPage(FeatherBasePage):
    # Your custom fields here
    pass

Advanced Usage

Custom Page Models

from wagtail_feathers.models import FeatherBasePage, ItemPage
from wagtail_feathers.blocks import CommonContentBlock
from wagtail.fields import StreamField

class ArticlePage(ItemPage):
    body = StreamField([
        ("content", CommonContentBlock()),
    ], use_json_field=True)
    
    content_panels = ItemPage.content_panels + [
        FieldPanel("body"),
    ]

Taxonomy Integration

from wagtail_feathers.models import Category, Classifier, ClassifierGroup

# Create categories
category = Category.objects.create(name="Technology", slug="tech")

# Create classifier groups and classifiers
group = ClassifierGroup.objects.create(name="Topics", max_selections=3)
classifier = Classifier.objects.create(
    name="AI & Machine Learning",
    slug="ai-ml",
    group=group
)

Navigation Menus

from wagtail_feathers.models import Menu, MenuItem, FlatMenu, NestedMenu

# Create a flat menu
flat_menu = FlatMenu.objects.create(name="Main Navigation")

# Create a nested menu
nested_menu = NestedMenu.objects.create(name="Footer Navigation")

# Create menu items
MenuItem.objects.create(
    menu=flat_menu,
    link_page=home_page,
    link_text="Home",
    sort_order=1
)

Error Pages

from wagtail_feathers.models import ErrorPage

# Create custom error pages
error_404 = ErrorPage.objects.create(
    title="404",
    error_code="404",
    heading="Page Not Found",
    message="<p>The page you are looking for does not exist.</p>"
)

FAQ System

from wagtail_feathers.models import FAQ, FAQItem

# Create FAQ categories and items
category = FAQ.objects.create(
        name="General Questions",
        slug="general"
)

faq = FAQItem.objects.create(
        question="How do I get started?",
        answer="<p>Follow our quick start guide...</p>",
        faq=category
)

SEO & Metadata

from wagtail_feathers.models import FeatherBasePage
from wagtail_feathers.models.seo import SeoContentType, TwitterCardType

class ArticlePage(FeatherBasePage):
    # SEO fields are automatically available via SeoMixin
    pass

# In your templates - add to <head> section:
# {% load seo_tags %}
# {% include "wagtail_feathers/seo/meta.html" %}

# In your templates - add before </body>:
# {% include "wagtail_feathers/seo/structured_data.html" %}

# Programmatic SEO control:
page = ArticlePage.objects.get(pk=1)
page.seo_title = "Custom SEO Title"  # Uses Wagtail's built-in field
page.search_description = "Custom meta description for search engines"  # Uses Wagtail's built-in field
page.seo_content_type = SeoContentType.ARTICLE
page.twitter_card_type = TwitterCardType.SUMMARY_LARGE_IMAGE
page.save()

Reading Time Features

All ItemPage models automatically include reading time calculation:

from wagtail_feathers.models import ItemPage

class BlogPost(ItemPage):
    # Reading time is automatically calculated from:
    # - title, introduction, body content
    # - Configurable words-per-minute (default: 200 WPM)
    pass

# In templates:
# {% load reading_time_tags %}
# {% reading_time page %}  # "5 min read"
# {% reading_time page "long" %}  # "5 minutes read"  
# {% reading_time_badge page %}  # Badge with clock icon

# Custom reading time logic:
class TechnicalArticle(ItemPage):
    summary = RichTextField(blank=True)
    
    def get_additional_word_sources(self):
        """Include summary in word count."""
        words = 0
        if self.summary:
            words += self._count_text_words(str(self.summary))
        return words
    
    # Slower reading speed for technical content
    words_per_minute = 150

# Reading time extends SEO automatically:
# - Twitter Cards: "Reading time: 5 min read"
# - Structured Data: "timeRequired": "PT5M"
# - Meta tags: article:reading_time

Extending SEO Features

The SEO functionality in wagtail_feathers is designed to be extensible. Here are several ways to enhance it:

1. Adding Custom SEO Fields

from wagtail_feathers.models import FeatherBasePage, SeoMixin
from wagtail.admin.panels import FieldPanel

class ArticlePage(FeatherBasePage):
    # Add your own SEO-related fields
    meta_keywords = models.CharField(
        max_length=255, 
        blank=True,
        help_text="Comma-separated keywords (optional for modern SEO)"
    )
    
    facebook_app_id = models.CharField(
        max_length=50,
        blank=True,
        help_text="Facebook App ID for Open Graph"
    )
    
    # Extend the SEO panels
    seo_panels = SeoMixin.seo_panels + [
        FieldPanel("meta_keywords"),
        FieldPanel("facebook_app_id"),
    ]
    
    # Override SEO methods for custom behavior
    def get_seo_description(self):
        """Custom SEO description logic."""
        # Try your custom field first
        if hasattr(self, "excerpt") and self.excerpt:
            return self.excerpt[:160]
        
        # Fall back to parent implementation
        return super().get_seo_description()

2. Custom Structured Data

import json
from wagtail_feathers.models import SeoMixin

class ProductPage(FeatherBasePage):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    brand = models.CharField(max_length=100)
    sku = models.CharField(max_length=50)
    
    def get_structured_data(self):
        """Override to add Product schema."""
        if self.seo_content_type == "product":
            site = self.get_site()
            base_url = site.root_url if site.root_url else f"https://{site.hostname}"
            
            schema = {
                "@context": "https://schema.org",
                "@type": "Product",
                "name": self.get_seo_title(),
                "description": self.get_seo_description(),
                "sku": self.sku,
                "brand": {
                    "@type": "Brand",
                    "name": self.brand
                },
                "offers": {
                    "@type": "Offer",
                    "price": str(self.price),
                    "priceCurrency": "USD",
                    "availability": "https://schema.org/InStock"
                }
            }
            
            # Add image if available
            if self.get_seo_image():
                schema["image"] = f"{base_url}{self.get_seo_image().file.url}"
                
            return json.dumps(schema, ensure_ascii=False)
        
        # Fall back to parent implementation
        return super().get_structured_data()

3. SEO-Focused Page Types

from wagtail_feathers.models import ItemPage
from wagtail_feathers.models.seo import SeoContentType, TwitterCardType

class SEOOptimizedArticle(ItemPage):
    """Article page optimized for SEO best practices."""
    
    # Set SEO defaults
    seo_content_type = SeoContentType.ARTICLE
    twitter_card_type = TwitterCardType.SUMMARY_LARGE_IMAGE
    
    # Additional SEO fields
    focus_keyword = models.CharField(
        max_length=100,
        blank=True,
        help_text="Primary keyword/phrase for this article"
    )
    
    reading_time = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Estimated reading time in minutes"
    )
    
    content_panels = ItemPage.content_panels + [
        FieldPanel("focus_keyword"),
        FieldPanel("reading_time"),
    ]
    
    def get_structured_data(self):
        """Enhanced article schema with reading time."""
        schema_str = super().get_structured_data()
        if schema_str and self.seo_content_type == SeoContentType.ARTICLE:
            schema = json.loads(schema_str)
            
            # Add reading time
            if self.reading_time:
                schema["timeRequired"] = f"PT{self.reading_time}M"
            
            # Add focus keyword as about
            if self.focus_keyword:
                schema["about"] = {
                    "@type": "Thing",
                    "name": self.focus_keyword
                }
                
            return json.dumps(schema, ensure_ascii=False)
        
        return schema_str

4. Custom SEO Template Tags

# In your app"s templatetags/custom_seo_tags.py
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.simple_tag
def custom_meta_tags(page):
    """Add custom meta tags based on page type."""
    tags = []
    
    # Add article-specific tags
    if hasattr(page, "focus_keyword") and page.focus_keyword:
        tags.append(f"<meta name='keywords' content='{page.focus_keyword}'>")
    
    # Add Facebook App ID if available
    if hasattr(page, "facebook_app_id") and page.facebook_app_id:
        tags.append(f"<meta property='fb:app_id' content='{page.facebook_app_id}'>")
    
    # Add reading time for articles
    if hasattr(page, "reading_time") and page.reading_time:
        tags.append(f"<meta name='twitter:label1' content="Reading time">")
        tags.append(f"<meta name='twitter:data1' content='{page.reading_time} min read'>")
    
    return mark_safe("\n".join(tags))

# In your templates:
# {% load custom_seo_tags %}
# {% custom_meta_tags page %}

5. SEO Analytics Integration

class AnalyticsEnhancedPage(FeatherBasePage):
    """Page with SEO analytics tracking."""
    
    google_analytics_id = models.CharField(
        max_length=20,
        blank=True,
        help_text="Google Analytics tracking ID (e.g., GA-XXXXX-X)"
    )
    
    track_scroll_depth = models.BooleanField(
        default=False,
        help_text="Enable scroll depth tracking for SEO insights"
    )
    
    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        
        # Add SEO tracking context
        context.update({
            "enable_seo_tracking": self.track_scroll_depth,
            "ga_tracking_id": self.google_analytics_id,
        })
        
        return context

6. Multi-language SEO

# If using wagtail-localize
from wagtail_feathers.models.seo import SeoMixin
from wagtail_localize.fields import TranslatableField

class MultilingualSEOPage(FeatherBasePage):
    """Page with enhanced multilingual SEO support."""
    
    # Additional translatable SEO fields
    local_keywords = models.CharField(
        max_length=255,
        blank=True,
        help_text="Keywords in local language"
    )
    
    cultural_context = models.TextField(
        blank=True,
        help_text="Cultural context for this market"
    )
    
    # Add to translatable fields
    if SeoMixin.WAGTAIL_LOCALIZE_AVAILABLE:
        translatable_fields = SeoMixin.translatable_fields + [
            TranslatableField("local_keywords"),
            TranslatableField("cultural_context"),
        ]
    
    def get_seo_description(self):
        """Localized SEO description."""
        if self.cultural_context:
            return f"{super().get_seo_description()} {self.cultural_context}"[:160]
        return super().get_seo_description()

7. Using Extension Points

The SeoMixin provides several extension points for easy customization:

class BlogArticlePage(ItemPage):
    """Blog article with enhanced SEO using extension points."""
    
    author_twitter = models.CharField(max_length=50, blank=True)
    focus_keyword = models.CharField(max_length=100, blank=True)
    # Note: reading_time is automatically provided by ItemPage
    
    def get_custom_meta_tags(self):
        """Add custom meta tags."""
        tags = {}
        
        if self.focus_keyword:
            tags["keywords"] = self.focus_keyword
        
        # Reading time is automatically added via ReadingTimeMixin
        # but you can customize it here if needed
            
        return tags
    
    def get_custom_twitter_tags(self):
        """Add custom Twitter Card tags."""
        tags = {}
        
        if self.author_twitter:
            tags["twitter:creator"] = f"@{self.author_twitter}"
        
        # Reading time Twitter tags are automatically added via ReadingTimeMixin
        # Additional Twitter tags can be added here
            
        return tags
    
    def get_custom_og_tags(self):
        """Add custom Open Graph tags."""
        tags = {}
        
        if self.author_twitter:
            tags["article:author"] = f"https://twitter.com/{self.author_twitter}"
            
        return tags
    
    def get_sitemap_priority(self):
        """Higher priority for recent articles and longer reads."""
        from django.utils import timezone
        from datetime import timedelta
        
        base_priority = super().get_sitemap_priority()
        
        # Boost recent articles
        if hasattr(self, "publication_date"):
            recent_threshold = timezone.now().date() - timedelta(days=30)
            if self.publication_date >= recent_threshold:
                base_priority += 0.1
        
        # Boost longer articles (more comprehensive content)
        if self.reading_time_minutes and self.reading_time_minutes >= 10:
            base_priority += 0.1
                
        return min(1.0, base_priority)  # Cap at 1.0

These examples show how to build upon wagtail_feathers SEO foundation to create powerful, customized SEO solutions for specific use cases.

People & Authors

from wagtail_feathers.models import Person, PersonGroup, AuthorType, PageAuthor

# Create person groups and people
group = PersonGroup.objects.create(name="Editorial Team")
person = Person.objects.create(
    first_name="John",
    last_name="Doe",
    email="john@example.com"
)
person.groups.add(group)

# Create author types and assign to pages
author_type = AuthorType.objects.create(name="Writer")
PageAuthor.objects.create(
    page=my_page,
    person=person,
    author_type=author_type
)

Requirements

  • Python 3.11+
  • Django 5.0+
  • Wagtail 5.2+

See pyproject.toml for complete dependency list.

Development

Setup Development Environment

# Clone the repository
git clone https://github.com/softquantum/wagtail-feathers.git
cd wagtail-feathers

# Navigate to demo directory
cd demo

# Install demo dependencies (includes editable package install)
pip install -r requirements.txt

# Set up database and demo data
python manage.py migrate
python manage.py setup_demo_data

# Start the demo server
python manage.py runserver
# Run tests
pytest

# Run tests across multiple environments
tox

Try the Demo Site

Demo Access:

The demo includes (WORK IN PROGRESS):

  • Sample pages using wagtail-feathers features
  • SEO optimization examples
  • Reading time calculation
  • Taxonomy and navigation examples
  • Live package development (changes reflect immediately)

Testing

The package includes some tests covering:

  • Model functionality and relationships
  • Template rendering and themes
  • Navigation generation
  • Taxonomy management
  • Multi-site behavior
# Run all tests
pytest

Code Quality

# Lint code
ruff check src/

# Format code  
ruff format src/

# Run via tox
tox -e lint
tox -e format

Architecture

Wagtail Feathers follows a modular architecture:

wagtail_feathers/
โ”œโ”€โ”€ models/           # Core model definitions
โ”‚   โ”œโ”€โ”€ base.py      # FeatherBasePage and foundation classes
โ”‚   โ”œโ”€โ”€ author.py    # AuthorType and PageAuthor models
โ”‚   โ”œโ”€โ”€ errors.py    # ErrorPage model
โ”‚   โ”œโ”€โ”€ faq.py       # FAQ and FAQItem models  
โ”‚   โ”œโ”€โ”€ geographic.py # PageCountry model
โ”‚   โ”œโ”€โ”€ inline.py    # Related content models
โ”‚   โ”œโ”€โ”€ navigation.py # Menu and navigation models
โ”‚   โ”œโ”€โ”€ person.py    # Person and PersonGroup models
โ”‚   โ”œโ”€โ”€ settings.py  # SiteSettings model
โ”‚   โ”œโ”€โ”€ social.py    # Social media models
โ”‚   โ”œโ”€โ”€ specialized_pages.py # Specialized page types
โ”‚   โ”œโ”€โ”€ taxonomy.py  # Category and Classifier models
โ”‚   โ””โ”€โ”€ utils.py     # Model utilities
โ”œโ”€โ”€ blocks.py        # StreamField content blocks
โ”œโ”€โ”€ templatetags/    # Template tag libraries
โ”œโ”€โ”€ viewsets/        # Wagtail admin viewsets
โ”œโ”€โ”€ themes.py        # Theme system
โ””โ”€โ”€ management/      # Django management commands

Key Models

Based on the migration, wagtail_feathers provides these core models:

  • Page Foundation: FeatherBasePage, FeatherBasePageTag
  • Taxonomy: Category, Classifier, ClassifierGroup, PageCategory, PageClassifier
  • Navigation: Menu, MenuItem, FlatMenu, NestedMenu, Footer, FooterNavigation
  • People & Authors: Person, PersonGroup, AuthorType, PageAuthor
  • Content Organization: FAQ, FAQItem, ErrorPage
  • Relationships: RelatedPage, RelatedDocument, RelatedExternalLink
  • Geography: PageCountry
  • Social: SocialMediaSettings, SocialMediaLink
  • Settings: SiteSettings

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

BSD-3-Clause

Credits & Attributions

  • โœ… Wagtail Feathers is Developed by Maxime Decooman.
  • โœ… Built on the excellent Wagtail CMS (License BSD-3-Clause)
  • โœ… The icons prefixed "heroicons-" are sourced from version 2.2.0 of Heroicons, the beautiful hand-crafted SVG icons library, by the makers of Tailwind CSS (License MIT).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wagtail_feathers-1.0rc2.tar.gz (701.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wagtail_feathers-1.0rc2-py3-none-any.whl (587.3 kB view details)

Uploaded Python 3

File details

Details for the file wagtail_feathers-1.0rc2.tar.gz.

File metadata

  • Download URL: wagtail_feathers-1.0rc2.tar.gz
  • Upload date:
  • Size: 701.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.5

File hashes

Hashes for wagtail_feathers-1.0rc2.tar.gz
Algorithm Hash digest
SHA256 c6765f9b11809305e5cb26c38fc2530ee05304b2b5cfedefe01fb98a981ec7a2
MD5 6ba2eb3ed3b93874278b615d809e4983
BLAKE2b-256 6e054957f0ac8c221c9a230e08f2764d57efaffa50a5278d48ba5a55f4470478

See more details on using hashes here.

File details

Details for the file wagtail_feathers-1.0rc2-py3-none-any.whl.

File metadata

File hashes

Hashes for wagtail_feathers-1.0rc2-py3-none-any.whl
Algorithm Hash digest
SHA256 3ee88fee4416df3c4c6f56298489a7810a30e0a7e32dd9a278f5fb06675a3be9
MD5 d8781943a7fb8e4af111d51c9d5dc6a4
BLAKE2b-256 e8b70ed7b74857f23de46d8fac9514a2844b77d30ecb90b6cb50eb891e8fc7b7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page